From 3d07f0211dc74710affd9154f61728d77cfb6f4c Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Thu, 7 Jul 2022 00:19:05 +0300 Subject: [PATCH] initial public commit --- .github/FUNDING.yaml | 13 + .github/workflows/release.yaml | 39 + .gitignore | 16 + .goreleaser.yaml | 43 + LICENSE.md | 17 + README.md | 124 + apis/admin.go | 261 + apis/admin_test.go | 654 + apis/base.go | 131 + apis/base_test.go | 122 + apis/collection.go | 185 + apis/collection_test.go | 451 + apis/file.go | 104 + apis/file_test.go | 102 + apis/logs.go | 82 + apis/logs_test.go | 196 + apis/middlewares.go | 277 + apis/middlewares_test.go | 503 + apis/realtime.go | 345 + apis/realtime_test.go | 292 + apis/record.go | 432 + apis/record_test.go | 914 ++ apis/settings.go | 71 + apis/settings_test.go | 188 + apis/user.go | 444 + apis/user_test.go | 900 ++ cmd/migrate.go | 77 + cmd/serve.go | 228 + cmd/version.go | 21 + core/app.go | 424 + core/base.go | 752 + core/base_test.go | 438 + core/db_cgo.go | 26 + core/db_nocgo.go | 16 + core/events.go | 230 + core/settings.go | 412 + core/settings_test.go | 606 + daos/admin.go | 124 + daos/admin_test.go | 238 + daos/base.go | 217 + daos/base_test.go | 245 + daos/collection.go | 163 + daos/collection_test.go | 253 + daos/param.go | 75 + daos/param_test.go | 150 + daos/record.go | 351 + daos/record_expand.go | 155 + daos/record_expand_test.go | 258 + daos/record_test.go | 473 + daos/request.go | 70 + daos/request_test.go | 148 + daos/table.go | 37 + daos/table_test.go | 81 + daos/user.go | 281 + daos/user_test.go | 274 + examples/base/main.go | 26 + forms/admin_login.go | 50 + forms/admin_login_test.go | 80 + forms/admin_password_reset_confirm.go | 76 + forms/admin_password_reset_confirm_test.go | 120 + forms/admin_password_reset_request.go | 70 + forms/admin_password_reset_request_test.go | 84 + forms/admin_upsert.go | 91 + forms/admin_upsert_test.go | 285 + forms/collection_upsert.go | 215 + forms/collection_upsert_test.go | 452 + forms/realtime_subscribe.go | 23 + forms/realtime_subscribe_test.go | 31 + forms/record_upsert.go | 368 + forms/record_upsert_test.go | 498 + forms/settings_upsert.go | 59 + forms/settings_upsert_test.go | 130 + forms/user_email_change_confirm.go | 113 + forms/user_email_change_confirm_test.go | 121 + forms/user_email_change_request.go | 57 + forms/user_email_change_request_test.go | 87 + forms/user_email_login.go | 52 + forms/user_email_login_test.go | 106 + forms/user_oauth2_login.go | 133 + forms/user_oauth2_login_test.go | 75 + forms/user_password_reset_confirm.go | 78 + forms/user_password_reset_confirm_test.go | 165 + forms/user_password_reset_request.go | 70 + forms/user_password_reset_request_test.go | 153 + forms/user_upsert.go | 118 + forms/user_upsert_test.go | 242 + forms/user_verification_confirm.go | 73 + forms/user_verification_confirm_test.go | 140 + forms/user_verification_request.go | 74 + forms/user_verification_request_test.go | 171 + forms/validators/file.go | 63 + forms/validators/file_test.go | 92 + forms/validators/record_data.go | 418 + forms/validators/record_data_test.go | 1443 ++ forms/validators/string.go | 21 + forms/validators/string_test.go | 30 + forms/validators/validators.go | 2 + go.mod | 87 + go.sum | 1151 ++ mails/admin.go | 76 + mails/admin_test.go | 37 + mails/base.go | 58 + mails/templates/admin_password_reset.go | 25 + mails/templates/html_content.go | 8 + mails/templates/layout.go | 117 + mails/templates/user_confirm_email_change.go | 26 + mails/templates/user_password_reset.go | 26 + mails/templates/user_verification.go | 26 + mails/user.go | 192 + mails/user_test.go | 87 + migrations/1640988000_init.go | 141 + migrations/logs/1640988000_init.go | 38 + models/admin.go | 13 + models/admin_test.go | 14 + models/base.go | 131 + models/base_test.go | 178 + models/collection.go | 27 + models/collection_test.go | 25 + models/param.go | 22 + models/param_test.go | 14 + models/record.go | 304 + models/record_test.go | 849 ++ models/request.go | 29 + models/request_test.go | 14 + models/schema/schema.go | 236 + models/schema/schema_field.go | 503 + models/schema/schema_field_test.go | 1344 ++ models/schema/schema_test.go | 414 + models/user.go | 47 + models/user_test.go | 43 + pocketbase.go | 205 + pocketbase_test.go | 96 + resolvers/record_field_resolver.go | 282 + resolvers/record_field_resolver_test.go | 245 + resolvers/resolvers.go | 2 + tests/api.go | 121 + tests/app.go | 447 + tests/data/.gitignore | 2 + tests/data/data.db | Bin 0 -> 159744 bytes tests/data/logs.db | Bin 0 -> 45056 bytes .../01562272-e67e-4925-9f37-02b5f899853c.txt | 1 + ...2272-e67e-4925-9f37-02b5f899853c.txt.attrs | 1 + .../4881bdef-06b4-4dea-8d97-6125ad242677.png | Bin 0 -> 1169 bytes ...bdef-06b4-4dea-8d97-6125ad242677.png.attrs | 1 + ...0_4881bdef-06b4-4dea-8d97-6125ad242677.png | Bin 0 -> 2170 bytes ...bdef-06b4-4dea-8d97-6125ad242677.png.attrs | 1 + .../8fe61d65-6a2e-4f11-87b3-d8a3170bfd4f.txt | 1 + ...1d65-6a2e-4f11-87b3-d8a3170bfd4f.txt.attrs | 1 + .../315d3131-c1f7-453a-8a91-f12c06207edc.png | Bin 0 -> 1169 bytes ...3131-c1f7-453a-8a91-f12c06207edc.png.attrs | 1 + .../55aa6938-e53c-4b58-b446-146f3d80b2c4.txt | 1 + ...6938-e53c-4b58-b446-146f3d80b2c4.txt.attrs | 1 + .../b635c395-6837-49e5-8535-b0a6ebfbdbf3.png | Bin 0 -> 1169 bytes ...c395-6837-49e5-8535-b0a6ebfbdbf3.png.attrs | 1 + .../c2c58441-27f5-4574-96f8-6f79dae9ff4d.png | Bin 0 -> 1169 bytes ...8441-27f5-4574-96f8-6f79dae9ff4d.png.attrs | 1 + .../e526d938-c5ab-41cb-a334-85b9c3e37f72.png | Bin 0 -> 1169 bytes ...d938-c5ab-41cb-a334-85b9c3e37f72.png.attrs | 1 + ...0_315d3131-c1f7-453a-8a91-f12c06207edc.png | Bin 0 -> 2170 bytes ...3131-c1f7-453a-8a91-f12c06207edc.png.attrs | 1 + ...0_b635c395-6837-49e5-8535-b0a6ebfbdbf3.png | Bin 0 -> 2170 bytes ...c395-6837-49e5-8535-b0a6ebfbdbf3.png.attrs | 1 + ...0_c2c58441-27f5-4574-96f8-6f79dae9ff4d.png | Bin 0 -> 2170 bytes ...8441-27f5-4574-96f8-6f79dae9ff4d.png.attrs | 1 + ...0_e526d938-c5ab-41cb-a334-85b9c3e37f72.png | Bin 0 -> 2170 bytes ...d938-c5ab-41cb-a334-85b9c3e37f72.png.attrs | 1 + .../5e74d0b5-c183-4419-9d7c-a6a3d4a7faca.txt | 1 + ...d0b5-c183-4419-9d7c-a6a3d4a7faca.txt.attrs | 1 + .../f80296fb-9fa5-4372-80f2-be196b973c7b.txt | 1 + ...96fb-9fa5-4372-80f2-be196b973c7b.txt.attrs | 1 + .../ff3b7633-6440-43d8-a957-1bfb3d3421ec.txt | 1 + ...7633-6440-43d8-a957-1bfb3d3421ec.txt.attrs | 1 + .../935a3325-f511-4d11-87f4-51034234a8d9.png | Bin 0 -> 1169 bytes ...3325-f511-4d11-87f4-51034234a8d9.png.attrs | 1 + ...0_935a3325-f511-4d11-87f4-51034234a8d9.png | Bin 0 -> 2170 bytes ...3325-f511-4d11-87f4-51034234a8d9.png.attrs | 1 + tests/logs.go | 50 + tests/mailer.go | 29 + tests/request.go | 54 + tokens/admin.go | 26 + tokens/admin_test.go | 54 + tokens/tokens.go | 2 + tokens/user.go | 44 + tokens/user_test.go | 100 + tools/auth/auth.go | 96 + tools/auth/auth_test.go | 57 + tools/auth/base_provider.go | 138 + tools/auth/base_provider_test.go | 183 + tools/auth/facebook.go | 51 + tools/auth/github.go | 87 + tools/auth/gitlab.go | 51 + tools/auth/google.go | 52 + tools/filesystem/filesystem.go | 250 + tools/filesystem/filesystem_test.go | 272 + tools/hook/hook.go | 64 + tools/hook/hook_test.go | 129 + tools/inflector/inflector.go | 118 + tools/inflector/inflector_test.go | 153 + tools/list/list.go | 117 + tools/list/list_test.go | 174 + tools/mailer/mailer.go | 18 + tools/mailer/sendmail.go | 79 + tools/mailer/smtp.go | 88 + tools/migrate/list.go | 59 + tools/migrate/list_test.go | 33 + tools/migrate/runner.go | 271 + tools/migrate/runner_test.go | 145 + tools/migrate/template.go | 21 + tools/rest/api_error.go | 107 + tools/rest/api_error_test.go | 150 + tools/rest/multi_binder.go | 59 + tools/rest/multi_binder_test.go | 102 + tools/rest/uploaded_file.go | 76 + tools/rest/uploaded_file_test.go | 84 + tools/routine/routine.go | 32 + tools/routine/routine_test.go | 27 + tools/search/filter.go | 198 + tools/search/filter_test.go | 104 + tools/search/provider.go | 245 + tools/search/provider_test.go | 505 + tools/search/simple_field_resolver.go | 58 + tools/search/simple_field_resolver_test.go | 81 + tools/search/sort.go | 59 + tools/search/sort_test.go | 67 + tools/security/encrypt.go | 75 + tools/security/encrypt_test.go | 93 + tools/security/jwt.go | 60 + tools/security/jwt_test.go | 179 + tools/security/random.go | 21 + tools/security/random_test.go | 33 + tools/store/store.go | 84 + tools/store/store_test.go | 126 + tools/subscriptions/broker.go | 58 + tools/subscriptions/broker_test.go | 86 + tools/subscriptions/client.go | 141 + tools/subscriptions/client_test.go | 131 + tools/types/datetime.go | 93 + tools/types/datetime_test.go | 198 + tools/types/json_array.go | 55 + tools/types/json_array_test.go | 95 + tools/types/json_map.go | 55 + tools/types/json_map_test.go | 92 + tools/types/json_raw.go | 83 + tools/types/json_raw_test.go | 178 + ui/.env | 4 + ui/.env.development | 1 + ui/.gitignore | 3 + ui/README.md | 24 + ui/dist/assets/Elements.c2e07307.js | 68 + .../FilterAutocompleteInput.15d21df7.js | 12 + ui/dist/assets/NotFoundPage.8b4364cc.js | 1 + .../PageAdminConfirmPasswordReset.a49a8974.js | 2 + .../PageAdminRequestPasswordReset.f5bd52f0.js | 2 + .../PageUserConfirmEmailChange.172a5083.js | 4 + .../PageUserConfirmPasswordReset.b297807e.js | 4 + .../PageUserConfirmVerification.07c01eba.js | 3 + ui/dist/assets/index.35a93598.css | 1 + ui/dist/assets/index.944ee0db.js | 363 + .../jetbrains-mono-v12-latin-600.woff | Bin 0 -> 25616 bytes .../jetbrains-mono-v12-latin-600.woff2 | Bin 0 -> 20048 bytes .../jetbrains-mono-v12-latin-regular.woff | Bin 0 -> 24824 bytes .../jetbrains-mono-v12-latin-regular.woff2 | Bin 0 -> 19180 bytes ui/dist/fonts/remixicon/remixicon.eot | Bin 0 -> 403228 bytes ui/dist/fonts/remixicon/remixicon.glyph.json | 1 + ui/dist/fonts/remixicon/remixicon.svg | 6835 ++++++++++ ui/dist/fonts/remixicon/remixicon.symbol.svg | 11356 ++++++++++++++++ ui/dist/fonts/remixicon/remixicon.ttf | Bin 0 -> 403056 bytes ui/dist/fonts/remixicon/remixicon.woff | Bin 0 -> 172876 bytes ui/dist/fonts/remixicon/remixicon.woff2 | Bin 0 -> 125268 bytes ...ource-sans-pro-v18-latin_cyrillic-600.woff | Bin 0 -> 23232 bytes ...urce-sans-pro-v18-latin_cyrillic-600.woff2 | Bin 0 -> 18756 bytes ...sans-pro-v18-latin_cyrillic-600italic.woff | Bin 0 -> 15608 bytes ...ans-pro-v18-latin_cyrillic-600italic.woff2 | Bin 0 -> 12548 bytes ...ource-sans-pro-v18-latin_cyrillic-700.woff | Bin 0 -> 23136 bytes ...urce-sans-pro-v18-latin_cyrillic-700.woff2 | Bin 0 -> 18644 bytes ...sans-pro-v18-latin_cyrillic-700italic.woff | Bin 0 -> 15712 bytes ...ans-pro-v18-latin_cyrillic-700italic.woff2 | Bin 0 -> 12628 bytes ...ce-sans-pro-v18-latin_cyrillic-italic.woff | Bin 0 -> 15776 bytes ...e-sans-pro-v18-latin_cyrillic-italic.woff2 | Bin 0 -> 12580 bytes ...e-sans-pro-v18-latin_cyrillic-regular.woff | Bin 0 -> 23328 bytes ...-sans-pro-v18-latin_cyrillic-regular.woff2 | Bin 0 -> 18784 bytes ui/dist/images/avatars/avatar0.svg | 1 + ui/dist/images/avatars/avatar1.svg | 1 + ui/dist/images/avatars/avatar2.svg | 1 + ui/dist/images/avatars/avatar3.svg | 1 + ui/dist/images/avatars/avatar4.svg | 1 + ui/dist/images/avatars/avatar5.svg | 1 + ui/dist/images/avatars/avatar6.svg | 1 + ui/dist/images/avatars/avatar7.svg | 1 + ui/dist/images/avatars/avatar8.svg | 1 + ui/dist/images/avatars/avatar9.svg | 1 + .../images/favicon/android-chrome-192x192.png | Bin 0 -> 2102 bytes .../images/favicon/android-chrome-512x512.png | Bin 0 -> 5707 bytes ui/dist/images/favicon/apple-touch-icon.png | Bin 0 -> 1874 bytes ui/dist/images/favicon/browserconfig.xml | 9 + ui/dist/images/favicon/favicon-16x16.png | Bin 0 -> 698 bytes ui/dist/images/favicon/favicon-32x32.png | Bin 0 -> 900 bytes ui/dist/images/favicon/favicon.ico | Bin 0 -> 15086 bytes ui/dist/images/favicon/mstile-144x144.png | Bin 0 -> 2545 bytes ui/dist/images/favicon/mstile-150x150.png | Bin 0 -> 2610 bytes ui/dist/images/favicon/mstile-310x150.png | Bin 0 -> 2815 bytes ui/dist/images/favicon/mstile-310x310.png | Bin 0 -> 5272 bytes ui/dist/images/favicon/mstile-70x70.png | Bin 0 -> 1852 bytes ui/dist/images/favicon/safari-pinned-tab.svg | 41 + ui/dist/images/favicon/site.webmanifest | 19 + ui/dist/images/logo.svg | 9 + ui/dist/index.html | 30 + ui/embed.go | 20 + ui/index.html | 28 + ui/package-lock.json | 1946 +++ ui/package.json | 34 + .../jetbrains-mono-v12-latin-600.woff | Bin 0 -> 25616 bytes .../jetbrains-mono-v12-latin-600.woff2 | Bin 0 -> 20048 bytes .../jetbrains-mono-v12-latin-regular.woff | Bin 0 -> 24824 bytes .../jetbrains-mono-v12-latin-regular.woff2 | Bin 0 -> 19180 bytes ui/public/fonts/remixicon/remixicon.eot | Bin 0 -> 403228 bytes .../fonts/remixicon/remixicon.glyph.json | 1 + ui/public/fonts/remixicon/remixicon.svg | 6835 ++++++++++ .../fonts/remixicon/remixicon.symbol.svg | 11356 ++++++++++++++++ ui/public/fonts/remixicon/remixicon.ttf | Bin 0 -> 403056 bytes ui/public/fonts/remixicon/remixicon.woff | Bin 0 -> 172876 bytes ui/public/fonts/remixicon/remixicon.woff2 | Bin 0 -> 125268 bytes ...ource-sans-pro-v18-latin_cyrillic-600.woff | Bin 0 -> 23232 bytes ...urce-sans-pro-v18-latin_cyrillic-600.woff2 | Bin 0 -> 18756 bytes ...sans-pro-v18-latin_cyrillic-600italic.woff | Bin 0 -> 15608 bytes ...ans-pro-v18-latin_cyrillic-600italic.woff2 | Bin 0 -> 12548 bytes ...ource-sans-pro-v18-latin_cyrillic-700.woff | Bin 0 -> 23136 bytes ...urce-sans-pro-v18-latin_cyrillic-700.woff2 | Bin 0 -> 18644 bytes ...sans-pro-v18-latin_cyrillic-700italic.woff | Bin 0 -> 15712 bytes ...ans-pro-v18-latin_cyrillic-700italic.woff2 | Bin 0 -> 12628 bytes ...ce-sans-pro-v18-latin_cyrillic-italic.woff | Bin 0 -> 15776 bytes ...e-sans-pro-v18-latin_cyrillic-italic.woff2 | Bin 0 -> 12580 bytes ...e-sans-pro-v18-latin_cyrillic-regular.woff | Bin 0 -> 23328 bytes ...-sans-pro-v18-latin_cyrillic-regular.woff2 | Bin 0 -> 18784 bytes ui/public/images/avatars/avatar0.svg | 1 + ui/public/images/avatars/avatar1.svg | 1 + ui/public/images/avatars/avatar2.svg | 1 + ui/public/images/avatars/avatar3.svg | 1 + ui/public/images/avatars/avatar4.svg | 1 + ui/public/images/avatars/avatar5.svg | 1 + ui/public/images/avatars/avatar6.svg | 1 + ui/public/images/avatars/avatar7.svg | 1 + ui/public/images/avatars/avatar8.svg | 1 + ui/public/images/avatars/avatar9.svg | 1 + .../images/favicon/android-chrome-192x192.png | Bin 0 -> 2102 bytes .../images/favicon/android-chrome-512x512.png | Bin 0 -> 5707 bytes ui/public/images/favicon/apple-touch-icon.png | Bin 0 -> 1874 bytes ui/public/images/favicon/browserconfig.xml | 9 + ui/public/images/favicon/favicon-16x16.png | Bin 0 -> 698 bytes ui/public/images/favicon/favicon-32x32.png | Bin 0 -> 900 bytes ui/public/images/favicon/favicon.ico | Bin 0 -> 15086 bytes ui/public/images/favicon/mstile-144x144.png | Bin 0 -> 2545 bytes ui/public/images/favicon/mstile-150x150.png | Bin 0 -> 2610 bytes ui/public/images/favicon/mstile-310x150.png | Bin 0 -> 2815 bytes ui/public/images/favicon/mstile-310x310.png | Bin 0 -> 5272 bytes ui/public/images/favicon/mstile-70x70.png | Bin 0 -> 1852 bytes .../images/favicon/safari-pinned-tab.svg | 41 + ui/public/images/favicon/site.webmanifest | 19 + ui/public/images/logo.svg | 9 + ui/src/App.svelte | 127 + ui/src/actions/tooltip.js | 184 + ui/src/components/Elements.svelte | 487 + ui/src/components/NotFoundPage.svelte | 5 + .../components/admins/AdminUpsertPanel.svelte | 258 + .../PageAdminConfirmPasswordReset.svelte | 66 + .../components/admins/PageAdminLogin.svelte | 66 + .../PageAdminRequestPasswordReset.svelte | 65 + ui/src/components/admins/PageAdmins.svelte | 198 + ui/src/components/base/Accordion.svelte | 98 + .../components/base/AutoExpandTextarea.svelte | 47 + .../components/base/BaseSelectOption.svelte | 10 + ui/src/components/base/CodeBlock.svelte | 50 + ui/src/components/base/Confirmation.svelte | 64 + ui/src/components/base/Field.svelte | 46 + .../base/FilterAutocompleteInput.svelte | 376 + ui/src/components/base/FormattedDate.svelte | 14 + ui/src/components/base/FullPage.svelte | 40 + ui/src/components/base/IdLabel.svelte | 15 + .../components/base/MultipleValueInput.svelte | 17 + ui/src/components/base/ObjectSelect.svelte | 71 + ui/src/components/base/OverlayPanel.svelte | 237 + ui/src/components/base/PreviewPopup.svelte | 37 + .../base/RedactedPasswordInput.svelte | 38 + ui/src/components/base/Searchbar.svelte | 108 + ui/src/components/base/Select.svelte | 315 + ui/src/components/base/SortHeader.svelte | 38 + ui/src/components/base/Toasts.svelte | 35 + ui/src/components/base/Toggler.svelte | 112 + .../base/UploadedFilePreview.svelte | 32 + .../collections/CollectionFieldsTab.svelte | 83 + .../collections/CollectionRulesTab.svelte | 188 + .../CollectionUpdateConfirm.svelte | 108 + .../collections/CollectionUpsertPanel.svelte | 306 + .../collections/CollectionsSidebar.svelte | 74 + .../collections/FieldAccordion.svelte | 286 + .../docs/CollectionDocsPanel.svelte | 111 + .../collections/docs/CreateApiDocs.svelte | 184 + .../collections/docs/DeleteApiDocs.svelte | 156 + .../collections/docs/FilterSyntax.svelte | 75 + .../collections/docs/ListApiDocs.svelte | 232 + .../collections/docs/RealtimeApiDocs.svelte | 109 + .../collections/docs/UpdateApiDocs.svelte | 214 + .../collections/docs/ViewApiDocs.svelte | 174 + .../collections/schema/BoolOptions.svelte | 6 + .../collections/schema/DateOptions.svelte | 34 + .../collections/schema/EmailOptions.svelte | 53 + .../collections/schema/FieldTypeSelect.svelte | 75 + .../collections/schema/FileOptions.svelte | 124 + .../collections/schema/JsonOptions.svelte | 6 + .../collections/schema/NumberOptions.svelte | 22 + .../collections/schema/RelationOptions.svelte | 71 + .../collections/schema/SelectOptions.svelte | 43 + .../collections/schema/TextOptions.svelte | 30 + .../collections/schema/UrlOptions.svelte | 9 + .../collections/schema/UserOptions.svelte | 36 + ui/src/components/logs/LogViewPanel.svelte | 95 + ui/src/components/logs/LogsChart.svelte | 180 + ui/src/components/logs/LogsList.svelte | 201 + ui/src/components/logs/PageLogs.svelte | 75 + ui/src/components/records/PageRecords.svelte | 130 + .../components/records/RecordFieldCell.svelte | 57 + .../records/RecordFilePreview.svelte | 23 + ui/src/components/records/RecordSelect.svelte | 123 + .../records/RecordSelectOption.svelte | 60 + .../records/RecordUpsertPanel.svelte | 269 + ui/src/components/records/RecordsList.svelte | 312 + .../records/fields/BoolField.svelte | 12 + .../records/fields/DateField.svelte | 22 + .../records/fields/EmailField.svelte | 16 + .../records/fields/FileField.svelte | 177 + .../records/fields/JsonField.svelte | 22 + .../records/fields/NumberField.svelte | 23 + .../records/fields/RelationField.svelte | 32 + .../records/fields/SelectField.svelte | 37 + .../records/fields/TextField.svelte | 17 + .../components/records/fields/UrlField.svelte | 16 + .../records/fields/UserField.svelte | 33 + .../settings/AuthProviderAccordion.svelte | 115 + .../settings/EmailAuthAccordion.svelte | 123 + .../settings/PageApplication.svelte | 115 + .../settings/PageAuthProviders.svelte | 142 + ui/src/components/settings/PageMail.svelte | 242 + ui/src/components/settings/PageStorage.svelte | 139 + .../settings/PageTokenOptions.svelte | 136 + .../settings/SettingsSidebar.svelte | 61 + .../users/PageUserConfirmEmailChange.svelte | 73 + .../users/PageUserConfirmPasswordReset.svelte | 79 + .../users/PageUserConfirmVerification.svelte | 57 + ui/src/components/users/PageUsers.svelte | 293 + ui/src/components/users/UserSelect.svelte | 113 + .../components/users/UserSelectOption.svelte | 15 + .../components/users/UserUpsertPanel.svelte | 263 + ui/src/main.js | 7 + ui/src/routes.js | 118 + ui/src/scss/_accordion.scss | 146 + ui/src/scss/_alert.scss | 115 + ui/src/scss/_animations.scss | 53 + ui/src/scss/_base.scss | 638 + ui/src/scss/_bulkbar.scss | 17 + ui/src/scss/_dropdown.scss | 131 + ui/src/scss/_flatpickr.scss | 734 + ui/src/scss/_fonts.scss | 89 + ui/src/scss/_form.scss | 1014 ++ ui/src/scss/_grid.scss | 79 + ui/src/scss/_icons.scss | 2294 ++++ ui/src/scss/_layout.scss | 331 + ui/src/scss/_mixins.scss | 58 + ui/src/scss/_overlay_panel.scss | 254 + ui/src/scss/_reset.scss | 50 + ui/src/scss/_searchbar.scss | 71 + ui/src/scss/_table.scss | 258 + ui/src/scss/_tabs.scss | 129 + ui/src/scss/_tooltip.scss | 57 + ui/src/scss/_vars.scss | 101 + ui/src/scss/main.scss | 39 + ui/src/scss/prism_light.scss | 3 + ui/src/stores/admin.js | 8 + ui/src/stores/collections.js | 68 + ui/src/stores/confirmation.js | 26 + ui/src/stores/errors.js | 32 + ui/src/stores/toasts.js | 76 + ui/src/utils/ApiClient.js | 98 + ui/src/utils/CommonHelper.js | 1048 ++ ui/vite.config.js | 32 + 484 files changed, 92412 insertions(+) create mode 100644 .github/FUNDING.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 apis/admin.go create mode 100644 apis/admin_test.go create mode 100644 apis/base.go create mode 100644 apis/base_test.go create mode 100644 apis/collection.go create mode 100644 apis/collection_test.go create mode 100644 apis/file.go create mode 100644 apis/file_test.go create mode 100644 apis/logs.go create mode 100644 apis/logs_test.go create mode 100644 apis/middlewares.go create mode 100644 apis/middlewares_test.go create mode 100644 apis/realtime.go create mode 100644 apis/realtime_test.go create mode 100644 apis/record.go create mode 100644 apis/record_test.go create mode 100644 apis/settings.go create mode 100644 apis/settings_test.go create mode 100644 apis/user.go create mode 100644 apis/user_test.go create mode 100644 cmd/migrate.go create mode 100644 cmd/serve.go create mode 100644 cmd/version.go create mode 100644 core/app.go create mode 100644 core/base.go create mode 100644 core/base_test.go create mode 100644 core/db_cgo.go create mode 100644 core/db_nocgo.go create mode 100644 core/events.go create mode 100644 core/settings.go create mode 100644 core/settings_test.go create mode 100644 daos/admin.go create mode 100644 daos/admin_test.go create mode 100644 daos/base.go create mode 100644 daos/base_test.go create mode 100644 daos/collection.go create mode 100644 daos/collection_test.go create mode 100644 daos/param.go create mode 100644 daos/param_test.go create mode 100644 daos/record.go create mode 100644 daos/record_expand.go create mode 100644 daos/record_expand_test.go create mode 100644 daos/record_test.go create mode 100644 daos/request.go create mode 100644 daos/request_test.go create mode 100644 daos/table.go create mode 100644 daos/table_test.go create mode 100644 daos/user.go create mode 100644 daos/user_test.go create mode 100644 examples/base/main.go create mode 100644 forms/admin_login.go create mode 100644 forms/admin_login_test.go create mode 100644 forms/admin_password_reset_confirm.go create mode 100644 forms/admin_password_reset_confirm_test.go create mode 100644 forms/admin_password_reset_request.go create mode 100644 forms/admin_password_reset_request_test.go create mode 100644 forms/admin_upsert.go create mode 100644 forms/admin_upsert_test.go create mode 100644 forms/collection_upsert.go create mode 100644 forms/collection_upsert_test.go create mode 100644 forms/realtime_subscribe.go create mode 100644 forms/realtime_subscribe_test.go create mode 100644 forms/record_upsert.go create mode 100644 forms/record_upsert_test.go create mode 100644 forms/settings_upsert.go create mode 100644 forms/settings_upsert_test.go create mode 100644 forms/user_email_change_confirm.go create mode 100644 forms/user_email_change_confirm_test.go create mode 100644 forms/user_email_change_request.go create mode 100644 forms/user_email_change_request_test.go create mode 100644 forms/user_email_login.go create mode 100644 forms/user_email_login_test.go create mode 100644 forms/user_oauth2_login.go create mode 100644 forms/user_oauth2_login_test.go create mode 100644 forms/user_password_reset_confirm.go create mode 100644 forms/user_password_reset_confirm_test.go create mode 100644 forms/user_password_reset_request.go create mode 100644 forms/user_password_reset_request_test.go create mode 100644 forms/user_upsert.go create mode 100644 forms/user_upsert_test.go create mode 100644 forms/user_verification_confirm.go create mode 100644 forms/user_verification_confirm_test.go create mode 100644 forms/user_verification_request.go create mode 100644 forms/user_verification_request_test.go create mode 100644 forms/validators/file.go create mode 100644 forms/validators/file_test.go create mode 100644 forms/validators/record_data.go create mode 100644 forms/validators/record_data_test.go create mode 100644 forms/validators/string.go create mode 100644 forms/validators/string_test.go create mode 100644 forms/validators/validators.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 mails/admin.go create mode 100644 mails/admin_test.go create mode 100644 mails/base.go create mode 100644 mails/templates/admin_password_reset.go create mode 100644 mails/templates/html_content.go create mode 100644 mails/templates/layout.go create mode 100644 mails/templates/user_confirm_email_change.go create mode 100644 mails/templates/user_password_reset.go create mode 100644 mails/templates/user_verification.go create mode 100644 mails/user.go create mode 100644 mails/user_test.go create mode 100644 migrations/1640988000_init.go create mode 100644 migrations/logs/1640988000_init.go create mode 100644 models/admin.go create mode 100644 models/admin_test.go create mode 100644 models/base.go create mode 100644 models/base_test.go create mode 100644 models/collection.go create mode 100644 models/collection_test.go create mode 100644 models/param.go create mode 100644 models/param_test.go create mode 100644 models/record.go create mode 100644 models/record_test.go create mode 100644 models/request.go create mode 100644 models/request_test.go create mode 100644 models/schema/schema.go create mode 100644 models/schema/schema_field.go create mode 100644 models/schema/schema_field_test.go create mode 100644 models/schema/schema_test.go create mode 100644 models/user.go create mode 100644 models/user_test.go create mode 100644 pocketbase.go create mode 100644 pocketbase_test.go create mode 100644 resolvers/record_field_resolver.go create mode 100644 resolvers/record_field_resolver_test.go create mode 100644 resolvers/resolvers.go create mode 100644 tests/api.go create mode 100644 tests/app.go create mode 100644 tests/data/.gitignore create mode 100644 tests/data/data.db create mode 100644 tests/data/logs.db create mode 100644 tests/data/storage/2c1010aa-b8fe-41d9-a980-99534ca8a167/94568ca2-0bee-49d7-b749-06cb97956fd9/01562272-e67e-4925-9f37-02b5f899853c.txt create mode 100644 tests/data/storage/2c1010aa-b8fe-41d9-a980-99534ca8a167/94568ca2-0bee-49d7-b749-06cb97956fd9/01562272-e67e-4925-9f37-02b5f899853c.txt.attrs create mode 100644 tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png create mode 100644 tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png.attrs create mode 100644 tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/100x100_4881bdef-06b4-4dea-8d97-6125ad242677.png create mode 100644 tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/100x100_4881bdef-06b4-4dea-8d97-6125ad242677.png.attrs create mode 100644 tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/848a1dea-5ddd-42d6-a00d-030547bffcfe/8fe61d65-6a2e-4f11-87b3-d8a3170bfd4f.txt create mode 100644 tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/848a1dea-5ddd-42d6-a00d-030547bffcfe/8fe61d65-6a2e-4f11-87b3-d8a3170bfd4f.txt.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/315d3131-c1f7-453a-8a91-f12c06207edc.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/315d3131-c1f7-453a-8a91-f12c06207edc.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/b635c395-6837-49e5-8535-b0a6ebfbdbf3.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/b635c395-6837-49e5-8535-b0a6ebfbdbf3.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/c2c58441-27f5-4574-96f8-6f79dae9ff4d.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/c2c58441-27f5-4574-96f8-6f79dae9ff4d.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/e526d938-c5ab-41cb-a334-85b9c3e37f72.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/e526d938-c5ab-41cb-a334-85b9c3e37f72.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_315d3131-c1f7-453a-8a91-f12c06207edc.png/100x100_315d3131-c1f7-453a-8a91-f12c06207edc.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_315d3131-c1f7-453a-8a91-f12c06207edc.png/100x100_315d3131-c1f7-453a-8a91-f12c06207edc.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_b635c395-6837-49e5-8535-b0a6ebfbdbf3.png/100x100_b635c395-6837-49e5-8535-b0a6ebfbdbf3.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_b635c395-6837-49e5-8535-b0a6ebfbdbf3.png/100x100_b635c395-6837-49e5-8535-b0a6ebfbdbf3.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_c2c58441-27f5-4574-96f8-6f79dae9ff4d.png/100x100_c2c58441-27f5-4574-96f8-6f79dae9ff4d.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_c2c58441-27f5-4574-96f8-6f79dae9ff4d.png/100x100_c2c58441-27f5-4574-96f8-6f79dae9ff4d.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_e526d938-c5ab-41cb-a334-85b9c3e37f72.png/100x100_e526d938-c5ab-41cb-a334-85b9c3e37f72.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_e526d938-c5ab-41cb-a334-85b9c3e37f72.png/100x100_e526d938-c5ab-41cb-a334-85b9c3e37f72.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/b84cd893-7119-43c9-8505-3c4e22da28a9/5e74d0b5-c183-4419-9d7c-a6a3d4a7faca.txt create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/b84cd893-7119-43c9-8505-3c4e22da28a9/5e74d0b5-c183-4419-9d7c-a6a3d4a7faca.txt.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/b84cd893-7119-43c9-8505-3c4e22da28a9/f80296fb-9fa5-4372-80f2-be196b973c7b.txt create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/b84cd893-7119-43c9-8505-3c4e22da28a9/f80296fb-9fa5-4372-80f2-be196b973c7b.txt.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/b84cd893-7119-43c9-8505-3c4e22da28a9/ff3b7633-6440-43d8-a957-1bfb3d3421ec.txt create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/b84cd893-7119-43c9-8505-3c4e22da28a9/ff3b7633-6440-43d8-a957-1bfb3d3421ec.txt.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/935a3325-f511-4d11-87f4-51034234a8d9.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/935a3325-f511-4d11-87f4-51034234a8d9.png.attrs create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/thumbs_935a3325-f511-4d11-87f4-51034234a8d9.png/100x100_935a3325-f511-4d11-87f4-51034234a8d9.png create mode 100644 tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/thumbs_935a3325-f511-4d11-87f4-51034234a8d9.png/100x100_935a3325-f511-4d11-87f4-51034234a8d9.png.attrs create mode 100644 tests/logs.go create mode 100644 tests/mailer.go create mode 100644 tests/request.go create mode 100644 tokens/admin.go create mode 100644 tokens/admin_test.go create mode 100644 tokens/tokens.go create mode 100644 tokens/user.go create mode 100644 tokens/user_test.go create mode 100644 tools/auth/auth.go create mode 100644 tools/auth/auth_test.go create mode 100644 tools/auth/base_provider.go create mode 100644 tools/auth/base_provider_test.go create mode 100644 tools/auth/facebook.go create mode 100644 tools/auth/github.go create mode 100644 tools/auth/gitlab.go create mode 100644 tools/auth/google.go create mode 100644 tools/filesystem/filesystem.go create mode 100644 tools/filesystem/filesystem_test.go create mode 100644 tools/hook/hook.go create mode 100644 tools/hook/hook_test.go create mode 100644 tools/inflector/inflector.go create mode 100644 tools/inflector/inflector_test.go create mode 100644 tools/list/list.go create mode 100644 tools/list/list_test.go create mode 100644 tools/mailer/mailer.go create mode 100644 tools/mailer/sendmail.go create mode 100644 tools/mailer/smtp.go create mode 100644 tools/migrate/list.go create mode 100644 tools/migrate/list_test.go create mode 100644 tools/migrate/runner.go create mode 100644 tools/migrate/runner_test.go create mode 100644 tools/migrate/template.go create mode 100644 tools/rest/api_error.go create mode 100644 tools/rest/api_error_test.go create mode 100644 tools/rest/multi_binder.go create mode 100644 tools/rest/multi_binder_test.go create mode 100644 tools/rest/uploaded_file.go create mode 100644 tools/rest/uploaded_file_test.go create mode 100644 tools/routine/routine.go create mode 100644 tools/routine/routine_test.go create mode 100644 tools/search/filter.go create mode 100644 tools/search/filter_test.go create mode 100644 tools/search/provider.go create mode 100644 tools/search/provider_test.go create mode 100644 tools/search/simple_field_resolver.go create mode 100644 tools/search/simple_field_resolver_test.go create mode 100644 tools/search/sort.go create mode 100644 tools/search/sort_test.go create mode 100644 tools/security/encrypt.go create mode 100644 tools/security/encrypt_test.go create mode 100644 tools/security/jwt.go create mode 100644 tools/security/jwt_test.go create mode 100644 tools/security/random.go create mode 100644 tools/security/random_test.go create mode 100644 tools/store/store.go create mode 100644 tools/store/store_test.go create mode 100644 tools/subscriptions/broker.go create mode 100644 tools/subscriptions/broker_test.go create mode 100644 tools/subscriptions/client.go create mode 100644 tools/subscriptions/client_test.go create mode 100644 tools/types/datetime.go create mode 100644 tools/types/datetime_test.go create mode 100644 tools/types/json_array.go create mode 100644 tools/types/json_array_test.go create mode 100644 tools/types/json_map.go create mode 100644 tools/types/json_map_test.go create mode 100644 tools/types/json_raw.go create mode 100644 tools/types/json_raw_test.go create mode 100644 ui/.env create mode 100644 ui/.env.development create mode 100644 ui/.gitignore create mode 100644 ui/README.md create mode 100644 ui/dist/assets/Elements.c2e07307.js create mode 100644 ui/dist/assets/FilterAutocompleteInput.15d21df7.js create mode 100644 ui/dist/assets/NotFoundPage.8b4364cc.js create mode 100644 ui/dist/assets/PageAdminConfirmPasswordReset.a49a8974.js create mode 100644 ui/dist/assets/PageAdminRequestPasswordReset.f5bd52f0.js create mode 100644 ui/dist/assets/PageUserConfirmEmailChange.172a5083.js create mode 100644 ui/dist/assets/PageUserConfirmPasswordReset.b297807e.js create mode 100644 ui/dist/assets/PageUserConfirmVerification.07c01eba.js create mode 100644 ui/dist/assets/index.35a93598.css create mode 100644 ui/dist/assets/index.944ee0db.js create mode 100644 ui/dist/fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff create mode 100644 ui/dist/fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff2 create mode 100644 ui/dist/fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff create mode 100644 ui/dist/fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff2 create mode 100644 ui/dist/fonts/remixicon/remixicon.eot create mode 100644 ui/dist/fonts/remixicon/remixicon.glyph.json create mode 100644 ui/dist/fonts/remixicon/remixicon.svg create mode 100644 ui/dist/fonts/remixicon/remixicon.symbol.svg create mode 100644 ui/dist/fonts/remixicon/remixicon.ttf create mode 100644 ui/dist/fonts/remixicon/remixicon.woff create mode 100644 ui/dist/fonts/remixicon/remixicon.woff2 create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2 create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2 create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2 create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2 create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2 create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff create mode 100644 ui/dist/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2 create mode 100644 ui/dist/images/avatars/avatar0.svg create mode 100644 ui/dist/images/avatars/avatar1.svg create mode 100644 ui/dist/images/avatars/avatar2.svg create mode 100644 ui/dist/images/avatars/avatar3.svg create mode 100644 ui/dist/images/avatars/avatar4.svg create mode 100644 ui/dist/images/avatars/avatar5.svg create mode 100644 ui/dist/images/avatars/avatar6.svg create mode 100644 ui/dist/images/avatars/avatar7.svg create mode 100644 ui/dist/images/avatars/avatar8.svg create mode 100644 ui/dist/images/avatars/avatar9.svg create mode 100644 ui/dist/images/favicon/android-chrome-192x192.png create mode 100644 ui/dist/images/favicon/android-chrome-512x512.png create mode 100644 ui/dist/images/favicon/apple-touch-icon.png create mode 100644 ui/dist/images/favicon/browserconfig.xml create mode 100644 ui/dist/images/favicon/favicon-16x16.png create mode 100644 ui/dist/images/favicon/favicon-32x32.png create mode 100644 ui/dist/images/favicon/favicon.ico create mode 100644 ui/dist/images/favicon/mstile-144x144.png create mode 100644 ui/dist/images/favicon/mstile-150x150.png create mode 100644 ui/dist/images/favicon/mstile-310x150.png create mode 100644 ui/dist/images/favicon/mstile-310x310.png create mode 100644 ui/dist/images/favicon/mstile-70x70.png create mode 100644 ui/dist/images/favicon/safari-pinned-tab.svg create mode 100644 ui/dist/images/favicon/site.webmanifest create mode 100644 ui/dist/images/logo.svg create mode 100644 ui/dist/index.html create mode 100644 ui/embed.go create mode 100644 ui/index.html create mode 100644 ui/package-lock.json create mode 100644 ui/package.json create mode 100644 ui/public/fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff create mode 100644 ui/public/fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff2 create mode 100644 ui/public/fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff create mode 100644 ui/public/fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff2 create mode 100644 ui/public/fonts/remixicon/remixicon.eot create mode 100644 ui/public/fonts/remixicon/remixicon.glyph.json create mode 100644 ui/public/fonts/remixicon/remixicon.svg create mode 100644 ui/public/fonts/remixicon/remixicon.symbol.svg create mode 100644 ui/public/fonts/remixicon/remixicon.ttf create mode 100644 ui/public/fonts/remixicon/remixicon.woff create mode 100644 ui/public/fonts/remixicon/remixicon.woff2 create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2 create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2 create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2 create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2 create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2 create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff create mode 100644 ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2 create mode 100644 ui/public/images/avatars/avatar0.svg create mode 100644 ui/public/images/avatars/avatar1.svg create mode 100644 ui/public/images/avatars/avatar2.svg create mode 100644 ui/public/images/avatars/avatar3.svg create mode 100644 ui/public/images/avatars/avatar4.svg create mode 100644 ui/public/images/avatars/avatar5.svg create mode 100644 ui/public/images/avatars/avatar6.svg create mode 100644 ui/public/images/avatars/avatar7.svg create mode 100644 ui/public/images/avatars/avatar8.svg create mode 100644 ui/public/images/avatars/avatar9.svg create mode 100644 ui/public/images/favicon/android-chrome-192x192.png create mode 100644 ui/public/images/favicon/android-chrome-512x512.png create mode 100644 ui/public/images/favicon/apple-touch-icon.png create mode 100644 ui/public/images/favicon/browserconfig.xml create mode 100644 ui/public/images/favicon/favicon-16x16.png create mode 100644 ui/public/images/favicon/favicon-32x32.png create mode 100644 ui/public/images/favicon/favicon.ico create mode 100644 ui/public/images/favicon/mstile-144x144.png create mode 100644 ui/public/images/favicon/mstile-150x150.png create mode 100644 ui/public/images/favicon/mstile-310x150.png create mode 100644 ui/public/images/favicon/mstile-310x310.png create mode 100644 ui/public/images/favicon/mstile-70x70.png create mode 100644 ui/public/images/favicon/safari-pinned-tab.svg create mode 100644 ui/public/images/favicon/site.webmanifest create mode 100644 ui/public/images/logo.svg create mode 100644 ui/src/App.svelte create mode 100644 ui/src/actions/tooltip.js create mode 100644 ui/src/components/Elements.svelte create mode 100644 ui/src/components/NotFoundPage.svelte create mode 100644 ui/src/components/admins/AdminUpsertPanel.svelte create mode 100644 ui/src/components/admins/PageAdminConfirmPasswordReset.svelte create mode 100644 ui/src/components/admins/PageAdminLogin.svelte create mode 100644 ui/src/components/admins/PageAdminRequestPasswordReset.svelte create mode 100644 ui/src/components/admins/PageAdmins.svelte create mode 100644 ui/src/components/base/Accordion.svelte create mode 100644 ui/src/components/base/AutoExpandTextarea.svelte create mode 100644 ui/src/components/base/BaseSelectOption.svelte create mode 100644 ui/src/components/base/CodeBlock.svelte create mode 100644 ui/src/components/base/Confirmation.svelte create mode 100644 ui/src/components/base/Field.svelte create mode 100644 ui/src/components/base/FilterAutocompleteInput.svelte create mode 100644 ui/src/components/base/FormattedDate.svelte create mode 100644 ui/src/components/base/FullPage.svelte create mode 100644 ui/src/components/base/IdLabel.svelte create mode 100644 ui/src/components/base/MultipleValueInput.svelte create mode 100644 ui/src/components/base/ObjectSelect.svelte create mode 100644 ui/src/components/base/OverlayPanel.svelte create mode 100644 ui/src/components/base/PreviewPopup.svelte create mode 100644 ui/src/components/base/RedactedPasswordInput.svelte create mode 100644 ui/src/components/base/Searchbar.svelte create mode 100644 ui/src/components/base/Select.svelte create mode 100644 ui/src/components/base/SortHeader.svelte create mode 100644 ui/src/components/base/Toasts.svelte create mode 100644 ui/src/components/base/Toggler.svelte create mode 100644 ui/src/components/base/UploadedFilePreview.svelte create mode 100644 ui/src/components/collections/CollectionFieldsTab.svelte create mode 100644 ui/src/components/collections/CollectionRulesTab.svelte create mode 100644 ui/src/components/collections/CollectionUpdateConfirm.svelte create mode 100644 ui/src/components/collections/CollectionUpsertPanel.svelte create mode 100644 ui/src/components/collections/CollectionsSidebar.svelte create mode 100644 ui/src/components/collections/FieldAccordion.svelte create mode 100644 ui/src/components/collections/docs/CollectionDocsPanel.svelte create mode 100644 ui/src/components/collections/docs/CreateApiDocs.svelte create mode 100644 ui/src/components/collections/docs/DeleteApiDocs.svelte create mode 100644 ui/src/components/collections/docs/FilterSyntax.svelte create mode 100644 ui/src/components/collections/docs/ListApiDocs.svelte create mode 100644 ui/src/components/collections/docs/RealtimeApiDocs.svelte create mode 100644 ui/src/components/collections/docs/UpdateApiDocs.svelte create mode 100644 ui/src/components/collections/docs/ViewApiDocs.svelte create mode 100644 ui/src/components/collections/schema/BoolOptions.svelte create mode 100644 ui/src/components/collections/schema/DateOptions.svelte create mode 100644 ui/src/components/collections/schema/EmailOptions.svelte create mode 100644 ui/src/components/collections/schema/FieldTypeSelect.svelte create mode 100644 ui/src/components/collections/schema/FileOptions.svelte create mode 100644 ui/src/components/collections/schema/JsonOptions.svelte create mode 100644 ui/src/components/collections/schema/NumberOptions.svelte create mode 100644 ui/src/components/collections/schema/RelationOptions.svelte create mode 100644 ui/src/components/collections/schema/SelectOptions.svelte create mode 100644 ui/src/components/collections/schema/TextOptions.svelte create mode 100644 ui/src/components/collections/schema/UrlOptions.svelte create mode 100644 ui/src/components/collections/schema/UserOptions.svelte create mode 100644 ui/src/components/logs/LogViewPanel.svelte create mode 100644 ui/src/components/logs/LogsChart.svelte create mode 100644 ui/src/components/logs/LogsList.svelte create mode 100644 ui/src/components/logs/PageLogs.svelte create mode 100644 ui/src/components/records/PageRecords.svelte create mode 100644 ui/src/components/records/RecordFieldCell.svelte create mode 100644 ui/src/components/records/RecordFilePreview.svelte create mode 100644 ui/src/components/records/RecordSelect.svelte create mode 100644 ui/src/components/records/RecordSelectOption.svelte create mode 100644 ui/src/components/records/RecordUpsertPanel.svelte create mode 100644 ui/src/components/records/RecordsList.svelte create mode 100644 ui/src/components/records/fields/BoolField.svelte create mode 100644 ui/src/components/records/fields/DateField.svelte create mode 100644 ui/src/components/records/fields/EmailField.svelte create mode 100644 ui/src/components/records/fields/FileField.svelte create mode 100644 ui/src/components/records/fields/JsonField.svelte create mode 100644 ui/src/components/records/fields/NumberField.svelte create mode 100644 ui/src/components/records/fields/RelationField.svelte create mode 100644 ui/src/components/records/fields/SelectField.svelte create mode 100644 ui/src/components/records/fields/TextField.svelte create mode 100644 ui/src/components/records/fields/UrlField.svelte create mode 100644 ui/src/components/records/fields/UserField.svelte create mode 100644 ui/src/components/settings/AuthProviderAccordion.svelte create mode 100644 ui/src/components/settings/EmailAuthAccordion.svelte create mode 100644 ui/src/components/settings/PageApplication.svelte create mode 100644 ui/src/components/settings/PageAuthProviders.svelte create mode 100644 ui/src/components/settings/PageMail.svelte create mode 100644 ui/src/components/settings/PageStorage.svelte create mode 100644 ui/src/components/settings/PageTokenOptions.svelte create mode 100644 ui/src/components/settings/SettingsSidebar.svelte create mode 100644 ui/src/components/users/PageUserConfirmEmailChange.svelte create mode 100644 ui/src/components/users/PageUserConfirmPasswordReset.svelte create mode 100644 ui/src/components/users/PageUserConfirmVerification.svelte create mode 100644 ui/src/components/users/PageUsers.svelte create mode 100644 ui/src/components/users/UserSelect.svelte create mode 100644 ui/src/components/users/UserSelectOption.svelte create mode 100644 ui/src/components/users/UserUpsertPanel.svelte create mode 100644 ui/src/main.js create mode 100644 ui/src/routes.js create mode 100644 ui/src/scss/_accordion.scss create mode 100644 ui/src/scss/_alert.scss create mode 100644 ui/src/scss/_animations.scss create mode 100644 ui/src/scss/_base.scss create mode 100644 ui/src/scss/_bulkbar.scss create mode 100644 ui/src/scss/_dropdown.scss create mode 100644 ui/src/scss/_flatpickr.scss create mode 100644 ui/src/scss/_fonts.scss create mode 100644 ui/src/scss/_form.scss create mode 100644 ui/src/scss/_grid.scss create mode 100644 ui/src/scss/_icons.scss create mode 100644 ui/src/scss/_layout.scss create mode 100644 ui/src/scss/_mixins.scss create mode 100644 ui/src/scss/_overlay_panel.scss create mode 100644 ui/src/scss/_reset.scss create mode 100644 ui/src/scss/_searchbar.scss create mode 100644 ui/src/scss/_table.scss create mode 100644 ui/src/scss/_tabs.scss create mode 100644 ui/src/scss/_tooltip.scss create mode 100644 ui/src/scss/_vars.scss create mode 100644 ui/src/scss/main.scss create mode 100644 ui/src/scss/prism_light.scss create mode 100644 ui/src/stores/admin.js create mode 100644 ui/src/stores/collections.js create mode 100644 ui/src/stores/confirmation.js create mode 100644 ui/src/stores/errors.js create mode 100644 ui/src/stores/toasts.js create mode 100644 ui/src/utils/ApiClient.js create mode 100644 ui/src/utils/CommonHelper.js create mode 100644 ui/vite.config.js diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml new file mode 100644 index 00000000..6f504dc0 --- /dev/null +++ b/.github/FUNDING.yaml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: ['https://www.paypal.com/donate?hosted_button_id=S98EMBN3G3HZY'] diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..7789ca28 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,39 @@ +name: basebuild + +on: + pull_request: + push: + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: latest + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '>=1.18.0' + + # This step usually is not needed because the /ui/dist is pregenerated locally + # but its here to ensure that each release embeds the latest admin ui artifacts. + # If the artificats differs, a "dirty error" is thrown - https://goreleaser.com/errors/dirty/ + - name: Build Admin dashboard UI + run: npm --prefix=./ui ci && npm --prefix=./ui run build + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v3 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5d03e3d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +/.vscode/ + +.DS_Store + +# goreleaser builds folder +/.builds/ + +# examples app directories +pb_data +pb_public + +# tests coverage +coverage.out + +# plaintask todo files +*.todo diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..42762bc3 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,43 @@ +project_name: pocketbase + +dist: .builds + +before: + hooks: + - go mod tidy + +builds: + - main: ./examples/base + binary: pocketbase + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + +release: + draft: true + +archives: + - + format: zip + files: + - LICENSE* + - CHANGELOG* + +checksum: + name_template: 'checksums.txt' + +snapshot: + name_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - '^examples:' + - '^ui:' diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..26265aa2 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,17 @@ +The MIT License (MIT) +Copyright (c) 2022, Gani Georgiev + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..71f5224d --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +

+ + PocketBase - open source backend in 1 file + +

+ +

+ build + Latest releases + Go package documentation +

+ +[PocketBase](https://pocketbase.io) is an open source Go backend, consisting of: + +- embedded database (_SQLite_) with **realtime subscriptions** +- backed-in **files and users management** +- convenient **Admin dashboard UI** +- and simple **REST-ish API** + +**For documentation and examples, please visit https://pocketbase.io/docs.** + +> ⚠️ Although the web API defintions are considered stable, +> please keep in mind that PocketBase is still under active development +> and therefore full backward compatibility is not guaranteed before reaching v1.0.0. + + +## API SDK clients + +The easiest way to interact with the API is to use one of the official SDK clients: + +- **JavaScript - [pocketbase/js-sdk](https://github.com/pocketbase/js-sdk)** (_browser and node_) +- **Dart** - _soon_ + + +## Overview + +PocketBase could be used as a standalone app or as a Go framework/toolkit that enables you to build +your own custom app specific business logic and still have a single portable executable at the end. + +### Installation + +```sh +# go 1.18+ +go get github.com/pocketbase/pocketbase +``` + +### Example + +```go +package main + +import ( + "log" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" +) + +func main() { + app := pocketbase.New() + + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + // add new "GET /api/hello" route to the app router (echo) + e.Router.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/api/hello", + Handler: func(c echo.Context) error { + return c.String(200, "Hello world!") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrUserAuth(), + }, + }) + + return nil + }) + + if err := app.Start(); err != nil { + log.Fatal(err) + } +} +``` + +### Running and building + +Running/building the application is the same as for any other Go program, aka. just `go run` and `go build`. + +**PocketBase embeds SQLite, but doesn't require CGO.** + +If CGO is enabled, it will use [mattn/go-sqlite3](https://pkg.go.dev/github.com/mattn/go-sqlite3) driver, otherwise - [modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite). + +Enable CGO only if you really need to squeeze the read/write query performance at the expense of complicating cross compilation. + +### Testing + +PocketBase comes with mixed bag of unit and integration tests. +To run them, use the default `go test` command: +```sh +go test ./... +``` + +Check also the [Testing guide](http://pocketbase.io/docs/testing) to learn how to write your own custom application tests. + +## Security + +If you discover a security vulnerability within PocketBase, please send an e-mail to **support at pocketbase.io**. + +All reports will be promptly addressed and you'll be credited accordingly. + + +## Contributing + +PocketBase is free and open source project licensed under the [MIT License](LICENSE.md). + +You could help continuing its development by: + +- [Suggest new features, report issues and fix bugs](https://github.com/pocketbase/pocketbase/issues) +- [Donate a small amount](https://pocketbase.io/support-us) + +> Please also note that PocketBase was initially created to serve as a new backend for my other open source project - [Presentator](https://presentator.io) (see [#183](https://github.com/presentator/presentator/issues/183)), +so all feature requests will be first aligned with what we need for Presentator v3. diff --git a/apis/admin.go b/apis/admin.go new file mode 100644 index 00000000..51276db4 --- /dev/null +++ b/apis/admin.go @@ -0,0 +1,261 @@ +package apis + +import ( + "log" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tokens" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/routine" + "github.com/pocketbase/pocketbase/tools/search" +) + +// BindAdminApi registers the admin api endpoints and the corresponding handlers. +func BindAdminApi(app core.App, rg *echo.Group) { + api := adminApi{app: app} + + subGroup := rg.Group("/admins", ActivityLogger(app)) + subGroup.POST("/auth-via-email", api.emailAuth, RequireGuestOnly()) + subGroup.POST("/request-password-reset", api.requestPasswordReset) + subGroup.POST("/confirm-password-reset", api.confirmPasswordReset) + subGroup.POST("/refresh", api.refresh, RequireAdminAuth()) + subGroup.GET("", api.list, RequireAdminAuth()) + subGroup.POST("", api.create, RequireAdminAuth()) + subGroup.GET("/:id", api.view, RequireAdminAuth()) + subGroup.PATCH("/:id", api.update, RequireAdminAuth()) + subGroup.DELETE("/:id", api.delete, RequireAdminAuth()) +} + +type adminApi struct { + app core.App +} + +func (api *adminApi) authResponse(c echo.Context, admin *models.Admin) error { + token, tokenErr := tokens.NewAdminAuthToken(api.app, admin) + if tokenErr != nil { + return rest.NewBadRequestError("Failed to create auth token.", tokenErr) + } + + event := &core.AdminAuthEvent{ + HttpContext: c, + Admin: admin, + Token: token, + } + + return api.app.OnAdminAuthRequest().Trigger(event, func(e *core.AdminAuthEvent) error { + return e.HttpContext.JSON(200, map[string]any{ + "token": e.Token, + "admin": e.Admin, + }) + }) +} + +func (api *adminApi) refresh(c echo.Context) error { + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin == nil { + return rest.NewNotFoundError("Missing auth admin context.", nil) + } + + return api.authResponse(c, admin) +} + +func (api *adminApi) emailAuth(c echo.Context) error { + form := forms.NewAdminLogin(api.app) + if readErr := c.Bind(form); readErr != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr) + } + + admin, submitErr := form.Submit() + if submitErr != nil { + return rest.NewBadRequestError("Failed to authenticate.", submitErr) + } + + return api.authResponse(c, admin) +} + +func (api *adminApi) requestPasswordReset(c echo.Context) error { + form := forms.NewAdminPasswordResetRequest(api.app) + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", err) + } + + if err := form.Validate(); err != nil { + return rest.NewBadRequestError("An error occured while validating the form.", err) + } + + // run in background because we don't need to show the result + // (prevents admins enumeration) + routine.FireAndForget(func() { + if err := form.Submit(); err != nil && api.app.IsDebug() { + log.Println(err) + } + }) + + return c.NoContent(http.StatusNoContent) +} + +func (api *adminApi) confirmPasswordReset(c echo.Context) error { + form := forms.NewAdminPasswordResetConfirm(api.app) + if readErr := c.Bind(form); readErr != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr) + } + + admin, submitErr := form.Submit() + if submitErr != nil { + return rest.NewBadRequestError("Failed to set new password.", submitErr) + } + + return api.authResponse(c, admin) +} + +func (api *adminApi) list(c echo.Context) error { + fieldResolver := search.NewSimpleFieldResolver( + "id", "created", "updated", "name", "email", + ) + + admins := []*models.Admin{} + + result, err := search.NewProvider(fieldResolver). + Query(api.app.Dao().AdminQuery()). + ParseAndExec(c.QueryString(), &admins) + + if err != nil { + return rest.NewBadRequestError("", err) + } + + event := &core.AdminsListEvent{ + HttpContext: c, + Admins: admins, + Result: result, + } + + return api.app.OnAdminsListRequest().Trigger(event, func(e *core.AdminsListEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.Result) + }) +} + +func (api *adminApi) view(c echo.Context) error { + id := c.PathParam("id") + if id == "" { + return rest.NewNotFoundError("", nil) + } + + admin, err := api.app.Dao().FindAdminById(id) + if err != nil || admin == nil { + return rest.NewNotFoundError("", err) + } + + event := &core.AdminViewEvent{ + HttpContext: c, + Admin: admin, + } + + return api.app.OnAdminViewRequest().Trigger(event, func(e *core.AdminViewEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.Admin) + }) +} + +func (api *adminApi) create(c echo.Context) error { + admin := &models.Admin{} + + form := forms.NewAdminUpsert(api.app, admin) + + // load request + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.AdminCreateEvent{ + HttpContext: c, + Admin: admin, + } + + handlerErr := api.app.OnAdminBeforeCreateRequest().Trigger(event, func(e *core.AdminCreateEvent) error { + // create the admin + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to create admin.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.Admin) + }) + + if handlerErr == nil { + api.app.OnAdminAfterCreateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *adminApi) update(c echo.Context) error { + id := c.PathParam("id") + if id == "" { + return rest.NewNotFoundError("", nil) + } + + admin, err := api.app.Dao().FindAdminById(id) + if err != nil || admin == nil { + return rest.NewNotFoundError("", err) + } + + form := forms.NewAdminUpsert(api.app, admin) + + // load request + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.AdminUpdateEvent{ + HttpContext: c, + Admin: admin, + } + + handlerErr := api.app.OnAdminBeforeUpdateRequest().Trigger(event, func(e *core.AdminUpdateEvent) error { + // update the admin + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to update admin.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.Admin) + }) + + if handlerErr == nil { + api.app.OnAdminAfterUpdateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *adminApi) delete(c echo.Context) error { + id := c.PathParam("id") + if id == "" { + return rest.NewNotFoundError("", nil) + } + + admin, err := api.app.Dao().FindAdminById(id) + if err != nil || admin == nil { + return rest.NewNotFoundError("", err) + } + + event := &core.AdminDeleteEvent{ + HttpContext: c, + Admin: admin, + } + + handlerErr := api.app.OnAdminBeforeDeleteRequest().Trigger(event, func(e *core.AdminDeleteEvent) error { + if err := api.app.Dao().DeleteAdmin(e.Admin); err != nil { + return rest.NewBadRequestError("Failed to delete admin.", err) + } + + return e.HttpContext.NoContent(http.StatusNoContent) + }) + + if handlerErr == nil { + api.app.OnAdminAfterDeleteRequest().Trigger(event) + } + + return handlerErr +} diff --git a/apis/admin_test.go b/apis/admin_test.go new file mode 100644 index 00000000..18da81af --- /dev/null +++ b/apis/admin_test.go @@ -0,0 +1,654 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" +) + +func TestAdminAuth(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + Url: "/api/admins/auth-via-email", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."},"password":{"code":"validation_required","message":"Cannot be blank."}}`}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + Url: "/api/admins/auth-via-email", + Body: strings.NewReader(`{`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "wrong email/password", + Method: http.MethodPost, + Url: "/api/admins/auth-via-email", + Body: strings.NewReader(`{"email":"missing@example.com","password":"wrong_pass"}`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid email/password (already authorized)", + Method: http.MethodPost, + Url: "/api/admins/auth-via-email", + Body: strings.NewReader(`{"email":"test@example.com","password":"1234567890"}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"message":"The request can be accessed only by guests.","data":{}`}, + }, + { + Name: "valid email/password (guest)", + Method: http.MethodPost, + Url: "/api/admins/auth-via-email", + Body: strings.NewReader(`{"email":"test@example.com","password":"1234567890"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"admin":{"id":"2b4a97cc-3f83-4d01-a26b-3d77bc842d3c"`, + `"token":`, + }, + ExpectedEvents: map[string]int{ + "OnAdminAuthRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminRequestPasswordReset(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + Url: "/api/admins/request-password-reset", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + Url: "/api/admins/request-password-reset", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing admin", + Method: http.MethodPost, + Url: "/api/admins/request-password-reset", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + ExpectedStatus: 204, + }, + { + Name: "existing admin", + Method: http.MethodPost, + Url: "/api/admins/request-password-reset", + Body: strings.NewReader(`{"email":"test@example.com"}`), + ExpectedStatus: 204, + // usually this events are fired but since the submit is + // executed in a separate go routine they are fired async + // ExpectedEvents: map[string]int{ + // "OnModelBeforeUpdate": 1, + // "OnModelAfterUpdate": 1, + // "OnMailerBeforeUserResetPasswordSend:1": 1, + // "OnMailerAfterUserResetPasswordSend:1": 1, + // }, + }, + { + Name: "existing admin (after already sent)", + Method: http.MethodPost, + Url: "/api/admins/request-password-reset", + Body: strings.NewReader(`{"email":"test@example.com"}`), + ExpectedStatus: 204, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminConfirmPasswordReset(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + Url: "/api/admins/confirm-password-reset", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"password":{"code":"validation_required","message":"Cannot be blank."},"passwordConfirm":{"code":"validation_required","message":"Cannot be blank."},"token":{"code":"validation_required","message":"Cannot be blank."}}`}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + Url: "/api/admins/confirm-password-reset", + Body: strings.NewReader(`{"password`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expired token", + Method: http.MethodPost, + Url: "/api/admins/confirm-password-reset", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA","password":"1234567890","passwordConfirm":"1234567890"}`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"token":{"code":"validation_invalid_token","message":"Invalid or expired token."}}}`}, + }, + { + Name: "valid token", + Method: http.MethodPost, + Url: "/api/admins/confirm-password-reset", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg5MzQ3NDAwMH0.72IhlL_5CpNGE0ZKM7sV9aAKa3wxQaMZdDiHBo0orpw","password":"1234567890","passwordConfirm":"1234567890"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"admin":{"id":"2b4a97cc-3f83-4d01-a26b-3d77bc842d3c"`, + `"token":`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnAdminAuthRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminRefresh(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPost, + Url: "/api/admins/refresh", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodPost, + Url: "/api/admins/refresh", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin", + Method: http.MethodPost, + Url: "/api/admins/refresh", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"admin":{"id":"2b4a97cc-3f83-4d01-a26b-3d77bc842d3c"`, + `"token":`, + }, + ExpectedEvents: map[string]int{ + "OnAdminAuthRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminsList(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/admins", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/admins", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin", + Method: http.MethodGet, + Url: "/api/admins", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":2`, + `"items":[{`, + `"id":"2b4a97cc-3f83-4d01-a26b-3d77bc842d3c"`, + `"id":"3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8"`, + }, + ExpectedEvents: map[string]int{ + "OnAdminsListRequest": 1, + }, + }, + { + Name: "authorized as admin + paging and sorting", + Method: http.MethodGet, + Url: "/api/admins?page=2&perPage=1&sort=-created", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":2`, + `"perPage":1`, + `"totalItems":2`, + `"items":[{`, + `"id":"2b4a97cc-3f83-4d01-a26b-3d77bc842d3c"`, + }, + ExpectedEvents: map[string]int{ + "OnAdminsListRequest": 1, + }, + }, + { + Name: "authorized as admin + invalid filter", + Method: http.MethodGet, + Url: "/api/admins?filter=invalidfield~'test2'", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + valid filter", + Method: http.MethodGet, + Url: "/api/admins?filter=email~'test2'", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"items":[{`, + `"id":"3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8"`, + }, + ExpectedEvents: map[string]int{ + "OnAdminsListRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminView(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + invalid admin id", + Method: http.MethodGet, + Url: "/api/admins/invalid", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + nonexisting admin id", + Method: http.MethodGet, + Url: "/api/admins/b97ccf83-34a2-4d01-a26b-3d77bc842d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + existing admin id", + Method: http.MethodGet, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8"`, + }, + ExpectedEvents: map[string]int{ + "OnAdminViewRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminDelete(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodDelete, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodDelete, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + invalid admin id", + Method: http.MethodDelete, + Url: "/api/admins/invalid", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + nonexisting admin id", + Method: http.MethodDelete, + Url: "/api/admins/b97ccf83-34a2-4d01-a26b-3d77bc842d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + existing admin id", + Method: http.MethodDelete, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnModelBeforeDelete": 1, + "OnModelAfterDelete": 1, + "OnAdminBeforeDeleteRequest": 1, + "OnAdminAfterDeleteRequest": 1, + }, + }, + { + Name: "authorized as admin - try to delete the only remaining admin", + Method: http.MethodDelete, + Url: "/api/admins/2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + // delete all admins except the authorized one + adminModel := &models.Admin{} + _, err := app.Dao().DB().Delete(adminModel.TableName(), dbx.Not(dbx.HashExp{ + "id": "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + })).Execute() + if err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "OnAdminBeforeDeleteRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminCreate(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPost, + Url: "/api/admins", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodPost, + Url: "/api/admins", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + empty data", + Method: http.MethodPost, + Url: "/api/admins", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."},"password":{"code":"validation_required","message":"Cannot be blank."}}`}, + ExpectedEvents: map[string]int{ + "OnAdminBeforeCreateRequest": 1, + }, + }, + { + Name: "authorized as admin + invalid data format", + Method: http.MethodPost, + Url: "/api/admins", + Body: strings.NewReader(`{`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + invalid data", + Method: http.MethodPost, + Url: "/api/admins", + Body: strings.NewReader(`{"email":"test@example.com","password":"1234","passwordConfirm":"4321","avatar":99}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`}, + ExpectedEvents: map[string]int{ + "OnAdminBeforeCreateRequest": 1, + }, + }, + { + Name: "authorized as admin + valid data", + Method: http.MethodPost, + Url: "/api/admins", + Body: strings.NewReader(`{"email":"testnew@example.com","password":"1234567890","passwordConfirm":"1234567890","avatar":3}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":`, + `"email":"testnew@example.com"`, + `"avatar":3`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeCreate": 1, + "OnModelAfterCreate": 1, + "OnAdminBeforeCreateRequest": 1, + "OnAdminAfterCreateRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestAdminUpdate(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPatch, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodPatch, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + invalid admin id", + Method: http.MethodPatch, + Url: "/api/admins/invalid", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + nonexisting admin id", + Method: http.MethodPatch, + Url: "/api/admins/b97ccf83-34a2-4d01-a26b-3d77bc842d3c", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + empty data", + Method: http.MethodPatch, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8"`, + `"email":"test2@example.com"`, + `"avatar":2`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnAdminBeforeUpdateRequest": 1, + "OnAdminAfterUpdateRequest": 1, + }, + }, + { + Name: "authorized as admin + invalid formatted data", + Method: http.MethodPatch, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + Body: strings.NewReader(`{`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + invalid data", + Method: http.MethodPatch, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + Body: strings.NewReader(`{"email":"test@example.com","password":"1234","passwordConfirm":"4321","avatar":99}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"avatar":{"code":"validation_max_less_equal_than_required","message":"Must be no greater than 9."},"email":{"code":"validation_admin_email_exists","message":"Admin email already exists."},"password":{"code":"validation_length_out_of_range","message":"The length must be between 10 and 100."},"passwordConfirm":{"code":"validation_values_mismatch","message":"Values don't match."}}`}, + ExpectedEvents: map[string]int{ + "OnAdminBeforeUpdateRequest": 1, + }, + }, + { + Method: http.MethodPatch, + Url: "/api/admins/3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", + Body: strings.NewReader(`{"email":"testnew@example.com","password":"1234567890","passwordConfirm":"1234567890","avatar":5}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8"`, + `"email":"testnew@example.com"`, + `"avatar":5`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnAdminBeforeUpdateRequest": 1, + "OnAdminAfterUpdateRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/base.go b/apis/base.go new file mode 100644 index 00000000..c09f4f75 --- /dev/null +++ b/apis/base.go @@ -0,0 +1,131 @@ +// Package apis implements the default PocketBase api services and middlewares. +package apis + +import ( + "fmt" + "io/fs" + "log" + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/ui" +) + +// InitApi creates a configured echo instance with registered +// system and app specific routes and middlewares. +func InitApi(app core.App) (*echo.Echo, error) { + e := echo.New() + e.Debug = app.IsDebug() + + // default middlewares + e.Pre(middleware.RemoveTrailingSlash()) + e.Use(middleware.Recover()) + e.Use(middleware.Secure()) + e.Use(LoadAuthContext(app)) + + // custom error handler + e.HTTPErrorHandler = func(c echo.Context, err error) { + if c.Response().Committed { + return + } + + var apiErr *rest.ApiError + + switch v := err.(type) { + case (*echo.HTTPError): + if v.Internal != nil && app.IsDebug() { + log.Println(v.Internal) + } + msg := fmt.Sprintf("%v", v.Message) + apiErr = rest.NewApiError(v.Code, msg, v) + case (*rest.ApiError): + if app.IsDebug() && v.RawData() != nil { + log.Println(v.RawData()) + } + apiErr = v + default: + if err != nil && app.IsDebug() { + log.Println(err) + } + apiErr = rest.NewBadRequestError("", err) + } + + // Send response + var cErr error + if c.Request().Method == http.MethodHead { + // @see https://github.com/labstack/echo/issues/608 + cErr = c.NoContent(apiErr.Code) + } else { + cErr = c.JSON(apiErr.Code, apiErr) + } + + // truly rare case; eg. client already disconnected + if cErr != nil && app.IsDebug() { + log.Println(err) + } + } + + // serves /ui/dist/index.html file + // (explicit route is used to avoid conflicts with `RemoveTrailingSlash` middleware) + e.FileFS("/_", "index.html", ui.DistIndexHTML, middleware.Gzip()) + + // serves static files from the /ui/dist directory + // (similar to echo.StaticFS but with gzip middleware enabled) + e.GET("/_/*", StaticDirectoryHandler(ui.DistDirFS, false), middleware.Gzip()) + + // default routes + api := e.Group("/api") + BindSettingsApi(app, api) + BindAdminApi(app, api) + BindUserApi(app, api) + BindCollectionApi(app, api) + BindRecordApi(app, api) + BindFileApi(app, api) + BindRealtimeApi(app, api) + BindLogsApi(app, api) + + // trigger the custom BeforeServe hook for the created api router + // allowing users to further adjust its options or register new routes + serveEvent := &core.ServeEvent{ + App: app, + Router: e, + } + if err := app.OnBeforeServe().Trigger(serveEvent); err != nil { + return nil, err + } + + // catch all any route + api.Any("/*", func(c echo.Context) error { + return echo.ErrNotFound + }, ActivityLogger(app)) + + return e, nil +} + +// StaticDirectoryHandler is similar to `echo.StaticDirectoryHandler` +// but without the directory redirect which conflicts with RemoveTrailingSlash middleware. +// +// @see https://github.com/labstack/echo/issues/2211 +func StaticDirectoryHandler(fileSystem fs.FS, disablePathUnescaping bool) echo.HandlerFunc { + return func(c echo.Context) error { + p := c.PathParam("*") + if !disablePathUnescaping { // when router is already unescaping we do not want to do is twice + tmpPath, err := url.PathUnescape(p) + if err != nil { + return fmt.Errorf("failed to unescape path variable: %w", err) + } + p = tmpPath + } + + // fs.FS.Open() already assumes that file names are relative to FS root path and considers name with prefix `/` as invalid + name := filepath.ToSlash(filepath.Clean(strings.TrimPrefix(p, "/"))) + + return c.FileFS(name, fileSystem) + } +} diff --git a/apis/base_test.go b/apis/base_test.go new file mode 100644 index 00000000..4a29bdaa --- /dev/null +++ b/apis/base_test.go @@ -0,0 +1,122 @@ +package apis_test + +import ( + "errors" + "net/http" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/rest" +) + +func Test404(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Method: http.MethodGet, + Url: "/api/missing", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Method: http.MethodPost, + Url: "/api/missing", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Method: http.MethodPatch, + Url: "/api/missing", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Method: http.MethodDelete, + Url: "/api/missing", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Method: http.MethodHead, + Url: "/api/missing", + ExpectedStatus: 404, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestCustomRoutesAndErrorsHandling(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "custom route", + Method: http.MethodGet, + Url: "/custom", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/custom", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + { + Name: "route with HTTPError", + Method: http.MethodGet, + Url: "/http-error", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/http-error", + Handler: func(c echo.Context) error { + return echo.ErrBadRequest + }, + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`{"code":400,"message":"Bad Request.","data":{}}`}, + }, + { + Name: "route with api error", + Method: http.MethodGet, + Url: "/api-error", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/api-error", + Handler: func(c echo.Context) error { + return rest.NewApiError(500, "test message", errors.New("internal_test")) + }, + }) + }, + ExpectedStatus: 500, + ExpectedContent: []string{`{"code":500,"message":"Test message.","data":{}}`}, + }, + { + Name: "route with plain error", + Method: http.MethodGet, + Url: "/plain-error", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/plain-error", + Handler: func(c echo.Context) error { + return errors.New("Test error") + }, + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`{"code":400,"message":"Something went wrong while processing your request.","data":{}}`}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/collection.go b/apis/collection.go new file mode 100644 index 00000000..7fd9b33c --- /dev/null +++ b/apis/collection.go @@ -0,0 +1,185 @@ +package apis + +import ( + "errors" + "log" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/search" +) + +// BindCollectionApi registers the collection api endpoints and the corresponding handlers. +func BindCollectionApi(app core.App, rg *echo.Group) { + api := collectionApi{app: app} + + subGroup := rg.Group("/collections", ActivityLogger(app), RequireAdminAuth()) + subGroup.GET("", api.list) + subGroup.POST("", api.create) + subGroup.GET("/:collection", api.view) + subGroup.PATCH("/:collection", api.update) + subGroup.DELETE("/:collection", api.delete) +} + +type collectionApi struct { + app core.App +} + +func (api *collectionApi) list(c echo.Context) error { + fieldResolver := search.NewSimpleFieldResolver( + "id", "created", "updated", "name", "system", + ) + + collections := []*models.Collection{} + + result, err := search.NewProvider(fieldResolver). + Query(api.app.Dao().CollectionQuery()). + ParseAndExec(c.QueryString(), &collections) + + if err != nil { + return rest.NewBadRequestError("", err) + } + + event := &core.CollectionsListEvent{ + HttpContext: c, + Collections: collections, + Result: result, + } + + return api.app.OnCollectionsListRequest().Trigger(event, func(e *core.CollectionsListEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.Result) + }) +} + +func (api *collectionApi) view(c echo.Context) error { + collection, err := api.app.Dao().FindCollectionByNameOrId(c.PathParam("collection")) + if err != nil || collection == nil { + return rest.NewNotFoundError("", err) + } + + event := &core.CollectionViewEvent{ + HttpContext: c, + Collection: collection, + } + + return api.app.OnCollectionViewRequest().Trigger(event, func(e *core.CollectionViewEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.Collection) + }) +} + +func (api *collectionApi) create(c echo.Context) error { + collection := &models.Collection{} + + form := forms.NewCollectionUpsert(api.app, collection) + + // read + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.CollectionCreateEvent{ + HttpContext: c, + Collection: collection, + } + + handlerErr := api.app.OnCollectionBeforeCreateRequest().Trigger(event, func(e *core.CollectionCreateEvent) error { + // submit + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to create the collection.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.Collection) + }) + + if handlerErr == nil { + api.app.OnCollectionAfterCreateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *collectionApi) update(c echo.Context) error { + collection, err := api.app.Dao().FindCollectionByNameOrId(c.PathParam("collection")) + if err != nil || collection == nil { + return rest.NewNotFoundError("", err) + } + + form := forms.NewCollectionUpsert(api.app, collection) + + // read + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.CollectionUpdateEvent{ + HttpContext: c, + Collection: collection, + } + + handlerErr := api.app.OnCollectionBeforeUpdateRequest().Trigger(event, func(e *core.CollectionUpdateEvent) error { + // submit + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to update the collection.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.Collection) + }) + + if handlerErr == nil { + api.app.OnCollectionAfterUpdateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *collectionApi) delete(c echo.Context) error { + collection, err := api.app.Dao().FindCollectionByNameOrId(c.PathParam("collection")) + if err != nil || collection == nil { + return rest.NewNotFoundError("", err) + } + + event := &core.CollectionDeleteEvent{ + HttpContext: c, + Collection: collection, + } + + handlerErr := api.app.OnCollectionBeforeDeleteRequest().Trigger(event, func(e *core.CollectionDeleteEvent) error { + if err := api.app.Dao().DeleteCollection(e.Collection); err != nil { + return rest.NewBadRequestError("Failed to delete collection. Make sure that the collection is not referenced by other collections.", err) + } + + // try to delete the collection files + if err := api.deleteCollectionFiles(e.Collection); err != nil && api.app.IsDebug() { + // non critical error - only log for debug + // (usually could happen because of S3 api limits) + log.Println(err) + } + + return e.HttpContext.NoContent(http.StatusNoContent) + }) + + if handlerErr == nil { + api.app.OnCollectionAfterDeleteRequest().Trigger(event) + } + + return handlerErr +} + +func (api *collectionApi) deleteCollectionFiles(collection *models.Collection) error { + fs, err := api.app.NewFilesystem() + if err != nil { + return err + } + defer fs.Close() + + failed := fs.DeletePrefix(collection.BaseFilesPath()) + if len(failed) > 0 { + return errors.New("Failed to delete all record files.") + } + + return nil +} diff --git a/apis/collection_test.go b/apis/collection_test.go new file mode 100644 index 00000000..09f306e5 --- /dev/null +++ b/apis/collection_test.go @@ -0,0 +1,451 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/tests" +) + +func TestCollectionsList(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/collections", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/collections", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin", + Method: http.MethodGet, + Url: "/api/collections", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":5`, + `"items":[{`, + `"id":"abe78266-fd4d-4aea-962d-8c0138ac522b"`, + `"id":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + `"id":"2c1010aa-b8fe-41d9-a980-99534ca8a167"`, + `"id":"3cd6fe92-70dc-4819-8542-4d036faabd89"`, + `"id":"f12f3eb6-b980-4bf6-b1e4-36de0450c8be"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionsListRequest": 1, + }, + }, + { + Name: "authorized as admin + paging and sorting", + Method: http.MethodGet, + Url: "/api/collections?page=2&perPage=2&sort=-created", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":2`, + `"perPage":2`, + `"totalItems":5`, + `"items":[{`, + `"id":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + `"id":"2c1010aa-b8fe-41d9-a980-99534ca8a167"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionsListRequest": 1, + }, + }, + { + Name: "authorized as admin + invalid filter", + Method: http.MethodGet, + Url: "/api/collections?filter=invalidfield~'demo2'", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + valid filter", + Method: http.MethodGet, + Url: "/api/collections?filter=name~'demo2'", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"items":[{`, + `"id":"2c1010aa-b8fe-41d9-a980-99534ca8a167"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionsListRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestCollectionView(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/collections/demo", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/collections/demo", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + nonexisting collection identifier", + Method: http.MethodGet, + Url: "/api/collections/missing", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + using the collection name", + Method: http.MethodGet, + Url: "/api/collections/demo", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionViewRequest": 1, + }, + }, + { + Name: "authorized as admin + using the collection id", + Method: http.MethodGet, + Url: "/api/collections/3f2888f8-075d-49fe-9d09-ea7e951000dc", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionViewRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestCollectionDelete(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodDelete, + Url: "/api/collections/demo3", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodDelete, + Url: "/api/collections/demo3", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + nonexisting collection identifier", + Method: http.MethodDelete, + Url: "/api/collections/b97ccf83-34a2-4d01-a26b-3d77bc842d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + using the collection name", + Method: http.MethodDelete, + Url: "/api/collections/demo3", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnModelBeforeDelete": 1, + "OnModelAfterDelete": 1, + "OnCollectionBeforeDeleteRequest": 1, + "OnCollectionAfterDeleteRequest": 1, + }, + }, + { + Name: "authorized as admin + using the collection id", + Method: http.MethodDelete, + Url: "/api/collections/3cd6fe92-70dc-4819-8542-4d036faabd89", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnModelBeforeDelete": 1, + "OnModelAfterDelete": 1, + "OnCollectionBeforeDeleteRequest": 1, + "OnCollectionAfterDeleteRequest": 1, + }, + }, + { + Name: "authorized as admin + trying to delete a system collection", + Method: http.MethodDelete, + Url: "/api/collections/profiles", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "OnCollectionBeforeDeleteRequest": 1, + }, + }, + { + Name: "authorized as admin + trying to delete a referenced collection", + Method: http.MethodDelete, + Url: "/api/collections/demo", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "OnCollectionBeforeDeleteRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestCollectionCreate(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPost, + Url: "/api/collections", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodPost, + Url: "/api/collections", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + empty data", + Method: http.MethodPost, + Url: "/api/collections", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"name":{"code":"validation_required"`, + `"schema":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionBeforeCreateRequest": 1, + }, + }, + { + Name: "authorized as admin + invalid data (eg. existing name)", + Method: http.MethodPost, + Url: "/api/collections", + Body: strings.NewReader(`{"name":"demo","schema":[{"type":"text","name":""}]}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"name":{"code":"validation_collection_name_exists"`, + `"schema":{"0":{"name":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionBeforeCreateRequest": 1, + }, + }, + { + Name: "authorized as admin + valid data", + Method: http.MethodPost, + Url: "/api/collections", + Body: strings.NewReader(`{"name":"new","schema":[{"type":"text","id":"12345789","name":"test"}]}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":`, + `"name":"new"`, + `"system":false`, + `"schema":[{"system":false,"id":"12345789","name":"test","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeCreate": 1, + "OnModelAfterCreate": 1, + "OnCollectionBeforeCreateRequest": 1, + "OnCollectionAfterCreateRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestCollectionUpdate(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPatch, + Url: "/api/collections/demo", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodPatch, + Url: "/api/collections/demo", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + empty data", + Method: http.MethodPatch, + Url: "/api/collections/demo", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnCollectionBeforeUpdateRequest": 1, + "OnCollectionAfterUpdateRequest": 1, + }, + }, + { + Name: "authorized as admin + invalid data (eg. existing name)", + Method: http.MethodPatch, + Url: "/api/collections/demo", + Body: strings.NewReader(`{"name":"demo2"}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"name":{"code":"validation_collection_name_exists"`, + }, + ExpectedEvents: map[string]int{ + "OnCollectionBeforeUpdateRequest": 1, + }, + }, + { + Name: "authorized as admin + valid data", + Method: http.MethodPatch, + Url: "/api/collections/demo", + Body: strings.NewReader(`{"name":"new"}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + `"name":"new"`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnCollectionBeforeUpdateRequest": 1, + "OnCollectionAfterUpdateRequest": 1, + }, + }, + { + Name: "authorized as admin + valid data and id as identifier", + Method: http.MethodPatch, + Url: "/api/collections/3f2888f8-075d-49fe-9d09-ea7e951000dc", + Body: strings.NewReader(`{"name":"new"}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + `"name":"new"`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnCollectionBeforeUpdateRequest": 1, + "OnCollectionAfterUpdateRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/file.go b/apis/file.go new file mode 100644 index 00000000..160acb63 --- /dev/null +++ b/apis/file.go @@ -0,0 +1,104 @@ +package apis + +import ( + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/rest" +) + +var imageContentTypes = []string{"image/png", "image/jpg", "image/jpeg"} +var defaultThumbSizes = []string{"100x100"} + +// BindFileApi registers the file api endpoints and the corresponding handlers. +func BindFileApi(app core.App, rg *echo.Group) { + api := fileApi{app: app} + + subGroup := rg.Group("/files", ActivityLogger(app)) + subGroup.GET("/:collection/:recordId/:filename", api.download, LoadCollectionContext(api.app)) +} + +type fileApi struct { + app core.App +} + +func (api *fileApi) download(c echo.Context) error { + collection, _ := c.Get(ContextCollectionKey).(*models.Collection) + if collection == nil { + return rest.NewNotFoundError("", nil) + } + + recordId := c.PathParam("recordId") + if recordId == "" { + return rest.NewNotFoundError("", nil) + } + + record, err := api.app.Dao().FindRecordById(collection, recordId, nil) + if err != nil { + return rest.NewNotFoundError("", err) + } + + filename := c.PathParam("filename") + + fileField := record.FindFileFieldByFile(filename) + if fileField == nil { + return rest.NewNotFoundError("", nil) + } + options, _ := fileField.Options.(*schema.FileOptions) + + fs, err := api.app.NewFilesystem() + if err != nil { + return rest.NewBadRequestError("Filesystem initialization failure.", err) + } + defer fs.Close() + + originalPath := record.BaseFilesPath() + "/" + filename + servedPath := originalPath + servedName := filename + + // check for valid thumb size param + thumbSize := c.QueryParam("thumb") + if thumbSize != "" && (list.ExistInSlice(thumbSize, defaultThumbSizes) || list.ExistInSlice(thumbSize, options.Thumbs)) { + // extract the original file meta attributes and check it existence + oAttrs, oAttrsErr := fs.Attributes(originalPath) + if oAttrsErr != nil { + return rest.NewNotFoundError("", err) + } + + // check if it is an image + if list.ExistInSlice(oAttrs.ContentType, imageContentTypes) { + // add thumb size as file suffix + servedName = thumbSize + "_" + filename + servedPath = record.BaseFilesPath() + "/thumbs_" + filename + "/" + servedName + + // check if the thumb exists: + // - if doesn't exist - create a new thumb with the specified thumb size + // - if exists - compare last modified dates to determine whether the thumb should be recreated + tAttrs, tAttrsErr := fs.Attributes(servedPath) + if tAttrsErr != nil || oAttrs.ModTime.After(tAttrs.ModTime) { + if err := fs.CreateThumb(originalPath, servedPath, thumbSize, false); err != nil { + servedPath = originalPath // fallback to the original + } + } + } + } + + event := &core.FileDownloadEvent{ + HttpContext: c, + Record: record, + Collection: collection, + FileField: fileField, + ServedPath: servedPath, + ServedName: servedName, + } + + return api.app.OnFileDownloadRequest().Trigger(event, func(e *core.FileDownloadEvent) error { + if err := fs.Serve(e.HttpContext.Response(), e.ServedPath, e.ServedName); err != nil { + return rest.NewNotFoundError("", err) + } + + return nil + }) +} diff --git a/apis/file_test.go b/apis/file_test.go new file mode 100644 index 00000000..1805a95c --- /dev/null +++ b/apis/file_test.go @@ -0,0 +1,102 @@ +package apis_test + +import ( + "github.com/pocketbase/pocketbase/tests" + "net/http" + "os" + "path" + "path/filepath" + "runtime" + "testing" +) + +func TestFileDownload(t *testing.T) { + _, currentFile, _, _ := runtime.Caller(0) + dataDirRelPath := "../tests/data/" + testFilePath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/848a1dea-5ddd-42d6-a00d-030547bffcfe/8fe61d65-6a2e-4f11-87b3-d8a3170bfd4f.txt") + testImgPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png") + testThumbPath := filepath.Join(path.Dir(currentFile), dataDirRelPath, "storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/100x100_4881bdef-06b4-4dea-8d97-6125ad242677.png") + + testFile, fileErr := os.ReadFile(testFilePath) + if fileErr != nil { + t.Fatal(fileErr) + } + + testImg, imgErr := os.ReadFile(testImgPath) + if imgErr != nil { + t.Fatal(imgErr) + } + + testThumb, thumbErr := os.ReadFile(testThumbPath) + if thumbErr != nil { + t.Fatal(thumbErr) + } + + scenarios := []tests.ApiScenario{ + { + Name: "missing collection", + Method: http.MethodGet, + Url: "/api/files/missing/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing record", + Method: http.MethodGet, + Url: "/api/files/demo/00000000-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing file", + Method: http.MethodGet, + Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/00000000-06b4-4dea-8d97-6125ad242677.png", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "existing image", + Method: http.MethodGet, + Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png", + ExpectedStatus: 200, + ExpectedContent: []string{string(testImg)}, + ExpectedEvents: map[string]int{ + "OnFileDownloadRequest": 1, + }, + }, + { + Name: "existing image - missing thumb (should fallback to the original)", + Method: http.MethodGet, + Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=999x999", + ExpectedStatus: 200, + ExpectedContent: []string{string(testImg)}, + ExpectedEvents: map[string]int{ + "OnFileDownloadRequest": 1, + }, + }, + { + Name: "existing image - existing thumb", + Method: http.MethodGet, + Url: "/api/files/demo/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png?thumb=100x100", + ExpectedStatus: 200, + ExpectedContent: []string{string(testThumb)}, + ExpectedEvents: map[string]int{ + "OnFileDownloadRequest": 1, + }, + }, + { + Name: "existing non image file - thumb parameter should be ignored", + Method: http.MethodGet, + Url: "/api/files/demo/848a1dea-5ddd-42d6-a00d-030547bffcfe/8fe61d65-6a2e-4f11-87b3-d8a3170bfd4f.txt?thumb=100x100", + ExpectedStatus: 200, + ExpectedContent: []string{string(testFile)}, + ExpectedEvents: map[string]int{ + "OnFileDownloadRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/logs.go b/apis/logs.go new file mode 100644 index 00000000..d055047f --- /dev/null +++ b/apis/logs.go @@ -0,0 +1,82 @@ +package apis + +import ( + "net/http" + + "github.com/pocketbase/dbx" + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/search" +) + +// BindLogsApi registers the request logs api endpoints. +func BindLogsApi(app core.App, rg *echo.Group) { + api := logsApi{app: app} + + subGroup := rg.Group("/logs", RequireAdminAuth()) + subGroup.GET("/requests", api.requestsList) + subGroup.GET("/requests/stats", api.requestsStats) + subGroup.GET("/requests/:id", api.requestView) +} + +type logsApi struct { + app core.App +} + +var requestFilterFields = []string{ + "rowid", "id", "created", "updated", + "url", "method", "status", "auth", + "ip", "referer", "userAgent", +} + +func (api *logsApi) requestsList(c echo.Context) error { + fieldResolver := search.NewSimpleFieldResolver(requestFilterFields...) + + result, err := search.NewProvider(fieldResolver). + Query(api.app.LogsDao().RequestQuery()). + ParseAndExec(c.QueryString(), &[]*models.Request{}) + + if err != nil { + return rest.NewBadRequestError("", err) + } + + return c.JSON(http.StatusOK, result) +} + +func (api *logsApi) requestsStats(c echo.Context) error { + fieldResolver := search.NewSimpleFieldResolver(requestFilterFields...) + + filter := c.QueryParam(search.FilterQueryParam) + + var expr dbx.Expression + if filter != "" { + var err error + expr, err = search.FilterData(filter).BuildExpr(fieldResolver) + if err != nil { + return rest.NewBadRequestError("Invalid filter format.", err) + } + } + + stats, err := api.app.LogsDao().RequestsStats(expr) + if err != nil { + return rest.NewBadRequestError("Failed to generate requests stats.", err) + } + + return c.JSON(http.StatusOK, stats) +} + +func (api *logsApi) requestView(c echo.Context) error { + id := c.PathParam("id") + if id == "" { + return rest.NewNotFoundError("", nil) + } + + request, err := api.app.LogsDao().FindRequestById(id) + if err != nil || request == nil { + return rest.NewNotFoundError("", err) + } + + return c.JSON(http.StatusOK, request) +} diff --git a/apis/logs_test.go b/apis/logs_test.go new file mode 100644 index 00000000..0e1607aa --- /dev/null +++ b/apis/logs_test.go @@ -0,0 +1,196 @@ +package apis_test + +import ( + "net/http" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRequestsList(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/logs/requests", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/logs/requests", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin", + Method: http.MethodGet, + Url: "/api/logs/requests", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if err := tests.MockRequestLogsData(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":2`, + `"items":[{`, + `"id":"873f2133-9f38-44fb-bf82-c8f53b310d91"`, + `"id":"f2133873-44fb-9f38-bf82-c918f53b310d"`, + }, + }, + { + Name: "authorized as admin + filter", + Method: http.MethodGet, + Url: "/api/logs/requests?filter=status>200", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if err := tests.MockRequestLogsData(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"items":[{`, + `"id":"f2133873-44fb-9f38-bf82-c918f53b310d"`, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRequestView(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/logs/requests/873f2133-9f38-44fb-bf82-c8f53b310d91", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/logs/requests/873f2133-9f38-44fb-bf82-c8f53b310d91", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin (nonexisting request log)", + Method: http.MethodGet, + Url: "/api/logs/requests/missing1-9f38-44fb-bf82-c8f53b310d91", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if err := tests.MockRequestLogsData(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin (existing request log)", + Method: http.MethodGet, + Url: "/api/logs/requests/873f2133-9f38-44fb-bf82-c8f53b310d91", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if err := tests.MockRequestLogsData(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"873f2133-9f38-44fb-bf82-c8f53b310d91"`, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRequestsStats(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/logs/requests/stats", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/logs/requests/stats", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin", + Method: http.MethodGet, + Url: "/api/logs/requests/stats", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if err := tests.MockRequestLogsData(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `[{"total":1,"date":"2022-05-01 10:00:00.000"},{"total":1,"date":"2022-05-02 10:00:00.000"}]`, + }, + }, + { + Name: "authorized as admin + filter", + Method: http.MethodGet, + Url: "/api/logs/requests/stats?filter=status>200", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if err := tests.MockRequestLogsData(app); err != nil { + t.Fatal(err) + } + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `[{"total":1,"date":"2022-05-02 10:00:00.000"}]`, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/middlewares.go b/apis/middlewares.go new file mode 100644 index 00000000..abf18e4d --- /dev/null +++ b/apis/middlewares.go @@ -0,0 +1,277 @@ +package apis + +import ( + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/routine" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +// Common request context keys used by the middlewares and api handlers. +const ( + ContextUserKey string = "user" + ContextAdminKey string = "admin" + ContextCollectionKey string = "collection" +) + +// RequireGuestOnly middleware requires a request to NOT have a valid +// Authorization header set. +// +// This middleware is the opposite of [apis.RequireAdminOrUserAuth()]. +func RequireGuestOnly() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + err := rest.NewBadRequestError("The request can be accessed only by guests.", nil) + + user, _ := c.Get(ContextUserKey).(*models.User) + if user != nil { + return err + } + + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin != nil { + return err + } + + return next(c) + } + } +} + +// RequireUserAuth middleware requires a request to have +// a valid user Authorization header set (aka. `Authorization: User ...`). +func RequireUserAuth() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + user, _ := c.Get(ContextUserKey).(*models.User) + if user == nil { + return rest.NewUnauthorizedError("The request requires valid user authorization token to be set.", nil) + } + + return next(c) + } + } +} + +// RequireAdminAuth middleware requires a request to have +// a valid admin Authorization header set (aka. `Authorization: Admin ...`). +func RequireAdminAuth() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin == nil { + return rest.NewUnauthorizedError("The request requires admin authorization token to be set.", nil) + } + + return next(c) + } + } +} + +// RequireAdminOrUserAuth middleware requires a request to have +// a valid admin or user Authorization header set +// (aka. `Authorization: Admin ...` or `Authorization: User ...`). +// +// This middleware is the opposite of [apis.RequireGuestOnly()]. +func RequireAdminOrUserAuth() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + user, _ := c.Get(ContextUserKey).(*models.User) + + if admin == nil && user == nil { + return rest.NewUnauthorizedError("The request requires admin or user authorization token to be set.", nil) + } + + return next(c) + } + } +} + +// RequireAdminOrOwnerAuth middleware requires a request to have +// a valid admin or user owner Authorization header set +// (aka. `Authorization: Admin ...` or `Authorization: User ...`). +// +// This middleware is similar to [apis.RequireAdminOrUserAuth()] but +// for the user token expects to have the same id as the path parameter +// `ownerIdParam` (default to "id"). +func RequireAdminOrOwnerAuth(ownerIdParam string) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if ownerIdParam == "" { + ownerIdParam = "id" + } + + ownerId := c.PathParam(ownerIdParam) + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + loggedUser, _ := c.Get(ContextUserKey).(*models.User) + + if admin == nil && loggedUser == nil { + return rest.NewUnauthorizedError("The request requires admin or user authorization token to be set.", nil) + } + + if admin == nil && loggedUser.Id != ownerId { + return rest.NewForbiddenError("You are not allowed to perform this request.", nil) + } + + return next(c) + } + } +} + +// LoadAuthContext middleware reads the Authorization request header +// and loads the token related user or admin instance into the +// request's context. +// +// This middleware is expected to be registered by default for all routes. +func LoadAuthContext(app core.App) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + token := c.Request().Header.Get("Authorization") + + if token != "" { + if strings.HasPrefix(token, "User ") { + user, err := app.Dao().FindUserByToken( + token[5:], + app.Settings().UserAuthToken.Secret, + ) + if err == nil && user != nil { + c.Set(ContextUserKey, user) + } + } else if strings.HasPrefix(token, "Admin ") { + admin, err := app.Dao().FindAdminByToken( + token[6:], + app.Settings().AdminAuthToken.Secret, + ) + if err == nil && admin != nil { + c.Set(ContextAdminKey, admin) + } + } + } + + return next(c) + } + } +} + +// LoadCollectionContext middleware finds the collection with related +// path identifier and loads it into the request context. +func LoadCollectionContext(app core.App) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if param := c.PathParam("collection"); param != "" { + collection, err := app.Dao().FindCollectionByNameOrId(param) + if err != nil || collection == nil { + return rest.NewNotFoundError("", err) + } + + c.Set(ContextCollectionKey, collection) + } + + return next(c) + } + } +} + +// ActivityLogger middleware takes care to save the request information +// into the logs database. +// +// The middleware does nothing if the app logs retention period is zero +// (aka. app.Settings().Logs.MaxDays = 0). +func ActivityLogger(app core.App) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + err := next(c) + + // no logs retention + if app.Settings().Logs.MaxDays == 0 { + return err + } + + httpRequest := c.Request() + httpResponse := c.Response() + status := httpResponse.Status + meta := types.JsonMap{} + + if err != nil { + switch v := err.(type) { + case (*echo.HTTPError): + status = v.Code + meta["errorMessage"] = v.Message + meta["errorDetails"] = fmt.Sprint(v.Internal) + case (*rest.ApiError): + status = v.Code + meta["errorMessage"] = v.Message + meta["errorDetails"] = fmt.Sprint(v.RawData()) + default: + status = http.StatusBadRequest + meta["errorMessage"] = v.Error() + } + } + + requestAuth := models.RequestAuthGuest + if c.Get(ContextUserKey) != nil { + requestAuth = models.RequestAuthUser + } else if c.Get(ContextAdminKey) != nil { + requestAuth = models.RequestAuthAdmin + } + + model := &models.Request{ + Url: httpRequest.URL.RequestURI(), + Method: strings.ToLower(httpRequest.Method), + Status: status, + Auth: requestAuth, + Ip: httpRequest.RemoteAddr, + Referer: httpRequest.Referer(), + UserAgent: httpRequest.UserAgent(), + Meta: meta, + } + // set timestamp fields before firing a new go routine + model.RefreshCreated() + model.RefreshUpdated() + + routine.FireAndForget(func() { + attempts := 1 + + BeginSave: + logErr := app.LogsDao().SaveRequest(model) + if logErr != nil { + // try one more time after 10s in case of SQLITE_BUSY or "database is locked" error + if attempts <= 2 { + attempts++ + time.Sleep(10 * time.Second) + goto BeginSave + } else if app.IsDebug() { + log.Println("Log save failed:", logErr) + } + } + + // Delete old request logs + // --- + now := time.Now() + lastLogsDeletedAt := cast.ToTime(app.Cache().Get("lastLogsDeletedAt")) + daysDiff := (now.Sub(lastLogsDeletedAt).Hours() * 24) + + if daysDiff > float64(app.Settings().Logs.MaxDays) { + deleteErr := app.LogsDao().DeleteOldRequests(now.AddDate(0, 0, -1*app.Settings().Logs.MaxDays)) + if deleteErr == nil { + app.Cache().Set("lastLogsDeletedAt", now) + } else if app.IsDebug() { + log.Println("Logs delete failed:", deleteErr) + } + } + }) + + return err + } + } +} diff --git a/apis/middlewares_test.go b/apis/middlewares_test.go new file mode 100644 index 00000000..0f429a1d --- /dev/null +++ b/apis/middlewares_test.go @@ -0,0 +1,503 @@ +package apis_test + +import ( + "net/http" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRequireGuestOnly(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "valid user token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireGuestOnly(), + }, + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid admin token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireGuestOnly(), + }, + }) + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expired/invalid token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireGuestOnly(), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + { + Name: "guest", + Method: http.MethodGet, + Url: "/my/test", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireGuestOnly(), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRequireUserAuth(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + Url: "/my/test", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireUserAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expired/invalid token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireUserAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid admin token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireUserAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid user token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireUserAuth(), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRequireAdminAuth(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + Url: "/my/test", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expired/invalid token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid user token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid admin token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminAuth(), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRequireAdminOrUserAuth(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + Url: "/my/test", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrUserAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expired/invalid token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrUserAuth(), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid user token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrUserAuth(), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + { + Name: "valid admin token", + Method: http.MethodGet, + Url: "/my/test", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrUserAuth(), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRequireAdminOrOwnerAuth(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "guest", + Method: http.MethodGet, + Url: "/my/test/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test/:id", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrOwnerAuth(""), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expired/invalid token", + Method: http.MethodGet, + Url: "/my/test/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.HkAldxpbn0EybkMfFGQKEJUIYKE5UJA0AjcsrV7Q6Io", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test/:id", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrOwnerAuth(""), + }, + }) + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid user token (different user)", + Method: http.MethodGet, + Url: "/my/test/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + RequestHeaders: map[string]string{ + // test3@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test/:id", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrOwnerAuth(""), + }, + }) + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid user token (owner)", + Method: http.MethodGet, + Url: "/my/test/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test/:id", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrOwnerAuth(""), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + { + Name: "valid admin token", + Method: http.MethodGet, + Url: "/my/test/2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + e.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/my/test/:custom", + Handler: func(c echo.Context) error { + return c.String(200, "test123") + }, + Middlewares: []echo.MiddlewareFunc{ + apis.RequireAdminOrOwnerAuth("custom"), + }, + }) + }, + ExpectedStatus: 200, + ExpectedContent: []string{"test123"}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/realtime.go b/apis/realtime.go new file mode 100644 index 00000000..3a3901f5 --- /dev/null +++ b/apis/realtime.go @@ -0,0 +1,345 @@ +package apis + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/pocketbase/dbx" + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/resolvers" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/search" + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +// BindRealtimeApi registers the realtime api endpoints. +func BindRealtimeApi(app core.App, rg *echo.Group) { + api := realtimeApi{app: app} + + subGroup := rg.Group("/realtime", ActivityLogger(app)) + subGroup.GET("", api.connect) + subGroup.POST("", api.setSubscriptions) + + api.bindEvents() +} + +type realtimeApi struct { + app core.App +} + +func (api *realtimeApi) connect(c echo.Context) error { + cancelCtx, cancelRequest := context.WithCancel(c.Request().Context()) + defer cancelRequest() + c.SetRequest(c.Request().Clone(cancelCtx)) + + // register new subscription client + client := subscriptions.NewDefaultClient() + api.app.SubscriptionsBroker().Register(client) + defer api.app.SubscriptionsBroker().Unregister(client.Id()) + + c.Response().Header().Set("Content-Type", "text/event-stream; charset=UTF-8") + c.Response().Header().Set("Cache-Control", "no-store") + c.Response().Header().Set("Connection", "keep-alive") + + event := &core.RealtimeConnectEvent{ + HttpContext: c, + Client: client, + } + + if err := api.app.OnRealtimeConnectRequest().Trigger(event); err != nil { + return err + } + + // signalize established connection (aka. fire "connect" message) + fmt.Fprint(c.Response(), "id:"+client.Id()+"\n") + fmt.Fprint(c.Response(), "event:PB_CONNECT\n") + fmt.Fprint(c.Response(), "data:{\"clientId\":\""+client.Id()+"\"}\n\n") + c.Response().Flush() + + // start an idle timer to keep track of inactive/forgotten connections + idleDuration := 5 * time.Minute + idleTimer := time.NewTimer(idleDuration) + defer idleTimer.Stop() + + for { + select { + case <-idleTimer.C: + cancelRequest() + case msg, ok := <-client.Channel(): + if !ok { + // channel is closed + if api.app.IsDebug() { + log.Println("Realtime connection closed (closed channel):", client.Id()) + } + return nil + } + + w := c.Response() + fmt.Fprint(w, "id:"+client.Id()+"\n") + fmt.Fprint(w, "event:"+msg.Name+"\n") + fmt.Fprint(w, "data:"+msg.Data+"\n\n") + w.Flush() + + idleTimer.Stop() + idleTimer.Reset(idleDuration) + case <-c.Request().Context().Done(): + // connection is closed + if api.app.IsDebug() { + log.Println("Realtime connection closed (cancelled request):", client.Id()) + } + return nil + } + } +} + +// note: in case of reconnect, clients will have to resubmit all subscriptions again +func (api *realtimeApi) setSubscriptions(c echo.Context) error { + form := forms.NewRealtimeSubscribe() + + // read request data + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("", err) + } + + // validate request data + if err := form.Validate(); err != nil { + return rest.NewBadRequestError("", err) + } + + // find subscription client + client, err := api.app.SubscriptionsBroker().ClientById(form.ClientId) + if err != nil { + return rest.NewNotFoundError("Missing or invalid client id.", err) + } + + // check if the previous request was authorized + oldAuthId := extractAuthIdFromGetter(client) + newAuthId := extractAuthIdFromGetter(c) + if oldAuthId != "" && oldAuthId != newAuthId { + return rest.NewForbiddenError("The current and the previous request authorization don't match.", nil) + } + + event := &core.RealtimeSubscribeEvent{ + HttpContext: c, + Client: client, + Subscriptions: form.Subscriptions, + } + + handlerErr := api.app.OnRealtimeBeforeSubscribeRequest().Trigger(event, func(e *core.RealtimeSubscribeEvent) error { + // update auth state + e.Client.Set(ContextAdminKey, e.HttpContext.Get(ContextAdminKey)) + e.Client.Set(ContextUserKey, e.HttpContext.Get(ContextUserKey)) + + // unsubscribe from any previous existing subscriptions + e.Client.Unsubscribe() + + // subscribe to the new subscriptions + e.Client.Subscribe(e.Subscriptions...) + + return e.HttpContext.NoContent(http.StatusNoContent) + }) + + if handlerErr == nil { + api.app.OnRealtimeAfterSubscribeRequest().Trigger(event) + } + + return handlerErr +} + +func (api *realtimeApi) bindEvents() { + userTable := (&models.User{}).TableName() + adminTable := (&models.Admin{}).TableName() + + // update user/admin auth state + api.app.OnModelAfterUpdate().Add(func(data *core.ModelEvent) error { + modelTable := data.Model.TableName() + + var contextKey string + if modelTable == userTable { + contextKey = ContextUserKey + } else if modelTable == adminTable { + contextKey = ContextAdminKey + } else { + return nil + } + + for _, client := range api.app.SubscriptionsBroker().Clients() { + model, _ := client.Get(contextKey).(models.Model) + if model != nil && model.GetId() == data.Model.GetId() { + client.Set(contextKey, data.Model) + } + } + + return nil + }) + + // remove user/admin client(s) + api.app.OnModelAfterDelete().Add(func(data *core.ModelEvent) error { + modelTable := data.Model.TableName() + + var contextKey string + if modelTable == userTable { + contextKey = ContextUserKey + } else if modelTable == adminTable { + contextKey = ContextAdminKey + } else { + return nil + } + + for _, client := range api.app.SubscriptionsBroker().Clients() { + model, _ := client.Get(contextKey).(models.Model) + if model != nil && model.GetId() == data.Model.GetId() { + api.app.SubscriptionsBroker().Unregister(client.Id()) + } + } + + return nil + }) + + api.app.OnRecordAfterCreateRequest().Add(func(data *core.RecordCreateEvent) error { + api.broadcastRecord("create", data.Record) + return nil + }) + + api.app.OnRecordAfterUpdateRequest().Add(func(data *core.RecordUpdateEvent) error { + api.broadcastRecord("update", data.Record) + return nil + }) + + api.app.OnRecordAfterDeleteRequest().Add(func(data *core.RecordDeleteEvent) error { + api.broadcastRecord("delete", data.Record) + return nil + }) +} + +func (api *realtimeApi) canAccessRecord(client subscriptions.Client, record *models.Record, accessRule *string) bool { + admin, _ := client.Get(ContextAdminKey).(*models.Admin) + if admin != nil { + // admins can access everything + return true + } + + if accessRule == nil { + // only admins can access this record + return false + } + + ruleFunc := func(q *dbx.SelectQuery) error { + if *accessRule == "" { + return nil // empty public rule + } + + // emulate request data + requestData := map[string]any{ + "method": "get", + "query": map[string]any{}, + "data": map[string]any{}, + "user": nil, + } + user, _ := client.Get(ContextUserKey).(*models.User) + if user != nil { + requestData["user"], _ = user.AsMap() + } + + resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestData) + expr, err := search.FilterData(*accessRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + + return nil + } + + foundRecord, err := api.app.Dao().FindRecordById(record.Collection(), record.Id, ruleFunc) + if err == nil && foundRecord != nil { + return true + } + + return false +} + +type recordData struct { + Action string `json:"action"` + Record *models.Record `json:"record"` +} + +func (api *realtimeApi) broadcastRecord(action string, record *models.Record) error { + collection := record.Collection() + if collection == nil { + return errors.New("Record collection not set.") + } + + clients := api.app.SubscriptionsBroker().Clients() + if len(clients) == 0 { + return nil // no subscribers + } + + subscriptionRuleMap := map[string]*string{ + (collection.Name + "/" + record.Id): collection.ViewRule, + (collection.Id + "/" + record.Id): collection.ViewRule, + collection.Name: collection.ListRule, + collection.Id: collection.ListRule, + } + + recordData := &recordData{ + Action: action, + Record: record, + } + + serializedData, err := json.Marshal(recordData) + if err != nil { + if api.app.IsDebug() { + log.Println(err) + } + return err + } + + for _, client := range clients { + for subscription, rule := range subscriptionRuleMap { + if !client.HasSubscription(subscription) { + continue + } + + if !api.canAccessRecord(client, record, rule) { + continue + } + + msg := subscriptions.Message{ + Name: subscription, + Data: string(serializedData), + } + + client.Channel() <- msg + } + } + + return nil +} + +type getter interface { + Get(string) any +} + +func extractAuthIdFromGetter(val getter) string { + user, _ := val.Get(ContextUserKey).(*models.User) + if user != nil { + return user.Id + } + + admin, _ := val.Get(ContextAdminKey).(*models.Admin) + if admin != nil { + return admin.Id + } + + return "" +} diff --git a/apis/realtime_test.go b/apis/realtime_test.go new file mode 100644 index 00000000..7dd9191f --- /dev/null +++ b/apis/realtime_test.go @@ -0,0 +1,292 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +func TestRealtimeConnect(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Method: http.MethodGet, + Url: "/api/realtime", + ExpectedStatus: 200, + ExpectedContent: []string{ + `id:`, + `event:PB_CONNECT`, + `data:{"clientId":`, + }, + ExpectedEvents: map[string]int{ + "OnRealtimeConnectRequest": 1, + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if len(app.SubscriptionsBroker().Clients()) != 0 { + t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients())) + } + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRealtimeSubscribe(t *testing.T) { + client := subscriptions.NewDefaultClient() + + resetClient := func() { + client.Unsubscribe() + client.Set(apis.ContextAdminKey, nil) + client.Set(apis.ContextUserKey, nil) + } + + scenarios := []tests.ApiScenario{ + { + Name: "missing client", + Method: http.MethodPost, + Url: "/api/realtime", + Body: strings.NewReader(`{"clientId":"missing","subscriptions":["test1", "test2"]}`), + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "existing client - empty subscriptions", + Method: http.MethodPost, + Url: "/api/realtime", + Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":[]}`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRealtimeBeforeSubscribeRequest": 1, + "OnRealtimeAfterSubscribeRequest": 1, + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + client.Subscribe("test0") + app.SubscriptionsBroker().Register(client) + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + if len(client.Subscriptions()) != 0 { + t.Errorf("Expected no subscriptions, got %v", client.Subscriptions()) + } + resetClient() + }, + }, + { + Name: "existing client - 2 new subscriptions", + Method: http.MethodPost, + Url: "/api/realtime", + Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRealtimeBeforeSubscribeRequest": 1, + "OnRealtimeAfterSubscribeRequest": 1, + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + client.Subscribe("test0") + app.SubscriptionsBroker().Register(client) + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + expectedSubs := []string{"test1", "test2"} + if len(expectedSubs) != len(client.Subscriptions()) { + t.Errorf("Expected subscriptions %v, got %v", expectedSubs, client.Subscriptions()) + } + + for _, s := range expectedSubs { + if !client.HasSubscription(s) { + t.Errorf("Cannot find %q subscription in %v", s, client.Subscriptions()) + } + } + resetClient() + }, + }, + { + Name: "existing client - authorized admin", + Method: http.MethodPost, + Url: "/api/realtime", + Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRealtimeBeforeSubscribeRequest": 1, + "OnRealtimeAfterSubscribeRequest": 1, + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + app.SubscriptionsBroker().Register(client) + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + admin, _ := client.Get(apis.ContextAdminKey).(*models.Admin) + if admin == nil { + t.Errorf("Expected admin auth model, got nil") + } + resetClient() + }, + }, + { + Name: "existing client - authorized user", + Method: http.MethodPost, + Url: "/api/realtime", + Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRealtimeBeforeSubscribeRequest": 1, + "OnRealtimeAfterSubscribeRequest": 1, + }, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + app.SubscriptionsBroker().Register(client) + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + user, _ := client.Get(apis.ContextUserKey).(*models.User) + if user == nil { + t.Errorf("Expected user auth model, got nil") + } + resetClient() + }, + }, + { + Name: "existing client - mismatched auth", + Method: http.MethodPost, + Url: "/api/realtime", + Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test1", "test2"]}`), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + initialAuth := &models.User{} + initialAuth.RefreshId() + client.Set(apis.ContextUserKey, initialAuth) + + app.SubscriptionsBroker().Register(client) + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + user, _ := client.Get(apis.ContextUserKey).(*models.User) + if user == nil { + t.Errorf("Expected user auth model, got nil") + } + resetClient() + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRealtimeUserDeleteEvent(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + apis.InitApi(testApp) + + user, err := testApp.Dao().FindUserByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + + client := subscriptions.NewDefaultClient() + client.Set(apis.ContextUserKey, user) + testApp.SubscriptionsBroker().Register(client) + + testApp.OnModelAfterDelete().Trigger(&core.ModelEvent{Dao: testApp.Dao(), Model: user}) + + if len(testApp.SubscriptionsBroker().Clients()) != 0 { + t.Fatalf("Expected no subscription clients, found %d", len(testApp.SubscriptionsBroker().Clients())) + } +} + +func TestRealtimeUserUpdateEvent(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + apis.InitApi(testApp) + + user1, err := testApp.Dao().FindUserByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + + client := subscriptions.NewDefaultClient() + client.Set(apis.ContextUserKey, user1) + testApp.SubscriptionsBroker().Register(client) + + // refetch the user and change its email + user2, err := testApp.Dao().FindUserByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + user2.Email = "new@example.com" + + testApp.OnModelAfterUpdate().Trigger(&core.ModelEvent{Dao: testApp.Dao(), Model: user2}) + + clientUser, _ := client.Get(apis.ContextUserKey).(*models.User) + if clientUser.Email != user2.Email { + t.Fatalf("Expected user with email %q, got %q", user2.Email, clientUser.Email) + } +} + +func TestRealtimeAdminDeleteEvent(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + apis.InitApi(testApp) + + admin, err := testApp.Dao().FindAdminByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + + client := subscriptions.NewDefaultClient() + client.Set(apis.ContextAdminKey, admin) + testApp.SubscriptionsBroker().Register(client) + + testApp.OnModelAfterDelete().Trigger(&core.ModelEvent{Dao: testApp.Dao(), Model: admin}) + + if len(testApp.SubscriptionsBroker().Clients()) != 0 { + t.Fatalf("Expected no subscription clients, found %d", len(testApp.SubscriptionsBroker().Clients())) + } +} + +func TestRealtimeAdminUpdateEvent(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + apis.InitApi(testApp) + + admin1, err := testApp.Dao().FindAdminByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + + client := subscriptions.NewDefaultClient() + client.Set(apis.ContextAdminKey, admin1) + testApp.SubscriptionsBroker().Register(client) + + // refetch the user and change its email + admin2, err := testApp.Dao().FindAdminByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + admin2.Email = "new@example.com" + + testApp.OnModelAfterUpdate().Trigger(&core.ModelEvent{Dao: testApp.Dao(), Model: admin2}) + + clientAdmin, _ := client.Get(apis.ContextAdminKey).(*models.Admin) + if clientAdmin.Email != admin2.Email { + t.Fatalf("Expected user with email %q, got %q", admin2.Email, clientAdmin.Email) + } +} diff --git a/apis/record.go b/apis/record.go new file mode 100644 index 00000000..1ac3e508 --- /dev/null +++ b/apis/record.go @@ -0,0 +1,432 @@ +package apis + +import ( + "fmt" + "log" + "net/http" + "strings" + + "github.com/pocketbase/dbx" + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/resolvers" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/search" +) + +const expandQueryParam = "expand" + +// BindRecordApi registers the record api endpoints and the corresponding handlers. +func BindRecordApi(app core.App, rg *echo.Group) { + api := recordApi{app: app} + + subGroup := rg.Group( + "/collections/:collection/records", + ActivityLogger(app), + LoadCollectionContext(app), + ) + + subGroup.GET("", api.list) + subGroup.POST("", api.create) + subGroup.GET("/:id", api.view) + subGroup.PATCH("/:id", api.update) + subGroup.DELETE("/:id", api.delete) +} + +type recordApi struct { + app core.App +} + +func (api *recordApi) list(c echo.Context) error { + collection, _ := c.Get(ContextCollectionKey).(*models.Collection) + if collection == nil { + return rest.NewNotFoundError("", "Missing collection context.") + } + + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin == nil && collection.ListRule == nil { + // only admins can access if the rule is nil + return rest.NewForbiddenError("Only admins can perform this action.", nil) + } + + // forbid user/guest defined non-relational joins (aka. @collection.*) + queryStr := c.QueryString() + if admin == nil && queryStr != "" && (strings.Contains(queryStr, "@collection") || strings.Contains(queryStr, "%40collection")) { + return rest.NewForbiddenError("Only admins can filter by @collection.", nil) + } + + requestData := api.exportRequestData(c) + + fieldsResolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData) + + searchProvider := search.NewProvider(fieldsResolver). + Query(api.app.Dao().RecordQuery(collection)) + + if admin == nil && collection.ListRule != nil { + searchProvider.AddFilter(search.FilterData(*collection.ListRule)) + } + + var rawRecords = []dbx.NullStringMap{} + result, err := searchProvider.ParseAndExec(queryStr, &rawRecords) + if err != nil { + return rest.NewBadRequestError("Invalid filter parameters.", err) + } + + records := models.NewRecordsFromNullStringMaps(collection, rawRecords) + + // expand records relations + expands := strings.Split(c.QueryParam(expandQueryParam), ",") + if len(expands) > 0 { + expandErr := api.app.Dao().ExpandRecords( + records, + expands, + api.expandFunc(c, requestData), + ) + if expandErr != nil && api.app.IsDebug() { + log.Println("Failed to expand relations: ", expandErr) + } + } + + result.Items = records + + event := &core.RecordsListEvent{ + HttpContext: c, + Collection: collection, + Records: records, + Result: result, + } + + return api.app.OnRecordsListRequest().Trigger(event, func(e *core.RecordsListEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.Result) + }) +} + +func (api *recordApi) view(c echo.Context) error { + collection, _ := c.Get(ContextCollectionKey).(*models.Collection) + if collection == nil { + return rest.NewNotFoundError("", "Missing collection context.") + } + + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin == nil && collection.ViewRule == nil { + // only admins can access if the rule is nil + return rest.NewForbiddenError("Only admins can perform this action.", nil) + } + + recordId := c.PathParam("id") + if recordId == "" { + return rest.NewNotFoundError("", nil) + } + + requestData := api.exportRequestData(c) + + ruleFunc := func(q *dbx.SelectQuery) error { + if admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" { + resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData) + expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + } + return nil + } + + record, fetchErr := api.app.Dao().FindRecordById(collection, recordId, ruleFunc) + if fetchErr != nil || record == nil { + return rest.NewNotFoundError("", fetchErr) + } + + expands := strings.Split(c.QueryParam(expandQueryParam), ",") + if len(expands) > 0 { + expandErr := api.app.Dao().ExpandRecord( + record, + expands, + api.expandFunc(c, requestData), + ) + if expandErr != nil && api.app.IsDebug() { + log.Println("Failed to expand relations: ", expandErr) + } + } + + event := &core.RecordViewEvent{ + HttpContext: c, + Record: record, + } + + return api.app.OnRecordViewRequest().Trigger(event, func(e *core.RecordViewEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.Record) + }) +} + +func (api *recordApi) create(c echo.Context) error { + collection, _ := c.Get(ContextCollectionKey).(*models.Collection) + if collection == nil { + return rest.NewNotFoundError("", "Missing collection context.") + } + + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin == nil && collection.CreateRule == nil { + // only admins can access if the rule is nil + return rest.NewForbiddenError("Only admins can perform this action.", nil) + } + + requestData := api.exportRequestData(c) + + // temporary save the record and check it against the create rule + if admin == nil && collection.CreateRule != nil && *collection.CreateRule != "" { + ruleFunc := func(q *dbx.SelectQuery) error { + resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData) + expr, err := search.FilterData(*collection.CreateRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + return nil + } + + testRecord := models.NewRecord(collection) + testForm := forms.NewRecordUpsert(api.app, testRecord) + if err := testForm.LoadData(c.Request()); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + testErr := testForm.DrySubmit(func(txDao *daos.Dao) error { + _, fetchErr := txDao.FindRecordById(collection, testRecord.Id, ruleFunc) + return fetchErr + }) + if testErr != nil { + return rest.NewBadRequestError("Failed to create record.", testErr) + } + } + + record := models.NewRecord(collection) + form := forms.NewRecordUpsert(api.app, record) + + // load request + if err := form.LoadData(c.Request()); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.RecordCreateEvent{ + HttpContext: c, + Record: record, + } + + handlerErr := api.app.OnRecordBeforeCreateRequest().Trigger(event, func(e *core.RecordCreateEvent) error { + // create the record + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to create record.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.Record) + }) + + if handlerErr == nil { + api.app.OnRecordAfterCreateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *recordApi) update(c echo.Context) error { + collection, _ := c.Get(ContextCollectionKey).(*models.Collection) + if collection == nil { + return rest.NewNotFoundError("", "Missing collection context.") + } + + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin == nil && collection.UpdateRule == nil { + // only admins can access if the rule is nil + return rest.NewForbiddenError("Only admins can perform this action.", nil) + } + + recordId := c.PathParam("id") + if recordId == "" { + return rest.NewNotFoundError("", nil) + } + + requestData := api.exportRequestData(c) + + ruleFunc := func(q *dbx.SelectQuery) error { + if admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" { + resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData) + expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + } + return nil + } + + // fetch record + record, fetchErr := api.app.Dao().FindRecordById(collection, recordId, ruleFunc) + if fetchErr != nil || record == nil { + return rest.NewNotFoundError("", fetchErr) + } + + form := forms.NewRecordUpsert(api.app, record) + + // load request + if err := form.LoadData(c.Request()); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.RecordUpdateEvent{ + HttpContext: c, + Record: record, + } + + handlerErr := api.app.OnRecordBeforeUpdateRequest().Trigger(event, func(e *core.RecordUpdateEvent) error { + // update the record + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to update record.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.Record) + }) + + if handlerErr == nil { + api.app.OnRecordAfterUpdateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *recordApi) delete(c echo.Context) error { + collection, _ := c.Get(ContextCollectionKey).(*models.Collection) + if collection == nil { + return rest.NewNotFoundError("", "Missing collection context.") + } + + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + if admin == nil && collection.DeleteRule == nil { + // only admins can access if the rule is nil + return rest.NewForbiddenError("Only admins can perform this action.", nil) + } + + recordId := c.PathParam("id") + if recordId == "" { + return rest.NewNotFoundError("", nil) + } + + requestData := api.exportRequestData(c) + + ruleFunc := func(q *dbx.SelectQuery) error { + if admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" { + resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData) + expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + } + return nil + } + + record, fetchErr := api.app.Dao().FindRecordById(collection, recordId, ruleFunc) + if fetchErr != nil || record == nil { + return rest.NewNotFoundError("", fetchErr) + } + + event := &core.RecordDeleteEvent{ + HttpContext: c, + Record: record, + } + + handlerErr := api.app.OnRecordBeforeDeleteRequest().Trigger(event, func(e *core.RecordDeleteEvent) error { + // delete the record + if err := api.app.Dao().DeleteRecord(e.Record); err != nil { + return rest.NewBadRequestError("Failed to delete record. Make sure that the record is not part of a required relation reference.", err) + } + + // try to delete the record files + if err := api.deleteRecordFiles(e.Record); err != nil && api.app.IsDebug() { + // non critical error - only log for debug + // (usually could happen due to S3 api limits) + log.Println(err) + } + + return e.HttpContext.NoContent(http.StatusNoContent) + }) + + if handlerErr == nil { + api.app.OnRecordAfterDeleteRequest().Trigger(event) + } + + return handlerErr +} + +func (api *recordApi) deleteRecordFiles(record *models.Record) error { + fs, err := api.app.NewFilesystem() + if err != nil { + return err + } + defer fs.Close() + + failed := fs.DeletePrefix(record.BaseFilesPath()) + if len(failed) > 0 { + return fmt.Errorf("Failed to delete %d record files.", len(failed)) + } + + return nil +} + +func (api *recordApi) exportRequestData(c echo.Context) map[string]any { + result := map[string]any{} + queryParams := map[string]any{} + bodyData := map[string]any{} + method := c.Request().Method + + echo.BindQueryParams(c, &queryParams) + + rest.BindBody(c, &bodyData) + + result["method"] = method + result["query"] = queryParams + result["data"] = bodyData + result["user"] = nil + + loggedUser, _ := c.Get(ContextUserKey).(*models.User) + if loggedUser != nil { + result["user"], _ = loggedUser.AsMap() + } + + return result +} + +func (api *recordApi) expandFunc(c echo.Context, requestData map[string]any) daos.ExpandFetchFunc { + admin, _ := c.Get(ContextAdminKey).(*models.Admin) + + return func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) { + return api.app.Dao().FindRecordsByIds(relCollection, relIds, func(q *dbx.SelectQuery) error { + if admin != nil { + return nil // admin can access everything + } + + if relCollection.ViewRule == nil { + return fmt.Errorf("Only admins can view collection %q records", relCollection.Name) + } + + if *relCollection.ViewRule != "" { + resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), relCollection, requestData) + expr, err := search.FilterData(*(relCollection.ViewRule)).BuildExpr(resolver) + if err != nil { + return err + } + resolver.UpdateQuery(q) + q.AndWhere(expr) + } + + return nil + }) + } +} diff --git a/apis/record_test.go b/apis/record_test.go new file mode 100644 index 00000000..00eab00c --- /dev/null +++ b/apis/record_test.go @@ -0,0 +1,914 @@ +package apis_test + +import ( + "net/http" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordsList(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "missing collection", + Method: http.MethodGet, + Url: "/api/collections/missing/records", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "unauthorized trying to access nil rule collection (aka. need admin auth)", + Method: http.MethodGet, + Url: "/api/collections/demo/records", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user trying to access nil rule collection (aka. need admin auth)", + Method: http.MethodGet, + Url: "/api/collections/demo/records", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "public collection but with admin only filter/sort (aka. @collection)", + Method: http.MethodGet, + Url: "/api/collections/demo3/records?filter=@collection.demo.title='test'", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "public collection but with ENCODED admin only filter/sort (aka. @collection)", + Method: http.MethodGet, + Url: "/api/collections/demo3/records?filter=%40collection.demo.title%3D%27test%27", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin trying to access nil rule collection (aka. need admin auth)", + Method: http.MethodGet, + Url: "/api/collections/demo/records", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":3`, + `"items":[{`, + `"id":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`, + `"id":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + `"id":"b5c2ffc2-bafd-48f7-b8b7-090638afe209"`, + }, + ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + }, + { + Name: "public collection", + Method: http.MethodGet, + Url: "/api/collections/demo3/records", + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"items":[{`, + `"id":"2c542824-9de1-42fe-8924-e57c86267760"`, + }, + ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + }, + { + Name: "using the collection id as identifier", + Method: http.MethodGet, + Url: "/api/collections/3cd6fe92-70dc-4819-8542-4d036faabd89/records", + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":1`, + `"items":[{`, + `"id":"2c542824-9de1-42fe-8924-e57c86267760"`, + }, + ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + }, + { + Name: "valid query params", + Method: http.MethodGet, + Url: "/api/collections/demo/records?filter=title%7E%27test%27&sort=-title", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":2`, + `"items":[{`, + `"id":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`, + `"id":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + }, + ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + }, + { + Name: "invalid filter", + Method: http.MethodGet, + Url: "/api/collections/demo/records?filter=invalid~'test'", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expand", + Method: http.MethodGet, + Url: "/api/collections/demo2/records?expand=manyrels,onerel&perPage=2&sort=created", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":2`, + `"totalItems":2`, + `"items":[{`, + `"id":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + `"id":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`, + `"manyrels":[{`, + `"manyrels":[]`, + `"rel_cascade":"`, + `"rel_cascade":null`, + `"onerel":{"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc","@collectionName":"demo",`, + `"json":[1,2,3]`, + `"select":["a","b"]`, + `"select":[]`, + `"user":null`, + `"bool":true`, + `"number":456`, + `"user":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`, + }, + ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + }, + { + Name: "authorized as user that DOESN'T match the collection list rule", + Method: http.MethodGet, + Url: "/api/collections/demo2/records", + RequestHeaders: map[string]string{ + // test@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":0`, + `"items":[]`, + }, + ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + }, + { + Name: "authorized as user that matches the collection list rule", + Method: http.MethodGet, + Url: "/api/collections/demo2/records", + RequestHeaders: map[string]string{ + // test3@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":2`, + `"items":[{`, + `"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`, + `"id":"94568ca2-0bee-49d7-b749-06cb97956fd9"`, + }, + ExpectedEvents: map[string]int{"OnRecordsListRequest": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordView(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "missing collection", + Method: http.MethodGet, + Url: "/api/collections/missing/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing record (unauthorized)", + Method: http.MethodGet, + Url: "/api/collections/demo/records/00000000-bafd-48f7-b8b7-090638afe209", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "invalid record id (authorized)", + Method: http.MethodGet, + Url: "/api/collections/demo/records/invalid", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing record (authorized)", + Method: http.MethodGet, + Url: "/api/collections/demo/records/00000000-bafd-48f7-b8b7-090638afe209", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "mismatched collection-record pair (unauthorized)", + Method: http.MethodGet, + Url: "/api/collections/demo/records/63c2ab80-84ab-4057-a592-4604a731f78f", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "mismatched collection-record pair (authorized)", + Method: http.MethodGet, + Url: "/api/collections/demo/records/63c2ab80-84ab-4057-a592-4604a731f78f", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "unauthorized trying to access nil rule collection (aka. need admin auth)", + Method: http.MethodGet, + Url: "/api/collections/demo/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user trying to access nil rule collection (aka. need admin auth)", + Method: http.MethodGet, + Url: "/api/collections/demo/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "access record as admin", + Method: http.MethodGet, + Url: "/api/collections/demo/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + `"@collectionName":"demo"`, + `"id":"b5c2ffc2-bafd-48f7-b8b7-090638afe209"`, + }, + ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + }, + { + Name: "access record as admin (using the collection id as identifier)", + Method: http.MethodGet, + Url: "/api/collections/3f2888f8-075d-49fe-9d09-ea7e951000dc/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + `"@collectionName":"demo"`, + `"id":"b5c2ffc2-bafd-48f7-b8b7-090638afe209"`, + }, + ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + }, + { + Name: "access record as admin (test rule skipping)", + Method: http.MethodGet, + Url: "/api/collections/demo2/records/94568ca2-0bee-49d7-b749-06cb97956fd9", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"@collectionId":"2c1010aa-b8fe-41d9-a980-99534ca8a167"`, + `"@collectionName":"demo2"`, + `"id":"94568ca2-0bee-49d7-b749-06cb97956fd9"`, + `"manyrels":[]`, + `"onerel":"b5c2ffc2-bafd-48f7-b8b7-090638afe209"`, + }, + ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + }, + { + Name: "access record as user (filter mismatch)", + Method: http.MethodGet, + Url: "/api/collections/demo2/records/94568ca2-0bee-49d7-b749-06cb97956fd9", + RequestHeaders: map[string]string{ + // test3@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "access record as user (filter match)", + Method: http.MethodGet, + Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f", + RequestHeaders: map[string]string{ + // test3@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"@collectionId":"2c1010aa-b8fe-41d9-a980-99534ca8a167"`, + `"@collectionName":"demo2"`, + `"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`, + `"manyrels":["848a1dea-5ddd-42d6-a00d-030547bffcfe","577bd676-aacb-4072-b7da-99d00ee210a4"]`, + `"onerel":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`, + }, + ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + }, + { + Name: "expand relations", + Method: http.MethodGet, + Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f?expand=manyrels,onerel", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"@collectionId":"2c1010aa-b8fe-41d9-a980-99534ca8a167"`, + `"@collectionName":"demo2"`, + `"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`, + `"manyrels":[{`, + `"onerel":{`, + `"@collectionId":"3f2888f8-075d-49fe-9d09-ea7e951000dc"`, + `"@collectionName":"demo"`, + `"id":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`, + `"id":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + }, + ExpectedEvents: map[string]int{"OnRecordViewRequest": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordDelete(t *testing.T) { + ensureDeletedFiles := func(app *tests.TestApp, collectionId string, recordId string) { + storageDir := filepath.Join(app.DataDir(), "storage", collectionId, recordId) + + entries, _ := os.ReadDir(storageDir) + if len(entries) != 0 { + t.Errorf("Expected empty/deleted dir, found %d", len(entries)) + } + } + + scenarios := []tests.ApiScenario{ + { + Name: "missing collection", + Method: http.MethodDelete, + Url: "/api/collections/missing/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing record (unauthorized)", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/00000000-bafd-48f7-b8b7-090638afe209", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing record (authorized)", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/00000000-bafd-48f7-b8b7-090638afe209", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "mismatched collection-record pair (unauthorized)", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/63c2ab80-84ab-4057-a592-4604a731f78f", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "mismatched collection-record pair (authorized)", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/63c2ab80-84ab-4057-a592-4604a731f78f", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "unauthorized trying to access nil rule collection (aka. need admin auth)", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/577bd676-aacb-4072-b7da-99d00ee210a4", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user trying to access nil rule collection (aka. need admin auth)", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/577bd676-aacb-4072-b7da-99d00ee210a4", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "access record as admin", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/577bd676-aacb-4072-b7da-99d00ee210a4", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRecordBeforeDeleteRequest": 1, + "OnRecordAfterDeleteRequest": 1, + "OnModelAfterUpdate": 1, // nullify related record + "OnModelBeforeUpdate": 1, // nullify related record + "OnModelBeforeDelete": 2, // +1 cascade delete related record + "OnModelAfterDelete": 2, // +1 cascade delete related record + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4") + }, + }, + { + Name: "access record as admin (using the collection id as identifier)", + Method: http.MethodDelete, + Url: "/api/collections/3f2888f8-075d-49fe-9d09-ea7e951000dc/records/577bd676-aacb-4072-b7da-99d00ee210a4", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRecordBeforeDeleteRequest": 1, + "OnRecordAfterDeleteRequest": 1, + "OnModelAfterUpdate": 1, // nullify related record + "OnModelBeforeUpdate": 1, // nullify related record + "OnModelBeforeDelete": 2, // +1 cascade delete related record + "OnModelAfterDelete": 2, // +1 cascade delete related record + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4") + }, + }, + { + Name: "deleting record as admin (test rule skipping)", + Method: http.MethodDelete, + Url: "/api/collections/demo2/records/94568ca2-0bee-49d7-b749-06cb97956fd9", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRecordBeforeDeleteRequest": 1, + "OnRecordAfterDeleteRequest": 1, + "OnModelBeforeDelete": 1, + "OnModelAfterDelete": 1, + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "94568ca2-0bee-49d7-b749-06cb97956fd9") + }, + }, + { + Name: "deleting record as user (filter mismatch)", + Method: http.MethodDelete, + Url: "/api/collections/demo2/records/94568ca2-0bee-49d7-b749-06cb97956fd9", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "deleting record as user (filter match)", + Method: http.MethodDelete, + Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRecordBeforeDeleteRequest": 1, + "OnRecordAfterDeleteRequest": 1, + "OnModelBeforeDelete": 1, + "OnModelAfterDelete": 1, + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f") + }, + }, + { + Name: "trying to delete record while being part of a non-cascade required relation", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/848a1dea-5ddd-42d6-a00d-030547bffcfe", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + ExpectedEvents: map[string]int{ + "OnRecordBeforeDeleteRequest": 1, + }, + }, + { + Name: "cascade delete referenced records", + Method: http.MethodDelete, + Url: "/api/collections/demo/records/577bd676-aacb-4072-b7da-99d00ee210a4", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnRecordBeforeDeleteRequest": 1, + "OnRecordAfterDeleteRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnModelBeforeDelete": 2, + "OnModelAfterDelete": 2, + }, + AfterFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + recId := "63c2ab80-84ab-4057-a592-4604a731f78f" + col, _ := app.Dao().FindCollectionByNameOrId("demo2") + rec, _ := app.Dao().FindRecordById(col, recId, nil) + if rec != nil { + t.Errorf("Expected record %s to be cascade deleted", recId) + } + ensureDeletedFiles(app, "3f2888f8-075d-49fe-9d09-ea7e951000dc", "577bd676-aacb-4072-b7da-99d00ee210a4") + ensureDeletedFiles(app, "2c1010aa-b8fe-41d9-a980-99534ca8a167", "63c2ab80-84ab-4057-a592-4604a731f78f") + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordCreate(t *testing.T) { + formData, mp, err := tests.MockMultipartData(map[string]string{ + "title": "new", + }, "file") + if err != nil { + t.Fatal(err) + } + + scenarios := []tests.ApiScenario{ + { + Name: "missing collection", + Method: http.MethodPost, + Url: "/api/collections/missing/records", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "guest trying to access nil-rule collection", + Method: http.MethodPost, + Url: "/api/collections/demo/records", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "user trying to access nil-rule collection", + Method: http.MethodPost, + Url: "/api/collections/demo/records", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "submit invalid format", + Method: http.MethodPost, + Url: "/api/collections/demo3/records", + Body: strings.NewReader(`{"`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "submit nil body", + Method: http.MethodPost, + Url: "/api/collections/demo3/records", + Body: nil, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "guest submit in public collection", + Method: http.MethodPost, + Url: "/api/collections/demo3/records", + Body: strings.NewReader(`{"title":"new"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":`, + `"title":"new"`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeCreateRequest": 1, + "OnRecordAfterCreateRequest": 1, + "OnModelBeforeCreate": 1, + "OnModelAfterCreate": 1, + }, + }, + { + Name: "user submit in restricted collection (rule failure check)", + Method: http.MethodPost, + Url: "/api/collections/demo2/records", + Body: strings.NewReader(`{ + "rel_cascade": "577bd676-aacb-4072-b7da-99d00ee210a4", + "onerel": "577bd676-aacb-4072-b7da-99d00ee210a4", + "manyrels": ["577bd676-aacb-4072-b7da-99d00ee210a4"], + "text": "test123", + "bool": "false" + }`), + RequestHeaders: map[string]string{ + // test@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "user submit in restricted collection (rule pass check)", + Method: http.MethodPost, + Url: "/api/collections/demo2/records", + Body: strings.NewReader(`{ + "rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4", + "onerel":"577bd676-aacb-4072-b7da-99d00ee210a4", + "manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"], + "text":"test123", + "bool":true + }`), + RequestHeaders: map[string]string{ + // test3@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":`, + `"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + `"onerel":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + `"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"]`, + `"text":"test123"`, + `"bool":true`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeCreateRequest": 1, + "OnRecordAfterCreateRequest": 1, + "OnModelBeforeCreate": 1, + "OnModelAfterCreate": 1, + }, + }, + { + Name: "admin submit in restricted collection (rule skip check)", + Method: http.MethodPost, + Url: "/api/collections/demo2/records", + Body: strings.NewReader(`{ + "rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4", + "onerel":"577bd676-aacb-4072-b7da-99d00ee210a4", + "manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"], + "text":"test123", + "bool":false + }`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":`, + `"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + `"onerel":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + `"manyrels":["577bd676-aacb-4072-b7da-99d00ee210a4"]`, + `"text":"test123"`, + `"bool":false`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeCreateRequest": 1, + "OnRecordAfterCreateRequest": 1, + "OnModelBeforeCreate": 1, + "OnModelAfterCreate": 1, + }, + }, + { + Name: "submit via multipart form data", + Method: http.MethodPost, + Url: "/api/collections/demo/records", + Body: formData, + RequestHeaders: map[string]string{ + "Content-Type": mp.FormDataContentType(), + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"`, + `"title":"new"`, + `"file":"`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeCreateRequest": 1, + "OnRecordAfterCreateRequest": 1, + "OnModelBeforeCreate": 1, + "OnModelAfterCreate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestRecordUpdate(t *testing.T) { + formData, mp, err := tests.MockMultipartData(map[string]string{ + "title": "new", + }, "file") + if err != nil { + t.Fatal(err) + } + + scenarios := []tests.ApiScenario{ + { + Name: "missing collection", + Method: http.MethodPatch, + Url: "/api/collections/missing/records/2c542824-9de1-42fe-8924-e57c86267760", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing record", + Method: http.MethodPatch, + Url: "/api/collections/demo3/records/00000000-9de1-42fe-8924-e57c86267760", + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "guest trying to edit nil-rule collection record", + Method: http.MethodPatch, + Url: "/api/collections/demo/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "user trying to edit nil-rule collection record", + Method: http.MethodPatch, + Url: "/api/collections/demo/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "submit invalid format", + Method: http.MethodPatch, + Url: "/api/collections/demo3/records/2c542824-9de1-42fe-8924-e57c86267760", + Body: strings.NewReader(`{"`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "submit nil body", + Method: http.MethodPatch, + Url: "/api/collections/demo3/records/2c542824-9de1-42fe-8924-e57c86267760", + Body: nil, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "guest submit in public collection", + Method: http.MethodPatch, + Url: "/api/collections/demo3/records/2c542824-9de1-42fe-8924-e57c86267760", + Body: strings.NewReader(`{"title":"new"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"2c542824-9de1-42fe-8924-e57c86267760"`, + `"title":"new"`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeUpdateRequest": 1, + "OnRecordAfterUpdateRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + }, + }, + { + Name: "user submit in restricted collection (rule failure check)", + Method: http.MethodPatch, + Url: "/api/collections/demo2/records/94568ca2-0bee-49d7-b749-06cb97956fd9", + Body: strings.NewReader(`{"text": "test_new"}`), + RequestHeaders: map[string]string{ + // test@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "user submit in restricted collection (rule pass check)", + Method: http.MethodPatch, + Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f", + Body: strings.NewReader(`{ + "text":"test_new", + "bool":false + }`), + RequestHeaders: map[string]string{ + // test3@example.com + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImVtYWlsIjoidGVzdDNAZXhhbXBsZS5jb20iLCJpZCI6Ijk3Y2MzZDNkLTZiYTItMzgzZi1iNDJhLTdiYzg0ZDI3NDEwYyIsImV4cCI6MTg5MzUxNTU3Nn0.Q965uvlTxxOsZbACXSgJQNXykYK0TKZ87nyPzemvN4E", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`, + `"rel_cascade":"577bd676-aacb-4072-b7da-99d00ee210a4"`, + `"onerel":"848a1dea-5ddd-42d6-a00d-030547bffcfe"`, + `"manyrels":["848a1dea-5ddd-42d6-a00d-030547bffcfe","577bd676-aacb-4072-b7da-99d00ee210a4"]`, + `"bool":false`, + `"text":"test_new"`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeUpdateRequest": 1, + "OnRecordAfterUpdateRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + }, + }, + { + Name: "admin submit in restricted collection (rule skip check)", + Method: http.MethodPatch, + Url: "/api/collections/demo2/records/63c2ab80-84ab-4057-a592-4604a731f78f", + Body: strings.NewReader(`{ + "text":"test_new" + }`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"63c2ab80-84ab-4057-a592-4604a731f78f"`, + `"text":"test_new"`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeUpdateRequest": 1, + "OnRecordAfterUpdateRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + }, + }, + { + Name: "submit via multipart form data", + Method: http.MethodPatch, + Url: "/api/collections/demo/records/b5c2ffc2-bafd-48f7-b8b7-090638afe209", + Body: formData, + RequestHeaders: map[string]string{ + "Content-Type": mp.FormDataContentType(), + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"b5c2ffc2-bafd-48f7-b8b7-090638afe209"`, + `"title":"new"`, + `"file":"`, + }, + ExpectedEvents: map[string]int{ + "OnRecordBeforeUpdateRequest": 1, + "OnRecordAfterUpdateRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/settings.go b/apis/settings.go new file mode 100644 index 00000000..6cfd3155 --- /dev/null +++ b/apis/settings.go @@ -0,0 +1,71 @@ +package apis + +import ( + "net/http" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tools/rest" +) + +// BindSettingsApi registers the settings api endpoints. +func BindSettingsApi(app core.App, rg *echo.Group) { + api := settingsApi{app: app} + + subGroup := rg.Group("/settings", ActivityLogger(app), RequireAdminAuth()) + subGroup.GET("", api.list) + subGroup.PATCH("", api.set) +} + +type settingsApi struct { + app core.App +} + +func (api *settingsApi) list(c echo.Context) error { + settings, err := api.app.Settings().RedactClone() + if err != nil { + return rest.NewBadRequestError("", err) + } + + event := &core.SettingsListEvent{ + HttpContext: c, + RedactedSettings: settings, + } + + return api.app.OnSettingsListRequest().Trigger(event, func(e *core.SettingsListEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.RedactedSettings) + }) +} + +func (api *settingsApi) set(c echo.Context) error { + form := forms.NewSettingsUpsert(api.app) + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", err) + } + + event := &core.SettingsUpdateEvent{ + HttpContext: c, + OldSettings: api.app.Settings(), + NewSettings: form.Settings, + } + + handlerErr := api.app.OnSettingsBeforeUpdateRequest().Trigger(event, func(e *core.SettingsUpdateEvent) error { + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("An error occured while submitting the form.", err) + } + + redactedSettings, err := api.app.Settings().RedactClone() + if err != nil { + return rest.NewBadRequestError("", err) + } + + return e.HttpContext.JSON(http.StatusOK, redactedSettings) + }) + + if handlerErr == nil { + api.app.OnSettingsAfterUpdateRequest().Trigger(event) + } + + return handlerErr +} diff --git a/apis/settings_test.go b/apis/settings_test.go new file mode 100644 index 00000000..afd6e65d --- /dev/null +++ b/apis/settings_test.go @@ -0,0 +1,188 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/tests" +) + +func TestSettingsList(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/settings", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodGet, + Url: "/api/settings", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin", + Method: http.MethodGet, + Url: "/api/settings", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"meta":{`, + `"logs":{`, + `"smtp":{`, + `"s3":{`, + `"adminAuthToken":{`, + `"adminPasswordResetToken":{`, + `"userAuthToken":{`, + `"userPasswordResetToken":{`, + `"userEmailChangeToken":{`, + `"userVerificationToken":{`, + `"emailAuth":{`, + `"googleAuth":{`, + `"facebookAuth":{`, + `"githubAuth":{`, + `"gitlabAuth":{`, + `"secret":"******"`, + `"clientSecret":"******"`, + }, + ExpectedEvents: map[string]int{ + "OnSettingsListRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestSettingsSet(t *testing.T) { + validData := `{"meta":{"appName":"update_test"},"emailAuth":{"minPasswordLength": 12}}` + + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPatch, + Url: "/api/settings", + Body: strings.NewReader(validData), + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user", + Method: http.MethodPatch, + Url: "/api/settings", + Body: strings.NewReader(validData), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin submitting empty data", + Method: http.MethodPatch, + Url: "/api/settings", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"meta":{`, + `"logs":{`, + `"smtp":{`, + `"s3":{`, + `"adminAuthToken":{`, + `"adminPasswordResetToken":{`, + `"userAuthToken":{`, + `"userPasswordResetToken":{`, + `"userEmailChangeToken":{`, + `"userVerificationToken":{`, + `"emailAuth":{`, + `"googleAuth":{`, + `"facebookAuth":{`, + `"githubAuth":{`, + `"gitlabAuth":{`, + `"secret":"******"`, + `"clientSecret":"******"`, + `"appName":"Acme"`, + `"minPasswordLength":8`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnSettingsBeforeUpdateRequest": 1, + "OnSettingsAfterUpdateRequest": 1, + }, + }, + { + Name: "authorized as admin submitting invalid data", + Method: http.MethodPatch, + Url: "/api/settings", + Body: strings.NewReader(`{"meta":{"appName":""},"emailAuth":{"minPasswordLength": 3}}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"emailAuth":{"minPasswordLength":{"code":"validation_min_greater_equal_than_required","message":"Must be no less than 5."}}`, + `"meta":{"appName":{"code":"validation_required","message":"Cannot be blank."}}`, + }, + ExpectedEvents: map[string]int{ + "OnSettingsBeforeUpdateRequest": 1, + }, + }, + { + Name: "authorized as admin submitting valid data", + Method: http.MethodPatch, + Url: "/api/settings", + Body: strings.NewReader(validData), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"meta":{`, + `"logs":{`, + `"smtp":{`, + `"s3":{`, + `"adminAuthToken":{`, + `"adminPasswordResetToken":{`, + `"userAuthToken":{`, + `"userPasswordResetToken":{`, + `"userEmailChangeToken":{`, + `"userVerificationToken":{`, + `"emailAuth":{`, + `"googleAuth":{`, + `"facebookAuth":{`, + `"githubAuth":{`, + `"gitlabAuth":{`, + `"secret":"******"`, + `"clientSecret":"******"`, + `"appName":"update_test"`, + `"minPasswordLength":12`, + }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnSettingsBeforeUpdateRequest": 1, + "OnSettingsAfterUpdateRequest": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/apis/user.go b/apis/user.go new file mode 100644 index 00000000..ea8f9347 --- /dev/null +++ b/apis/user.go @@ -0,0 +1,444 @@ +package apis + +import ( + "log" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tokens" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/routine" + "github.com/pocketbase/pocketbase/tools/search" + "github.com/pocketbase/pocketbase/tools/security" + "golang.org/x/oauth2" +) + +// BindUserApi registers the user api endpoints and the corresponding handlers. +func BindUserApi(app core.App, rg *echo.Group) { + api := userApi{app: app} + + subGroup := rg.Group("/users", ActivityLogger(app)) + subGroup.GET("/auth-methods", api.authMethods) + subGroup.POST("/auth-via-oauth2", api.oauth2Auth, RequireGuestOnly()) + subGroup.POST("/auth-via-email", api.emailAuth, RequireGuestOnly()) + subGroup.POST("/request-password-reset", api.requestPasswordReset) + subGroup.POST("/confirm-password-reset", api.confirmPasswordReset) + subGroup.POST("/request-verification", api.requestVerification) + subGroup.POST("/confirm-verification", api.confirmVerification) + subGroup.POST("/request-email-change", api.requestEmailChange, RequireUserAuth()) + subGroup.POST("/confirm-email-change", api.confirmEmailChange) + subGroup.POST("/refresh", api.refresh, RequireUserAuth()) + // crud + subGroup.GET("", api.list, RequireAdminAuth()) + subGroup.POST("", api.create) + subGroup.GET("/:id", api.view, RequireAdminOrOwnerAuth("id")) + subGroup.PATCH("/:id", api.update, RequireAdminAuth()) + subGroup.DELETE("/:id", api.delete, RequireAdminOrOwnerAuth("id")) +} + +type userApi struct { + app core.App +} + +func (api *userApi) authResponse(c echo.Context, user *models.User, meta any) error { + token, tokenErr := tokens.NewUserAuthToken(api.app, user) + if tokenErr != nil { + return rest.NewBadRequestError("Failed to create auth token.", tokenErr) + } + + event := &core.UserAuthEvent{ + HttpContext: c, + User: user, + Token: token, + Meta: meta, + } + + return api.app.OnUserAuthRequest().Trigger(event, func(e *core.UserAuthEvent) error { + result := map[string]any{ + "token": e.Token, + "user": e.User, + } + + if e.Meta != nil { + result["meta"] = e.Meta + } + + return e.HttpContext.JSON(http.StatusOK, result) + }) +} + +func (api *userApi) refresh(c echo.Context) error { + user, _ := c.Get(ContextUserKey).(*models.User) + if user == nil { + return rest.NewNotFoundError("Missing auth user context.", nil) + } + + return api.authResponse(c, user, nil) +} + +type providerInfo struct { + Name string `json:"name"` + State string `json:"state"` + CodeVerifier string `json:"codeVerifier"` + CodeChallenge string `json:"codeChallenge"` + CodeChallengeMethod string `json:"codeChallengeMethod"` + AuthUrl string `json:"authUrl"` +} + +func (api *userApi) authMethods(c echo.Context) error { + result := struct { + EmailPassword bool `json:"emailPassword"` + AuthProviders []providerInfo `json:"authProviders"` + }{ + EmailPassword: true, + AuthProviders: []providerInfo{}, + } + + settings := api.app.Settings() + + result.EmailPassword = settings.EmailAuth.Enabled + + nameConfigMap := settings.NamedAuthProviderConfigs() + + for name, config := range nameConfigMap { + if !config.Enabled { + continue + } + + provider, err := auth.NewProviderByName(name) + if err != nil { + if api.app.IsDebug() { + log.Println(err) + } + + // skip provider + continue + } + + if err := config.SetupProvider(provider); err != nil { + if api.app.IsDebug() { + log.Println(err) + } + + // skip provider + continue + } + + state := security.RandomString(30) + codeVerifier := security.RandomString(30) + codeChallenge := security.S256Challenge(codeVerifier) + codeChallengeMethod := "S256" + result.AuthProviders = append(result.AuthProviders, providerInfo{ + Name: name, + State: state, + CodeVerifier: codeVerifier, + CodeChallenge: codeChallenge, + CodeChallengeMethod: codeChallengeMethod, + AuthUrl: provider.BuildAuthUrl( + state, + oauth2.SetAuthURLParam("code_challenge", codeChallenge), + oauth2.SetAuthURLParam("code_challenge_method", codeChallengeMethod), + ) + "&redirect_uri=", // empty redirect_uri so that users can append their url + }) + } + + return c.JSON(http.StatusOK, result) +} + +func (api *userApi) oauth2Auth(c echo.Context) error { + form := forms.NewUserOauth2Login(api.app) + if readErr := c.Bind(form); readErr != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr) + } + + user, authData, submitErr := form.Submit() + if submitErr != nil { + return rest.NewBadRequestError("Failed to authenticated.", submitErr) + } + + return api.authResponse(c, user, authData) +} + +func (api *userApi) emailAuth(c echo.Context) error { + if !api.app.Settings().EmailAuth.Enabled { + return rest.NewBadRequestError("Email/Password authentication is not enabled.", nil) + } + + form := forms.NewUserEmailLogin(api.app) + if readErr := c.Bind(form); readErr != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr) + } + + user, submitErr := form.Submit() + if submitErr != nil { + return rest.NewBadRequestError("Failed to authenticate.", submitErr) + } + + return api.authResponse(c, user, nil) +} + +func (api *userApi) requestPasswordReset(c echo.Context) error { + form := forms.NewUserPasswordResetRequest(api.app) + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", err) + } + + if err := form.Validate(); err != nil { + return rest.NewBadRequestError("An error occured while validating the form.", err) + } + + // run in background because we don't need to show + // the result to the user (prevents users enumeration) + routine.FireAndForget(func() { + if err := form.Submit(); err != nil && api.app.IsDebug() { + log.Println(err) + } + }) + + return c.NoContent(http.StatusNoContent) +} + +func (api *userApi) confirmPasswordReset(c echo.Context) error { + form := forms.NewUserPasswordResetConfirm(api.app) + if readErr := c.Bind(form); readErr != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr) + } + + user, submitErr := form.Submit() + if submitErr != nil { + return rest.NewBadRequestError("Failed to set new password.", submitErr) + } + + return api.authResponse(c, user, nil) +} + +func (api *userApi) requestEmailChange(c echo.Context) error { + loggedUser, _ := c.Get(ContextUserKey).(*models.User) + if loggedUser == nil { + return rest.NewUnauthorizedError("The request requires valid authorized user.", nil) + } + + form := forms.NewUserEmailChangeRequest(api.app, loggedUser) + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", err) + } + + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to request email change.", err) + } + + return c.NoContent(http.StatusNoContent) +} + +func (api *userApi) confirmEmailChange(c echo.Context) error { + form := forms.NewUserEmailChangeConfirm(api.app) + if readErr := c.Bind(form); readErr != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr) + } + + user, submitErr := form.Submit() + if submitErr != nil { + return rest.NewBadRequestError("Failed to confirm email change.", submitErr) + } + + return api.authResponse(c, user, nil) +} + +func (api *userApi) requestVerification(c echo.Context) error { + form := forms.NewUserVerificationRequest(api.app) + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", err) + } + + if err := form.Validate(); err != nil { + return rest.NewBadRequestError("An error occured while validating the form.", err) + } + + // run in background because we don't need to show + // the result to the user (prevents users enumeration) + routine.FireAndForget(func() { + if err := form.Submit(); err != nil && api.app.IsDebug() { + log.Println(err) + } + }) + + return c.NoContent(http.StatusNoContent) +} + +func (api *userApi) confirmVerification(c echo.Context) error { + form := forms.NewUserVerificationConfirm(api.app) + if readErr := c.Bind(form); readErr != nil { + return rest.NewBadRequestError("An error occured while reading the submitted data.", readErr) + } + + user, submitErr := form.Submit() + if submitErr != nil { + return rest.NewBadRequestError("An error occured while submitting the form.", submitErr) + } + + return api.authResponse(c, user, nil) +} + +// ------------------------------------------------------------------- +// CRUD +// ------------------------------------------------------------------- + +func (api *userApi) list(c echo.Context) error { + fieldResolver := search.NewSimpleFieldResolver( + "id", "created", "updated", "email", "verified", + ) + + users := []*models.User{} + + result, searchErr := search.NewProvider(fieldResolver). + Query(api.app.Dao().UserQuery()). + ParseAndExec(c.QueryString(), &users) + if searchErr != nil { + return rest.NewBadRequestError("", searchErr) + } + + // eager load user profiles (if any) + if err := api.app.Dao().LoadProfiles(users); err != nil { + return rest.NewBadRequestError("", err) + } + + event := &core.UsersListEvent{ + HttpContext: c, + Users: users, + Result: result, + } + + return api.app.OnUsersListRequest().Trigger(event, func(e *core.UsersListEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.Result) + }) +} + +func (api *userApi) view(c echo.Context) error { + id := c.PathParam("id") + if id == "" { + return rest.NewNotFoundError("", nil) + } + + user, err := api.app.Dao().FindUserById(id) + if err != nil || user == nil { + return rest.NewNotFoundError("", err) + } + + event := &core.UserViewEvent{ + HttpContext: c, + User: user, + } + + return api.app.OnUserViewRequest().Trigger(event, func(e *core.UserViewEvent) error { + return e.HttpContext.JSON(http.StatusOK, e.User) + }) +} + +func (api *userApi) create(c echo.Context) error { + if !api.app.Settings().EmailAuth.Enabled { + return rest.NewBadRequestError("Email/Password authentication is not enabled.", nil) + } + + user := &models.User{} + form := forms.NewUserUpsert(api.app, user) + + // load request + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.UserCreateEvent{ + HttpContext: c, + User: user, + } + + handlerErr := api.app.OnUserBeforeCreateRequest().Trigger(event, func(e *core.UserCreateEvent) error { + // create the user + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to create user.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.User) + }) + + if handlerErr == nil { + api.app.OnUserAfterCreateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *userApi) update(c echo.Context) error { + id := c.PathParam("id") + if id == "" { + return rest.NewNotFoundError("", nil) + } + + user, err := api.app.Dao().FindUserById(id) + if err != nil || user == nil { + return rest.NewNotFoundError("", err) + } + + form := forms.NewUserUpsert(api.app, user) + + // load request + if err := c.Bind(form); err != nil { + return rest.NewBadRequestError("Failed to read the submitted data due to invalid formatting.", err) + } + + event := &core.UserUpdateEvent{ + HttpContext: c, + User: user, + } + + handlerErr := api.app.OnUserBeforeUpdateRequest().Trigger(event, func(e *core.UserUpdateEvent) error { + // update the user + if err := form.Submit(); err != nil { + return rest.NewBadRequestError("Failed to update user.", err) + } + + return e.HttpContext.JSON(http.StatusOK, e.User) + }) + + if handlerErr == nil { + api.app.OnUserAfterUpdateRequest().Trigger(event) + } + + return handlerErr +} + +func (api *userApi) delete(c echo.Context) error { + id := c.PathParam("id") + if id == "" { + return rest.NewNotFoundError("", nil) + } + + user, err := api.app.Dao().FindUserById(id) + if err != nil || user == nil { + return rest.NewNotFoundError("", err) + } + + event := &core.UserDeleteEvent{ + HttpContext: c, + User: user, + } + + handlerErr := api.app.OnUserBeforeDeleteRequest().Trigger(event, func(e *core.UserDeleteEvent) error { + // delete the user model + if err := api.app.Dao().DeleteUser(e.User); err != nil { + return rest.NewBadRequestError("Failed to delete user. Make sure that the user is not part of a required relation reference.", err) + } + + return e.HttpContext.NoContent(http.StatusNoContent) + }) + + if handlerErr == nil { + api.app.OnUserAfterDeleteRequest().Trigger(event) + } + + return handlerErr +} diff --git a/apis/user_test.go b/apis/user_test.go new file mode 100644 index 00000000..76dbd62b --- /dev/null +++ b/apis/user_test.go @@ -0,0 +1,900 @@ +package apis_test + +import ( + "net/http" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/tests" +) + +func TestUsersAuthMethods(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Method: http.MethodGet, + Url: "/api/users/auth-methods", + ExpectedStatus: 200, + ExpectedContent: []string{ + `"emailPassword":true`, + `"authProviders":[{`, + `"authProviders":[{`, + `"name":"gitlab"`, + `"state":`, + `"codeVerifier":`, + `"codeChallenge":`, + `"codeChallengeMethod":`, + `"authUrl":`, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserEmailAuth(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "authorized as user", + Method: http.MethodPost, + Url: "/api/users/auth-via-email", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin", + Method: http.MethodPost, + Url: "/api/users/auth-via-email", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "invalid body format", + Method: http.MethodPost, + Url: "/api/users/auth-via-email", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + Url: "/api/users/auth-via-email", + Body: strings.NewReader(`{"email":"","password":""}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"email":{`, + `"password":{`, + }, + }, + { + Name: "disabled email/pass auth with valid data", + Method: http.MethodPost, + Url: "/api/users/auth-via-email", + Body: strings.NewReader(`{"email":"test@example.com","password":"123456"}`), + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + app.Settings().EmailAuth.Enabled = false + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid data", + Method: http.MethodPost, + Url: "/api/users/auth-via-email", + Body: strings.NewReader(`{"email":"test2@example.com","password":"123456"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token"`, + `"user"`, + `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, + `"email":"test2@example.com"`, + `"verified":false`, // unverified user should be able to authenticate + }, + ExpectedEvents: map[string]int{"OnUserAuthRequest": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserRequestPasswordReset(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + Url: "/api/users/request-password-reset", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, + }, + { + Name: "invalid data", + Method: http.MethodPost, + Url: "/api/users/request-password-reset", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "missing user", + Method: http.MethodPost, + Url: "/api/users/request-password-reset", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + ExpectedStatus: 204, + }, + { + Name: "existing user", + Method: http.MethodPost, + Url: "/api/users/request-password-reset", + Body: strings.NewReader(`{"email":"test@example.com"}`), + ExpectedStatus: 204, + // usually this events are fired but since the submit is + // executed in a separate go routine they are fired async + // ExpectedEvents: map[string]int{ + // "OnModelBeforeUpdate": 1, + // "OnModelAfterUpdate": 1, + // "OnMailerBeforeUserResetPasswordSend": 1, + // "OnMailerAfterUserResetPasswordSend": 1, + // }, + }, + { + Name: "existing user (after already sent)", + Method: http.MethodPost, + Url: "/api/users/request-password-reset", + Body: strings.NewReader(`{"email":"test@example.com"}`), + ExpectedStatus: 204, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserConfirmPasswordReset(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + Url: "/api/users/confirm-password-reset", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"password":{"code":"validation_required","message":"Cannot be blank."},"passwordConfirm":{"code":"validation_required","message":"Cannot be blank."},"token":{"code":"validation_required","message":"Cannot be blank."}}`}, + }, + { + Name: "invalid data format", + Method: http.MethodPost, + Url: "/api/users/confirm-password-reset", + Body: strings.NewReader(`{"password`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "expired token", + Method: http.MethodPost, + Url: "/api/users/confirm-password-reset", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiNGQwMTk3Y2MtMmI0YS0zZjgzLWEyNmItZDc3YmM4NDIzZDNjIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNjQxMDMxMjAwfQ.t2lVe0ny9XruQsSFQdXqBi0I85i6vIUAQjFXZY5HPxc","password":"123456789","passwordConfirm":"123456789"}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{`, + `"code":"validation_invalid_token"`, + }, + }, + { + Name: "valid token and data", + Method: http.MethodPost, + Url: "/api/users/confirm-password-reset", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiNGQwMTk3Y2MtMmI0YS0zZjgzLWEyNmItZDc3YmM4NDIzZDNjIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxODYxOTU2MDAwfQ.V1gEbY4caEIF6IhQAJ8KZD4RvOGvTCFuYg1fTRSvhe0","password":"123456789","passwordConfirm":"123456789"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":`, + `"user":`, + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + `"email":"test@example.com"`, + }, + ExpectedEvents: map[string]int{"OnUserAuthRequest": 1, "OnModelAfterUpdate": 1, "OnModelBeforeUpdate": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserRequestVerification(t *testing.T) { + scenarios := []tests.ApiScenario{ + // empty data + { + Method: http.MethodPost, + Url: "/api/users/request-verification", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, + }, + // invalid data + { + Method: http.MethodPost, + Url: "/api/users/request-verification", + Body: strings.NewReader(`{"email`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + // missing user + { + Method: http.MethodPost, + Url: "/api/users/request-verification", + Body: strings.NewReader(`{"email":"missing@example.com"}`), + ExpectedStatus: 204, + }, + // existing already verified user + { + Method: http.MethodPost, + Url: "/api/users/request-verification", + Body: strings.NewReader(`{"email":"test@example.com"}`), + ExpectedStatus: 204, + }, + // existing unverified user + { + Method: http.MethodPost, + Url: "/api/users/request-verification", + Body: strings.NewReader(`{"email":"test2@example.com"}`), + ExpectedStatus: 204, + // usually this events are fired but since the submit is + // executed in a separate go routine they are fired async + // ExpectedEvents: map[string]int{ + // "OnModelBeforeUpdate": 1, + // "OnModelAfterUpdate": 1, + // "OnMailerBeforeUserVerificationSend": 1, + // "OnMailerAfterUserVerificationSend": 1, + // }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserConfirmVerification(t *testing.T) { + scenarios := []tests.ApiScenario{ + // empty data + { + Method: http.MethodPost, + Url: "/api/users/confirm-verification", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":`, + `"token":{"code":"validation_required"`, + }, + }, + // invalid data + { + Method: http.MethodPost, + Url: "/api/users/confirm-verification", + Body: strings.NewReader(`{"token`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + // expired token + { + Method: http.MethodPost, + Url: "/api/users/confirm-verification", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiN2JjODRkMjctNmJhMi1iNDJhLTM4M2YtNDE5N2NjM2QzZDBjIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsImV4cCI6MTY0MTAzMTIwMH0.YCqyREksfqn7cWu-innNNTbWQCr9DgYr7dduM2wxrtQ"}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{`, + `"code":"validation_invalid_token"`, + }, + }, + // valid token + { + Method: http.MethodPost, + Url: "/api/users/confirm-verification", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsImlkIjoiN2JjODRkMjctNmJhMi1iNDJhLTM4M2YtNDE5N2NjM2QzZDBjIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsImV4cCI6MTg2MTk1NjAwMH0.OsxRKuZrNTnwyVjvCwB4jY8TbT-NPZ-UFCpRhCvuv2U"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":`, + `"user":`, + `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, + `"email":"test2@example.com"`, + `"verified":true`, + }, + ExpectedEvents: map[string]int{ + "OnUserAuthRequest": 1, + "OnModelAfterUpdate": 1, + "OnModelBeforeUpdate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserRequestEmailChange(t *testing.T) { + scenarios := []tests.ApiScenario{ + // unauthorized + { + Method: http.MethodPost, + Url: "/api/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + // authorized as admin + { + Method: http.MethodPost, + Url: "/api/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + // invalid data + { + Method: http.MethodPost, + Url: "/api/users/request-email-change", + Body: strings.NewReader(`{"newEmail`), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + // empty data + { + Method: http.MethodPost, + Url: "/api/users/request-email-change", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":`, + `"newEmail":{"code":"validation_required"`, + }, + }, + // valid data (existing email) + { + Method: http.MethodPost, + Url: "/api/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"test2@example.com"}`), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":`, + `"newEmail":{"code":"validation_user_email_exists"`, + }, + }, + // valid data (new email) + { + Method: http.MethodPost, + Url: "/api/users/request-email-change", + Body: strings.NewReader(`{"newEmail":"change@example.com"}`), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnMailerBeforeUserChangeEmailSend": 1, + "OnMailerAfterUserChangeEmailSend": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserConfirmEmailChange(t *testing.T) { + scenarios := []tests.ApiScenario{ + // empty data + { + Method: http.MethodPost, + Url: "/api/users/confirm-email-change", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":`, + `"token":{"code":"validation_required"`, + `"password":{"code":"validation_required"`, + }, + }, + // invalid data + { + Method: http.MethodPost, + Url: "/api/users/confirm-email-change", + Body: strings.NewReader(`{"token`), + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + // expired token and correct password + { + Method: http.MethodPost, + Url: "/api/users/confirm-email-change", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjdiYzg0ZDI3LTZiYTItYjQyYS0zODNmLTQxOTdjYzNkM2QwYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoxNjQwOTkxNjAwfQ.DOqNtSDcXbWix8OsK13X-tjfWi6jZNlAzIZiwG_YDOs","password":"123456"}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"token":{`, + `"code":"validation_invalid_token"`, + }, + }, + // valid token and incorrect password + { + Method: http.MethodPost, + Url: "/api/users/confirm-email-change", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjdiYzg0ZDI3LTZiYTItYjQyYS0zODNmLTQxOTdjYzNkM2QwYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoxODkzNDUyNDAwfQ.aWMQJ_c49yFbzHO5TNhlkbKRokQ_isc2RbLGuSJx44c","password":"654321"}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"password":{`, + `"code":"validation_invalid_password"`, + }, + }, + // valid token and correct password + { + Method: http.MethodPost, + Url: "/api/users/confirm-email-change", + Body: strings.NewReader(`{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjdiYzg0ZDI3LTZiYTItYjQyYS0zODNmLTQxOTdjYzNkM2QwYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsIm5ld0VtYWlsIjoiY2hhbmdlQGV4YW1wbGUuY29tIiwiZXhwIjoxODkzNDUyNDAwfQ.aWMQJ_c49yFbzHO5TNhlkbKRokQ_isc2RbLGuSJx44c","password":"123456"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":`, + `"user":`, + `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, + `"email":"change@example.com"`, + `"verified":true`, + }, + ExpectedEvents: map[string]int{"OnUserAuthRequest": 1, "OnModelAfterUpdate": 1, "OnModelBeforeUpdate": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserRefresh(t *testing.T) { + scenarios := []tests.ApiScenario{ + // unauthorized + { + Method: http.MethodPost, + Url: "/api/users/refresh", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + // authorized as admin + { + Method: http.MethodPost, + Url: "/api/users/refresh", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + // authorized as user + { + Method: http.MethodPost, + Url: "/api/users/refresh", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"token":`, + `"user":`, + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + }, + ExpectedEvents: map[string]int{"OnUserAuthRequest": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUsersList(t *testing.T) { + scenarios := []tests.ApiScenario{ + // unauthorized + { + Method: http.MethodGet, + Url: "/api/users", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + // authorized as user + { + Method: http.MethodGet, + Url: "/api/users", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + // authorized as admin + { + Method: http.MethodGet, + Url: "/api/users", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":3`, + `"items":[{`, + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + `"id":"7bc84d27-6ba2-b42a-383f-4197cc3d3d0c"`, + `"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`, + }, + ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, + }, + // authorized as admin + paging and sorting + { + Method: http.MethodGet, + Url: "/api/users?page=2&perPage=2&sort=-created", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":2`, + `"perPage":2`, + `"totalItems":3`, + `"items":[{`, + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + }, + ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, + }, + // authorized as admin + invalid filter + { + Method: http.MethodGet, + Url: "/api/users?filter=invalidfield~'test2'", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + // authorized as admin + valid filter + { + Method: http.MethodGet, + Url: "/api/users?filter=verified=true", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"page":1`, + `"perPage":30`, + `"totalItems":2`, + `"items":[{`, + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + `"id":"97cc3d3d-6ba2-383f-b42a-7bc84d27410c"`, + }, + ExpectedEvents: map[string]int{"OnUsersListRequest": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserView(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodGet, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + nonexisting user id", + Method: http.MethodGet, + Url: "/api/users/00000000-0000-0000-0000-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + existing user id", + Method: http.MethodGet, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + }, + ExpectedEvents: map[string]int{"OnUserViewRequest": 1}, + }, + { + Name: "authorized as user - trying to view another user", + Method: http.MethodGet, + Url: "/api/users/7bc84d27-6ba2-b42a-383f-4197cc3d3d0c", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user - owner", + Method: http.MethodGet, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + }, + ExpectedEvents: map[string]int{"OnUserViewRequest": 1}, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserDelete(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodDelete, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + nonexisting user id", + Method: http.MethodDelete, + Url: "/api/users/00000000-0000-0000-0000-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin + existing user id", + Method: http.MethodDelete, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnUserBeforeDeleteRequest": 1, + "OnUserAfterDeleteRequest": 1, + "OnModelBeforeDelete": 2, // cascade delete to related Record model + "OnModelAfterDelete": 2, // cascade delete to related Record model + }, + }, + { + Name: "authorized as user - trying to delete another user", + Method: http.MethodDelete, + Url: "/api/users/7bc84d27-6ba2-b42a-383f-4197cc3d3d0c", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 403, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user - owner", + Method: http.MethodDelete, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 204, + ExpectedEvents: map[string]int{ + "OnUserBeforeDeleteRequest": 1, + "OnUserAfterDeleteRequest": 1, + "OnModelBeforeDelete": 2, // cascade delete to related Record model + "OnModelAfterDelete": 2, // cascade delete to related Record model + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserCreate(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "empty data", + Method: http.MethodPost, + Url: "/api/users", + Body: strings.NewReader(``), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"email":{"code":"validation_required"`, + `"password":{"code":"validation_required"`, + }, + ExpectedEvents: map[string]int{ + "OnUserBeforeCreateRequest": 1, + }, + }, + { + Name: "invalid data", + Method: http.MethodPost, + Url: "/api/users", + Body: strings.NewReader(`{"email":"test@example.com","password":"1234","passwordConfirm":"4321"}`), + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"email":{"code":"validation_user_email_exists"`, + `"password":{"code":"validation_length_out_of_range"`, + `"passwordConfirm":{"code":"validation_values_mismatch"`, + }, + ExpectedEvents: map[string]int{ + "OnUserBeforeCreateRequest": 1, + }, + }, + { + Name: "valid data but with disabled email/pass auth", + Method: http.MethodPost, + Url: "/api/users", + Body: strings.NewReader(`{"email":"newuser@example.com","password":"123456789","passwordConfirm":"123456789"}`), + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + app.Settings().EmailAuth.Enabled = false + }, + ExpectedStatus: 400, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "valid data", + Method: http.MethodPost, + Url: "/api/users", + Body: strings.NewReader(`{"email":"newuser@example.com","password":"123456789","passwordConfirm":"123456789"}`), + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":`, + `"email":"newuser@example.com"`, + }, + ExpectedEvents: map[string]int{ + "OnUserBeforeCreateRequest": 1, + "OnUserAfterCreateRequest": 1, + "OnModelBeforeCreate": 2, // +1 for the created profile record + "OnModelAfterCreate": 2, // +1 for the created profile record + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + +func TestUserUpdate(t *testing.T) { + scenarios := []tests.ApiScenario{ + { + Name: "unauthorized", + Method: http.MethodPatch, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + Body: strings.NewReader(`{"email":"new@example.com"}`), + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as user (owner)", + Method: http.MethodPatch, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + Body: strings.NewReader(`{"email":"new@example.com"}`), + RequestHeaders: map[string]string{ + "Authorization": "User eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + }, + ExpectedStatus: 401, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin - invalid/missing user id", + Method: http.MethodPatch, + Url: "/api/users/invalid", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 404, + ExpectedContent: []string{`"data":{}`}, + }, + { + Name: "authorized as admin - empty data", + Method: http.MethodPatch, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + Body: strings.NewReader(``), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + `"email":"test@example.com"`, + }, + ExpectedEvents: map[string]int{ + "OnUserBeforeUpdateRequest": 1, + "OnUserAfterUpdateRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + }, + }, + { + Name: "authorized as admin - invalid data", + Method: http.MethodPatch, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + Body: strings.NewReader(`{"email":"test2@example.com"}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 400, + ExpectedContent: []string{ + `"data":{`, + `"email":{"code":"validation_user_email_exists"`, + }, + ExpectedEvents: map[string]int{ + "OnUserBeforeUpdateRequest": 1, + }, + }, + { + Name: "authorized as admin - valid data", + Method: http.MethodPatch, + Url: "/api/users/4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + Body: strings.NewReader(`{"email":"new@example.com"}`), + RequestHeaders: map[string]string{ + "Authorization": "Admin eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + }, + ExpectedStatus: 200, + ExpectedContent: []string{ + `"id":"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`, + `"email":"new@example.com"`, + }, + ExpectedEvents: map[string]int{ + "OnUserBeforeUpdateRequest": 1, + "OnUserAfterUpdateRequest": 1, + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} diff --git a/cmd/migrate.go b/cmd/migrate.go new file mode 100644 index 00000000..dd35d453 --- /dev/null +++ b/cmd/migrate.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "log" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/migrations/logs" + "github.com/pocketbase/pocketbase/tools/migrate" + "github.com/spf13/cobra" +) + +// NewMigrateCommand creates and returns new command for handling DB migrations. +func NewMigrateCommand(app core.App) *cobra.Command { + desc := ` +Supported arguments are: +- up - runs all available migrations. +- down [number] - reverts the last [number] applied migrations. +- create folder name - creates new migration template file. +` + var databaseFlag string + + command := &cobra.Command{ + Use: "migrate", + Short: "Executes DB migration scripts", + ValidArgs: []string{"up", "down", "create"}, + Long: desc, + Run: func(command *cobra.Command, args []string) { + // normalize + if databaseFlag != "logs" { + databaseFlag = "db" + } + + connections := migrationsConnectionsMap(app) + + runner, err := migrate.NewRunner( + connections[databaseFlag].DB, + connections[databaseFlag].MigrationsList, + ) + if err != nil { + log.Fatal(err) + } + + if err := runner.Run(args...); err != nil { + log.Fatal(err) + } + }, + } + + command.PersistentFlags().StringVar( + &databaseFlag, + "database", + "db", + "specify the database connection to use (db or logs)", + ) + + return command +} + +type migrationsConnection struct { + DB *dbx.DB + MigrationsList migrate.MigrationsList +} + +func migrationsConnectionsMap(app core.App) map[string]migrationsConnection { + return map[string]migrationsConnection{ + "db": { + DB: app.DB(), + MigrationsList: migrations.AppMigrations, + }, + "logs": { + DB: app.LogsDB(), + MigrationsList: logs.LogsMigrations, + }, + } +} diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 00000000..34699b72 --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,228 @@ +package cmd + +import ( + "crypto/tls" + "errors" + "fmt" + "log" + "net" + "net/http" + "path/filepath" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/fatih/color" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/labstack/echo/v5/middleware" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/migrate" + "github.com/spf13/cobra" + "golang.org/x/crypto/acme" + "golang.org/x/crypto/acme/autocert" +) + +// NewServeCommand creates and returns new command responsible for +// starting the default PocketBase web server. +func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { + var allowedOrigins []string + var httpAddr string + var httpsAddr string + + command := &cobra.Command{ + Use: "serve", + Short: "Starts the web server (default to localhost:8090)", + Run: func(command *cobra.Command, args []string) { + router, err := apis.InitApi(app) + if err != nil { + panic(err) + } + + // configure cors + router.Use(middleware.CORSWithConfig(middleware.CORSConfig(middleware.CORSConfig{ + Skipper: middleware.DefaultSkipper, + AllowOrigins: allowedOrigins, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, + }))) + + // ensure that the latest migrations are applied before starting the server + if err := runMigrations(app); err != nil { + panic(err) + } + + // reload app settings in case a new default value was set with a migration + // (or if this is the first time the init migration was executed) + if err := app.RefreshSettings(); err != nil { + color.Yellow("=====================================") + color.Yellow("WARNING - Settings load error! \n%v", err) + color.Yellow("Fallback to the application defaults.") + color.Yellow("=====================================") + } + + // if no admins are found, create the first one + totalAdmins, err := app.Dao().TotalAdmins() + if err != nil { + log.Fatalln(err) + return + } + if totalAdmins == 0 { + if err := promptCreateAdmin(app); err != nil { + log.Fatalln(err) + return + } + } + + // start http server + // --- + mainAddr := httpAddr + if httpsAddr != "" { + mainAddr = httpsAddr + } + + mainHost, _, _ := net.SplitHostPort(mainAddr) + + certManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(filepath.Join(app.DataDir(), ".autocert_cache")), + HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost), + } + + serverConfig := &http.Server{ + TLSConfig: &tls.Config{ + GetCertificate: certManager.GetCertificate, + NextProtos: []string{acme.ALPNProto}, + }, + ReadTimeout: 60 * time.Second, + // WriteTimeout: 60 * time.Second, // breaks sse! + Handler: router, + Addr: mainAddr, + } + + if showStartBanner { + schema := "http" + if httpsAddr != "" { + schema = "https" + } + bold := color.New(color.Bold).Add(color.FgGreen) + bold.Printf("> Server started at: %s\n", color.CyanString("%s://%s", schema, serverConfig.Addr)) + fmt.Printf(" - REST API: %s\n", color.CyanString("%s://%s/api/", schema, serverConfig.Addr)) + fmt.Printf(" - Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr)) + } + + var serveErr error + if httpsAddr != "" { + // if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version + if httpAddr != "" { + go http.ListenAndServe(httpAddr, certManager.HTTPHandler(nil)) + } + + // start HTTPS server + serveErr = serverConfig.ListenAndServeTLS("", "") + } else { + // start HTTP server + serveErr = serverConfig.ListenAndServe() + } + + if serveErr != http.ErrServerClosed { + log.Fatalln(serveErr) + } + }, + } + + command.PersistentFlags().StringSliceVar( + &allowedOrigins, + "origins", + []string{"*"}, + "CORS allowed domain origins list", + ) + + command.PersistentFlags().StringVar( + &httpAddr, + "http", + "localhost:8090", + "api HTTP server address", + ) + + command.PersistentFlags().StringVar( + &httpsAddr, + "https", + "", + "api HTTPS server address (auto TLS via Let's Encrypt)\nthe incomming --http address traffic also will be redirected to this address", + ) + + return command +} + +func runMigrations(app core.App) error { + connections := migrationsConnectionsMap(app) + + for _, c := range connections { + runner, err := migrate.NewRunner(c.DB, c.MigrationsList) + if err != nil { + return err + } + + if _, err := runner.Up(); err != nil { + return err + } + } + + return nil +} + +func promptCreateAdmin(app core.App) error { + color.White("-------------------------------------") + color.Cyan("Lets create your first admin account:") + color.White("-------------------------------------") + + prompts := []*survey.Question{ + { + Name: "Email", + Prompt: &survey.Input{Message: "Email:"}, + Validate: func(val any) error { + if err := survey.Required(val); err != nil { + return err + } + if err := is.Email.Validate(val); err != nil { + return err + } + + return nil + }, + }, + { + Name: "Password", + Prompt: &survey.Password{Message: "Pass (min 10 chars):"}, + Validate: func(val any) error { + if str, ok := val.(string); !ok || len(str) < 10 { + return errors.New("The password must be at least 10 characters.") + } + return nil + }, + }, + } + + result := struct { + Email string + Password string + }{} + if err := survey.Ask(prompts, &result); err != nil { + return err + } + + form := forms.NewAdminUpsert(app, &models.Admin{}) + form.Email = result.Email + form.Password = result.Password + form.PasswordConfirm = result.Password + + if err := form.Submit(); err != nil { + return err + } + + color.Green("Successfully created admin %s!", result.Email) + fmt.Println("") + + return nil +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 00000000..988179e4 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,21 @@ +// Package cmd implements various PocketBase system commands. +package cmd + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/core" + "github.com/spf13/cobra" +) + +// NewVersionCommand creates and returns new command that prints +// the current PocketBase version. +func NewVersionCommand(app core.App, version string) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Prints the current PocketBase app version", + Run: func(command *cobra.Command, args []string) { + fmt.Printf("PocketBase v%s\n", version) + }, + } +} diff --git a/core/app.go b/core/app.go new file mode 100644 index 00000000..74990c2b --- /dev/null +++ b/core/app.go @@ -0,0 +1,424 @@ +// Package core is the backbone of PocketBase. +// +// It defines the main PocketBase App interface and its base implementation. +package core + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/mailer" + "github.com/pocketbase/pocketbase/tools/store" + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +// App defines the main PocketBase app interface. +type App interface { + // DB returns the default app database instance. + DB() *dbx.DB + + // Dao returns the default app Dao instance. + // + // This Dao could operate only on the tables and models + // associated with the default app database. For example, + // trying to access the request logs table will result in error. + Dao() *daos.Dao + + // LogsDB returns the app logs database instance. + LogsDB() *dbx.DB + + // LogsDao returns the app logs Dao instance. + // + // This Dao could operate only on the tables and models + // associated with the logs database. For example, trying to access + // the users table from LogsDao will result in error. + LogsDao() *daos.Dao + + // DataDir returns the app data directory path. + DataDir() string + + // EncryptionEnv returns the name of the app secret env key + // (used for settings encryption). + EncryptionEnv() string + + // IsDebug returns whether the app is in debug mode + // (showing more detailed error logs, executed sql statements, etc.). + IsDebug() bool + + // Settings returns the loaded app settings. + Settings() *Settings + + // Cache returns the app internal cache store. + Cache() *store.Store[any] + + // SubscriptionsBroker returns the app realtime subscriptions broker instance. + SubscriptionsBroker() *subscriptions.Broker + + // NewMailClient creates and returns a configured app mail client. + NewMailClient() mailer.Mailer + + // NewFilesystem creates and returns a configured filesystem.System instance. + // + // NB! Make sure to call `Close()` on the returned result + // after you are done working with it. + NewFilesystem() (*filesystem.System, error) + + // RefreshSettings reinitializes and reloads the stored application settings. + RefreshSettings() error + + // Bootstrap takes care for initializing the application + // (open db connections, load settings, etc.) + Bootstrap() error + + // ResetBootstrapState takes care for releasing initialized app resources + // (eg. closing db connections). + ResetBootstrapState() error + + // --------------------------------------------------------------- + // App event hooks + // --------------------------------------------------------------- + + // OnBeforeServe hook is triggered before serving the internal router (echo), + // allowing you to adjust its options and attach new routes. + OnBeforeServe() *hook.Hook[*ServeEvent] + + // --------------------------------------------------------------- + // Dao event hooks + // --------------------------------------------------------------- + + // OnModelBeforeCreate hook is triggered before inserting a new + // entry in the DB, allowing you to modify or validate the stored data. + OnModelBeforeCreate() *hook.Hook[*ModelEvent] + + // OnModelAfterCreate hook is triggered after successfuly + // inserting a new entry in the DB. + OnModelAfterCreate() *hook.Hook[*ModelEvent] + + // OnModelBeforeUpdate hook is triggered before updating existing + // entry in the DB, allowing you to modify or validate the stored data. + OnModelBeforeUpdate() *hook.Hook[*ModelEvent] + + // OnModelAfterUpdate hook is triggered after successfuly updating + // existing entry in the DB. + OnModelAfterUpdate() *hook.Hook[*ModelEvent] + + // OnModelBeforeDelete hook is triggered before deleting an + // existing entry from the DB. + OnModelBeforeDelete() *hook.Hook[*ModelEvent] + + // OnModelAfterDelete is triggered after successfuly deleting an + // existing entry from the DB. + OnModelAfterDelete() *hook.Hook[*ModelEvent] + + // --------------------------------------------------------------- + // Mailer event hooks + // --------------------------------------------------------------- + + // OnMailerBeforeAdminResetPasswordSend hook is triggered right before + // sending a password reset email to an admin. + // + // Could be used to send your own custom email template if + // hook.StopPropagation is returned in one of its listeners. + OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] + + // OnMailerAfterAdminResetPasswordSend hook is triggered after + // admin password reset email was successfuly sent. + OnMailerAfterAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] + + // OnMailerBeforeUserResetPasswordSend hook is triggered right before + // sending a password reset email to a user. + // + // Could be used to send your own custom email template if + // hook.StopPropagation is returned in one of its listeners. + OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent] + + // OnMailerAfterUserResetPasswordSend hook is triggered after + // a user password reset email was successfuly sent. + OnMailerAfterUserResetPasswordSend() *hook.Hook[*MailerUserEvent] + + // OnMailerBeforeUserVerificationSend hook is triggered right before + // sending a verification email to a user. + // + // Could be used to send your own custom email template if + // hook.StopPropagation is returned in one of its listeners. + OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent] + + // OnMailerAfterUserVerificationSend hook is triggered after a user + // verification email was successfuly sent. + OnMailerAfterUserVerificationSend() *hook.Hook[*MailerUserEvent] + + // OnMailerBeforeUserChangeEmailSend hook is triggered right before + // sending a confirmation new address email to a a user. + // + // Could be used to send your own custom email template if + // hook.StopPropagation is returned in one of its listeners. + OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent] + + // OnMailerAfterUserChangeEmailSend hook is triggered after a user + // change address email was successfuly sent. + OnMailerAfterUserChangeEmailSend() *hook.Hook[*MailerUserEvent] + + // --------------------------------------------------------------- + // Realtime API event hooks + // --------------------------------------------------------------- + + // OnRealtimeConnectRequest hook is triggered right before establishing + // the SSE client connection. + OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectEvent] + + // OnRealtimeBeforeSubscribeRequest hook is triggered before changing + // the client subscriptions, allowing you to further validate and + // modify the submitted change. + OnRealtimeBeforeSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] + + // OnRealtimeAfterSubscribeRequest hook is triggered after the client + // subscriptions were successfully changed. + OnRealtimeAfterSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] + + // --------------------------------------------------------------- + // Settings API event hooks + // --------------------------------------------------------------- + + // OnSettingsListRequest hook is triggered on each successfull + // API Settings list request. + // + // Could be used to validate or modify the response before + // returning it to the client. + OnSettingsListRequest() *hook.Hook[*SettingsListEvent] + + // OnSettingsBeforeUpdateRequest hook is triggered before each API + // Settings update request (after request data load and before settings persistence). + // + // Could be used to additionally validate the request data or + // implement completely different persistence behavior + // (returning hook.StopPropagation). + OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent] + + // OnSettingsAfterUpdateRequest hook is triggered after each + // successful API Settings update request. + OnSettingsAfterUpdateRequest() *hook.Hook[*SettingsUpdateEvent] + + // --------------------------------------------------------------- + // File API event hooks + // --------------------------------------------------------------- + + // OnFileDownloadRequest hook is triggered before each API File download request. + // + // Could be used to validate or modify the file response before + // returning it to the client. + OnFileDownloadRequest() *hook.Hook[*FileDownloadEvent] + + // --------------------------------------------------------------- + // Admin API event hooks + // --------------------------------------------------------------- + + // OnAdminsListRequest hook is triggered on each API Admins list request. + // + // Could be used to validate or modify the response before returning it to the client. + OnAdminsListRequest() *hook.Hook[*AdminsListEvent] + + // OnAdminViewRequest hook is triggered on each API Admin view request. + // + // Could be used to validate or modify the response before returning it to the client. + OnAdminViewRequest() *hook.Hook[*AdminViewEvent] + + // OnAdminBeforeCreateRequest hook is triggered before each API + // Admin create request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent] + + // OnAdminAfterCreateRequest hook is triggered after each + // successful API Admin create request. + OnAdminAfterCreateRequest() *hook.Hook[*AdminCreateEvent] + + // OnAdminBeforeUpdateRequest hook is triggered before each API + // Admin update request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent] + + // OnAdminAfterUpdateRequest hook is triggered after each + // successful API Admin update request. + OnAdminAfterUpdateRequest() *hook.Hook[*AdminUpdateEvent] + + // OnAdminBeforeDeleteRequest hook is triggered before each API + // Admin delete request (after model load and before actual deletion). + // + // Could be used to additionally validate the request data or implement + // completely different delete behavior (returning hook.StopPropagation). + OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent] + + // OnAdminAfterDeleteRequest hook is triggered after each + // successful API Admin delete request. + OnAdminAfterDeleteRequest() *hook.Hook[*AdminDeleteEvent] + + // OnAdminAuthRequest hook is triggered on each successful API Admin + // authentication request (sign-in, token refresh, etc.). + // + // Could be used to additionally validate or modify the + // authenticated admin data and token. + OnAdminAuthRequest() *hook.Hook[*AdminAuthEvent] + + // --------------------------------------------------------------- + // User API event hooks + // --------------------------------------------------------------- + + // OnUsersListRequest hook is triggered on each API Users list request. + // + // Could be used to validate or modify the response before returning it to the client. + OnUsersListRequest() *hook.Hook[*UsersListEvent] + + // OnUserViewRequest hook is triggered on each API User view request. + // + // Could be used to validate or modify the response before returning it to the client. + OnUserViewRequest() *hook.Hook[*UserViewEvent] + + // OnUserBeforeCreateRequest hook is triggered before each API User + // create request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnUserBeforeCreateRequest() *hook.Hook[*UserCreateEvent] + + // OnUserAfterCreateRequest hook is triggered after each + // successful API User create request. + OnUserAfterCreateRequest() *hook.Hook[*UserCreateEvent] + + // OnUserBeforeUpdateRequest hook is triggered before each API User + // update request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnUserBeforeUpdateRequest() *hook.Hook[*UserUpdateEvent] + + // OnUserAfterUpdateRequest hook is triggered after each + // successful API User update request. + OnUserAfterUpdateRequest() *hook.Hook[*UserUpdateEvent] + + // OnUserBeforeDeleteRequest hook is triggered before each API User + // delete request (after model load and before actual deletion). + // + // Could be used to additionally validate the request data or implement + // completely different delete behavior (returning hook.StopPropagation). + OnUserBeforeDeleteRequest() *hook.Hook[*UserDeleteEvent] + + // OnUserAfterDeleteRequest hook is triggered after each + // successful API User delete request. + OnUserAfterDeleteRequest() *hook.Hook[*UserDeleteEvent] + + // OnUserAuthRequest hook is triggered on each successful API User + // authentication request (sign-in, token refresh, etc.). + // + // Could be used to additionally validate or modify the + // authenticated user data and token. + OnUserAuthRequest() *hook.Hook[*UserAuthEvent] + + // OnUserBeforeOauth2Register hook is triggered before each User OAuth2 + // authentication request (when the client config has enabled new users registration). + // + // Could be used to additionally validate or modify the new user + // before persisting in the DB. + OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] + + // OnUserAfterOauth2Register hook is triggered after each successful User + // OAuth2 authentication sign-up request (right after the new user persistence). + OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] + + // --------------------------------------------------------------- + // Record API event hooks + // --------------------------------------------------------------- + + // OnRecordsListRequest hook is triggered on each API Records list request. + // + // Could be used to validate or modify the response before returning it to the client. + OnRecordsListRequest() *hook.Hook[*RecordsListEvent] + + // OnRecordViewRequest hook is triggered on each API Record view request. + // + // Could be used to validate or modify the response before returning it to the client. + OnRecordViewRequest() *hook.Hook[*RecordViewEvent] + + // OnRecordBeforeCreateRequest hook is triggered before each API Record + // create request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnRecordBeforeCreateRequest() *hook.Hook[*RecordCreateEvent] + + // OnRecordAfterCreateRequest hook is triggered after each + // successful API Record create request. + OnRecordAfterCreateRequest() *hook.Hook[*RecordCreateEvent] + + // OnRecordBeforeUpdateRequest hook is triggered before each API Record + // update request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnRecordBeforeUpdateRequest() *hook.Hook[*RecordUpdateEvent] + + // OnRecordAfterUpdateRequest hook is triggered after each + // successful API Record update request. + OnRecordAfterUpdateRequest() *hook.Hook[*RecordUpdateEvent] + + // OnRecordBeforeDeleteRequest hook is triggered before each API Record + // delete request (after model load and before actual deletion). + // + // Could be used to additionally validate the request data or implement + // completely different delete behavior (returning hook.StopPropagation). + OnRecordBeforeDeleteRequest() *hook.Hook[*RecordDeleteEvent] + + // OnRecordAfterDeleteRequest hook is triggered after each + // successful API Record delete request. + OnRecordAfterDeleteRequest() *hook.Hook[*RecordDeleteEvent] + + // --------------------------------------------------------------- + // Collection API event hooks + // --------------------------------------------------------------- + + // OnCollectionsListRequest hook is triggered on each API Collections list request. + // + // Could be used to validate or modify the response before returning it to the client. + OnCollectionsListRequest() *hook.Hook[*CollectionsListEvent] + + // OnCollectionViewRequest hook is triggered on each API Collection view request. + // + // Could be used to validate or modify the response before returning it to the client. + OnCollectionViewRequest() *hook.Hook[*CollectionViewEvent] + + // OnCollectionBeforeCreateRequest hook is triggered before each API Collection + // create request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent] + + // OnCollectionAfterCreateRequest hook is triggered after each + // successful API Collection create request. + OnCollectionAfterCreateRequest() *hook.Hook[*CollectionCreateEvent] + + // OnCollectionBeforeUpdateRequest hook is triggered before each API Collection + // update request (after request data load and before model persistence). + // + // Could be used to additionally validate the request data or implement + // completely different persistence behavior (returning hook.StopPropagation). + OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent] + + // OnCollectionAfterUpdateRequest hook is triggered after each + // successful API Collection update request. + OnCollectionAfterUpdateRequest() *hook.Hook[*CollectionUpdateEvent] + + // OnCollectionBeforeDeleteRequest hook is triggered before each API + // Collection delete request (after model load and before actual deletion). + // + // Could be used to additionally validate the request data or implement + // completely different delete behavior (returning hook.StopPropagation). + OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent] + + // OnCollectionAfterDeleteRequest hook is triggered after each + // successful API Collection delete request. + OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent] +} diff --git a/core/base.go b/core/base.go new file mode 100644 index 00000000..3fef5588 --- /dev/null +++ b/core/base.go @@ -0,0 +1,752 @@ +package core + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "os" + "path/filepath" + "time" + + "github.com/fatih/color" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/filesystem" + "github.com/pocketbase/pocketbase/tools/hook" + "github.com/pocketbase/pocketbase/tools/mailer" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/store" + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +var _ App = (*BaseApp)(nil) + +// BaseApp implements core.App and defines the base PocketBase app structure. +type BaseApp struct { + // configurable parameters + isDebug bool + dataDir string + encryptionEnv string + + // internals + cache *store.Store[any] + settings *Settings + db *dbx.DB + dao *daos.Dao + logsDB *dbx.DB + logsDao *daos.Dao + subscriptionsBroker *subscriptions.Broker + + // serve event hooks + onBeforeServe *hook.Hook[*ServeEvent] + + // dao event hooks + onModelBeforeCreate *hook.Hook[*ModelEvent] + onModelAfterCreate *hook.Hook[*ModelEvent] + onModelBeforeUpdate *hook.Hook[*ModelEvent] + onModelAfterUpdate *hook.Hook[*ModelEvent] + onModelBeforeDelete *hook.Hook[*ModelEvent] + onModelAfterDelete *hook.Hook[*ModelEvent] + + // mailer event hooks + onMailerBeforeAdminResetPasswordSend *hook.Hook[*MailerAdminEvent] + onMailerAfterAdminResetPasswordSend *hook.Hook[*MailerAdminEvent] + onMailerBeforeUserResetPasswordSend *hook.Hook[*MailerUserEvent] + onMailerAfterUserResetPasswordSend *hook.Hook[*MailerUserEvent] + onMailerBeforeUserVerificationSend *hook.Hook[*MailerUserEvent] + onMailerAfterUserVerificationSend *hook.Hook[*MailerUserEvent] + onMailerBeforeUserChangeEmailSend *hook.Hook[*MailerUserEvent] + onMailerAfterUserChangeEmailSend *hook.Hook[*MailerUserEvent] + + // realtime api event hooks + onRealtimeConnectRequest *hook.Hook[*RealtimeConnectEvent] + onRealtimeBeforeSubscribeRequest *hook.Hook[*RealtimeSubscribeEvent] + onRealtimeAfterSubscribeRequest *hook.Hook[*RealtimeSubscribeEvent] + + // settings api event hooks + onSettingsListRequest *hook.Hook[*SettingsListEvent] + onSettingsBeforeUpdateRequest *hook.Hook[*SettingsUpdateEvent] + onSettingsAfterUpdateRequest *hook.Hook[*SettingsUpdateEvent] + + // file api event hooks + onFileDownloadRequest *hook.Hook[*FileDownloadEvent] + + // admin api event hooks + onAdminsListRequest *hook.Hook[*AdminsListEvent] + onAdminViewRequest *hook.Hook[*AdminViewEvent] + onAdminBeforeCreateRequest *hook.Hook[*AdminCreateEvent] + onAdminAfterCreateRequest *hook.Hook[*AdminCreateEvent] + onAdminBeforeUpdateRequest *hook.Hook[*AdminUpdateEvent] + onAdminAfterUpdateRequest *hook.Hook[*AdminUpdateEvent] + onAdminBeforeDeleteRequest *hook.Hook[*AdminDeleteEvent] + onAdminAfterDeleteRequest *hook.Hook[*AdminDeleteEvent] + onAdminAuthRequest *hook.Hook[*AdminAuthEvent] + + // user api event hooks + onUsersListRequest *hook.Hook[*UsersListEvent] + onUserViewRequest *hook.Hook[*UserViewEvent] + onUserBeforeCreateRequest *hook.Hook[*UserCreateEvent] + onUserAfterCreateRequest *hook.Hook[*UserCreateEvent] + onUserBeforeUpdateRequest *hook.Hook[*UserUpdateEvent] + onUserAfterUpdateRequest *hook.Hook[*UserUpdateEvent] + onUserBeforeDeleteRequest *hook.Hook[*UserDeleteEvent] + onUserAfterDeleteRequest *hook.Hook[*UserDeleteEvent] + onUserAuthRequest *hook.Hook[*UserAuthEvent] + onUserBeforeOauth2Register *hook.Hook[*UserOauth2RegisterEvent] + onUserAfterOauth2Register *hook.Hook[*UserOauth2RegisterEvent] + + // record api event hooks + onRecordsListRequest *hook.Hook[*RecordsListEvent] + onRecordViewRequest *hook.Hook[*RecordViewEvent] + onRecordBeforeCreateRequest *hook.Hook[*RecordCreateEvent] + onRecordAfterCreateRequest *hook.Hook[*RecordCreateEvent] + onRecordBeforeUpdateRequest *hook.Hook[*RecordUpdateEvent] + onRecordAfterUpdateRequest *hook.Hook[*RecordUpdateEvent] + onRecordBeforeDeleteRequest *hook.Hook[*RecordDeleteEvent] + onRecordAfterDeleteRequest *hook.Hook[*RecordDeleteEvent] + + // collection api event hooks + onCollectionsListRequest *hook.Hook[*CollectionsListEvent] + onCollectionViewRequest *hook.Hook[*CollectionViewEvent] + onCollectionBeforeCreateRequest *hook.Hook[*CollectionCreateEvent] + onCollectionAfterCreateRequest *hook.Hook[*CollectionCreateEvent] + onCollectionBeforeUpdateRequest *hook.Hook[*CollectionUpdateEvent] + onCollectionAfterUpdateRequest *hook.Hook[*CollectionUpdateEvent] + onCollectionBeforeDeleteRequest *hook.Hook[*CollectionDeleteEvent] + onCollectionAfterDeleteRequest *hook.Hook[*CollectionDeleteEvent] +} + +// NewBaseApp creates and returns a new BaseApp instance +// configured with the provided arguments. +// +// To initialize the app, you need to call `app.Bootsrap()`. +func NewBaseApp(dataDir string, encryptionEnv string, isDebug bool) *BaseApp { + return &BaseApp{ + dataDir: dataDir, + isDebug: isDebug, + encryptionEnv: encryptionEnv, + cache: store.New[any](nil), + settings: NewSettings(), + subscriptionsBroker: subscriptions.NewBroker(), + + // serve event hooks + onBeforeServe: &hook.Hook[*ServeEvent]{}, + + // dao event hooks + onModelBeforeCreate: &hook.Hook[*ModelEvent]{}, + onModelAfterCreate: &hook.Hook[*ModelEvent]{}, + onModelBeforeUpdate: &hook.Hook[*ModelEvent]{}, + onModelAfterUpdate: &hook.Hook[*ModelEvent]{}, + onModelBeforeDelete: &hook.Hook[*ModelEvent]{}, + onModelAfterDelete: &hook.Hook[*ModelEvent]{}, + + // mailer event hooks + onMailerBeforeAdminResetPasswordSend: &hook.Hook[*MailerAdminEvent]{}, + onMailerAfterAdminResetPasswordSend: &hook.Hook[*MailerAdminEvent]{}, + onMailerBeforeUserResetPasswordSend: &hook.Hook[*MailerUserEvent]{}, + onMailerAfterUserResetPasswordSend: &hook.Hook[*MailerUserEvent]{}, + onMailerBeforeUserVerificationSend: &hook.Hook[*MailerUserEvent]{}, + onMailerAfterUserVerificationSend: &hook.Hook[*MailerUserEvent]{}, + onMailerBeforeUserChangeEmailSend: &hook.Hook[*MailerUserEvent]{}, + onMailerAfterUserChangeEmailSend: &hook.Hook[*MailerUserEvent]{}, + + // realtime API event hooks + onRealtimeConnectRequest: &hook.Hook[*RealtimeConnectEvent]{}, + onRealtimeBeforeSubscribeRequest: &hook.Hook[*RealtimeSubscribeEvent]{}, + onRealtimeAfterSubscribeRequest: &hook.Hook[*RealtimeSubscribeEvent]{}, + + // settings API event hooks + onSettingsListRequest: &hook.Hook[*SettingsListEvent]{}, + onSettingsBeforeUpdateRequest: &hook.Hook[*SettingsUpdateEvent]{}, + onSettingsAfterUpdateRequest: &hook.Hook[*SettingsUpdateEvent]{}, + + // file API event hooks + onFileDownloadRequest: &hook.Hook[*FileDownloadEvent]{}, + + // admin API event hooks + onAdminsListRequest: &hook.Hook[*AdminsListEvent]{}, + onAdminViewRequest: &hook.Hook[*AdminViewEvent]{}, + onAdminBeforeCreateRequest: &hook.Hook[*AdminCreateEvent]{}, + onAdminAfterCreateRequest: &hook.Hook[*AdminCreateEvent]{}, + onAdminBeforeUpdateRequest: &hook.Hook[*AdminUpdateEvent]{}, + onAdminAfterUpdateRequest: &hook.Hook[*AdminUpdateEvent]{}, + onAdminBeforeDeleteRequest: &hook.Hook[*AdminDeleteEvent]{}, + onAdminAfterDeleteRequest: &hook.Hook[*AdminDeleteEvent]{}, + onAdminAuthRequest: &hook.Hook[*AdminAuthEvent]{}, + + // user API event hooks + onUsersListRequest: &hook.Hook[*UsersListEvent]{}, + onUserViewRequest: &hook.Hook[*UserViewEvent]{}, + onUserBeforeCreateRequest: &hook.Hook[*UserCreateEvent]{}, + onUserAfterCreateRequest: &hook.Hook[*UserCreateEvent]{}, + onUserBeforeUpdateRequest: &hook.Hook[*UserUpdateEvent]{}, + onUserAfterUpdateRequest: &hook.Hook[*UserUpdateEvent]{}, + onUserBeforeDeleteRequest: &hook.Hook[*UserDeleteEvent]{}, + onUserAfterDeleteRequest: &hook.Hook[*UserDeleteEvent]{}, + onUserAuthRequest: &hook.Hook[*UserAuthEvent]{}, + onUserBeforeOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{}, + onUserAfterOauth2Register: &hook.Hook[*UserOauth2RegisterEvent]{}, + + // record API event hooks + onRecordsListRequest: &hook.Hook[*RecordsListEvent]{}, + onRecordViewRequest: &hook.Hook[*RecordViewEvent]{}, + onRecordBeforeCreateRequest: &hook.Hook[*RecordCreateEvent]{}, + onRecordAfterCreateRequest: &hook.Hook[*RecordCreateEvent]{}, + onRecordBeforeUpdateRequest: &hook.Hook[*RecordUpdateEvent]{}, + onRecordAfterUpdateRequest: &hook.Hook[*RecordUpdateEvent]{}, + onRecordBeforeDeleteRequest: &hook.Hook[*RecordDeleteEvent]{}, + onRecordAfterDeleteRequest: &hook.Hook[*RecordDeleteEvent]{}, + + // collection API event hooks + onCollectionsListRequest: &hook.Hook[*CollectionsListEvent]{}, + onCollectionViewRequest: &hook.Hook[*CollectionViewEvent]{}, + onCollectionBeforeCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, + onCollectionAfterCreateRequest: &hook.Hook[*CollectionCreateEvent]{}, + onCollectionBeforeUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, + onCollectionAfterUpdateRequest: &hook.Hook[*CollectionUpdateEvent]{}, + onCollectionBeforeDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, + onCollectionAfterDeleteRequest: &hook.Hook[*CollectionDeleteEvent]{}, + } +} + +// Bootstrap initializes the application +// (aka. create data dir, open db connections, load settings, etc.) +func (app *BaseApp) Bootstrap() error { + // clear resources of previous core state (if any) + if err := app.ResetBootstrapState(); err != nil { + return err + } + + // ensure that data dir exist + if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil { + return err + } + + if err := app.initDataDB(); err != nil { + return err + } + + if err := app.initLogsDB(); err != nil { + return err + } + + // we don't check for an error because the db migrations may + // have not been executed yet. + app.RefreshSettings() + + return nil +} + +// ResetBootstrapState takes care for releasing initialized app resources +// (eg. closing db connections). +func (app *BaseApp) ResetBootstrapState() error { + if app.db != nil { + if err := app.db.Close(); err != nil { + return err + } + } + + if app.logsDB != nil { + if err := app.logsDB.Close(); err != nil { + return err + } + } + + app.dao = nil + app.logsDao = nil + app.settings = nil + + return nil +} + +// DB returns the default app database instance. +func (app *BaseApp) DB() *dbx.DB { + return app.db +} + +// Dao returns the default app Dao instance. +func (app *BaseApp) Dao() *daos.Dao { + return app.dao +} + +// LogsDB returns the app logs database instance. +func (app *BaseApp) LogsDB() *dbx.DB { + return app.logsDB +} + +// LogsDao returns the app logs Dao instance. +func (app *BaseApp) LogsDao() *daos.Dao { + return app.logsDao +} + +// DataDir returns the app data directory path. +func (app *BaseApp) DataDir() string { + return app.dataDir +} + +// EncryptionEnv returns the name of the app secret env key +// (used for settings encryption). +func (app *BaseApp) EncryptionEnv() string { + return app.encryptionEnv +} + +// IsDebug returns whether the app is in debug mode +// (showing more detailed error logs, executed sql statements, etc.). +func (app *BaseApp) IsDebug() bool { + return app.isDebug +} + +// Settings returns the loaded app settings. +func (app *BaseApp) Settings() *Settings { + return app.settings +} + +// Cache returns the app internal cache store. +func (app *BaseApp) Cache() *store.Store[any] { + return app.cache +} + +// SubscriptionsBroker returns the app realtime subscriptions broker instance. +func (app *BaseApp) SubscriptionsBroker() *subscriptions.Broker { + return app.subscriptionsBroker +} + +// NewMailClient creates and returns a new SMTP or Sendmail client +// based on the current app settings. +func (app *BaseApp) NewMailClient() mailer.Mailer { + if app.Settings().Smtp.Enabled { + return mailer.NewSmtpClient( + app.Settings().Smtp.Host, + app.Settings().Smtp.Port, + app.Settings().Smtp.Username, + app.Settings().Smtp.Password, + app.Settings().Smtp.Tls, + ) + } + + return &mailer.Sendmail{} +} + +// NewFilesystem creates a new local or S3 filesystem instance +// based on the current app settings. +// +// NB! Make sure to call `Close()` on the returned result +// after you are done working with it. +func (app *BaseApp) NewFilesystem() (*filesystem.System, error) { + if app.settings.S3.Enabled { + return filesystem.NewS3( + app.settings.S3.Bucket, + app.settings.S3.Region, + app.settings.S3.Endpoint, + app.settings.S3.AccessKey, + app.settings.S3.Secret, + ) + } + + // fallback to local filesystem + return filesystem.NewLocal(filepath.Join(app.DataDir(), "storage")) +} + +// RefreshSettings reinitializes and reloads the stored application settings. +func (app *BaseApp) RefreshSettings() error { + if app.settings == nil { + app.settings = NewSettings() + } + + encryptionKey := os.Getenv(app.EncryptionEnv()) + + param, err := app.Dao().FindParamByKey(models.ParamAppSettings) + if err != nil && err != sql.ErrNoRows { + return err + } + + if param == nil { + // no settings were previously stored + return app.Dao().SaveParam(models.ParamAppSettings, app.settings, encryptionKey) + } + + // load the settings from the stored param into the app ones + // --- + newSettings := NewSettings() + + // try first without decryption + plainDecodeErr := json.Unmarshal(param.Value, newSettings) + + // failed, try to decrypt + if plainDecodeErr != nil { + // load without decrypt has failed and there is no encryption key to use for decrypt + if encryptionKey == "" { + return errors.New("Failed to load the stored app settings (missing or invalid encryption key).") + } + + // decrypt + decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey) + if decryptErr != nil { + return decryptErr + } + + // decode again + decryptedDecodeErr := json.Unmarshal(decrypted, newSettings) + if decryptedDecodeErr != nil { + return decryptedDecodeErr + } + } + + if err := app.settings.Merge(newSettings); err != nil { + return err + } + + if plainDecodeErr == nil && encryptionKey != "" { + // save because previously the settings weren't stored encrypted + saveErr := app.Dao().SaveParam(models.ParamAppSettings, app.settings, encryptionKey) + if saveErr != nil { + return saveErr + } + } + + return nil +} + +// ------------------------------------------------------------------- +// Serve event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnBeforeServe() *hook.Hook[*ServeEvent] { + return app.onBeforeServe +} + +// ------------------------------------------------------------------- +// Dao event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnModelBeforeCreate() *hook.Hook[*ModelEvent] { + return app.onModelBeforeCreate +} + +func (app *BaseApp) OnModelAfterCreate() *hook.Hook[*ModelEvent] { + return app.onModelAfterCreate +} + +func (app *BaseApp) OnModelBeforeUpdate() *hook.Hook[*ModelEvent] { + return app.onModelBeforeUpdate +} + +func (app *BaseApp) OnModelAfterUpdate() *hook.Hook[*ModelEvent] { + return app.onModelAfterUpdate +} + +func (app *BaseApp) OnModelBeforeDelete() *hook.Hook[*ModelEvent] { + return app.onModelBeforeDelete +} + +func (app *BaseApp) OnModelAfterDelete() *hook.Hook[*ModelEvent] { + return app.onModelAfterDelete +} + +// ------------------------------------------------------------------- +// Mailer event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnMailerBeforeAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] { + return app.onMailerBeforeAdminResetPasswordSend +} + +func (app *BaseApp) OnMailerAfterAdminResetPasswordSend() *hook.Hook[*MailerAdminEvent] { + return app.onMailerAfterAdminResetPasswordSend +} + +func (app *BaseApp) OnMailerBeforeUserResetPasswordSend() *hook.Hook[*MailerUserEvent] { + return app.onMailerBeforeUserResetPasswordSend +} + +func (app *BaseApp) OnMailerAfterUserResetPasswordSend() *hook.Hook[*MailerUserEvent] { + return app.onMailerAfterUserResetPasswordSend +} + +func (app *BaseApp) OnMailerBeforeUserVerificationSend() *hook.Hook[*MailerUserEvent] { + return app.onMailerBeforeUserVerificationSend +} + +func (app *BaseApp) OnMailerAfterUserVerificationSend() *hook.Hook[*MailerUserEvent] { + return app.onMailerAfterUserVerificationSend +} + +func (app *BaseApp) OnMailerBeforeUserChangeEmailSend() *hook.Hook[*MailerUserEvent] { + return app.onMailerBeforeUserChangeEmailSend +} + +func (app *BaseApp) OnMailerAfterUserChangeEmailSend() *hook.Hook[*MailerUserEvent] { + return app.onMailerAfterUserChangeEmailSend +} + +// ------------------------------------------------------------------- +// Realtime API event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnRealtimeConnectRequest() *hook.Hook[*RealtimeConnectEvent] { + return app.onRealtimeConnectRequest +} + +func (app *BaseApp) OnRealtimeBeforeSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] { + return app.onRealtimeBeforeSubscribeRequest +} + +func (app *BaseApp) OnRealtimeAfterSubscribeRequest() *hook.Hook[*RealtimeSubscribeEvent] { + return app.onRealtimeAfterSubscribeRequest +} + +// ------------------------------------------------------------------- +// Settings API event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnSettingsListRequest() *hook.Hook[*SettingsListEvent] { + return app.onSettingsListRequest +} + +func (app *BaseApp) OnSettingsBeforeUpdateRequest() *hook.Hook[*SettingsUpdateEvent] { + return app.onSettingsBeforeUpdateRequest +} + +func (app *BaseApp) OnSettingsAfterUpdateRequest() *hook.Hook[*SettingsUpdateEvent] { + return app.onSettingsAfterUpdateRequest +} + +// ------------------------------------------------------------------- +// File API event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnFileDownloadRequest() *hook.Hook[*FileDownloadEvent] { + return app.onFileDownloadRequest +} + +// ------------------------------------------------------------------- +// Admin API event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnAdminsListRequest() *hook.Hook[*AdminsListEvent] { + return app.onAdminsListRequest +} + +func (app *BaseApp) OnAdminViewRequest() *hook.Hook[*AdminViewEvent] { + return app.onAdminViewRequest +} + +func (app *BaseApp) OnAdminBeforeCreateRequest() *hook.Hook[*AdminCreateEvent] { + return app.onAdminBeforeCreateRequest +} + +func (app *BaseApp) OnAdminAfterCreateRequest() *hook.Hook[*AdminCreateEvent] { + return app.onAdminAfterCreateRequest +} + +func (app *BaseApp) OnAdminBeforeUpdateRequest() *hook.Hook[*AdminUpdateEvent] { + return app.onAdminBeforeUpdateRequest +} + +func (app *BaseApp) OnAdminAfterUpdateRequest() *hook.Hook[*AdminUpdateEvent] { + return app.onAdminAfterUpdateRequest +} + +func (app *BaseApp) OnAdminBeforeDeleteRequest() *hook.Hook[*AdminDeleteEvent] { + return app.onAdminBeforeDeleteRequest +} + +func (app *BaseApp) OnAdminAfterDeleteRequest() *hook.Hook[*AdminDeleteEvent] { + return app.onAdminAfterDeleteRequest +} + +func (app *BaseApp) OnAdminAuthRequest() *hook.Hook[*AdminAuthEvent] { + return app.onAdminAuthRequest +} + +// ------------------------------------------------------------------- +// User API event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnUsersListRequest() *hook.Hook[*UsersListEvent] { + return app.onUsersListRequest +} + +func (app *BaseApp) OnUserViewRequest() *hook.Hook[*UserViewEvent] { + return app.onUserViewRequest +} + +func (app *BaseApp) OnUserBeforeCreateRequest() *hook.Hook[*UserCreateEvent] { + return app.onUserBeforeCreateRequest +} + +func (app *BaseApp) OnUserAfterCreateRequest() *hook.Hook[*UserCreateEvent] { + return app.onUserAfterCreateRequest +} + +func (app *BaseApp) OnUserBeforeUpdateRequest() *hook.Hook[*UserUpdateEvent] { + return app.onUserBeforeUpdateRequest +} + +func (app *BaseApp) OnUserAfterUpdateRequest() *hook.Hook[*UserUpdateEvent] { + return app.onUserAfterUpdateRequest +} + +func (app *BaseApp) OnUserBeforeDeleteRequest() *hook.Hook[*UserDeleteEvent] { + return app.onUserBeforeDeleteRequest +} + +func (app *BaseApp) OnUserAfterDeleteRequest() *hook.Hook[*UserDeleteEvent] { + return app.onUserAfterDeleteRequest +} + +func (app *BaseApp) OnUserAuthRequest() *hook.Hook[*UserAuthEvent] { + return app.onUserAuthRequest +} + +func (app *BaseApp) OnUserBeforeOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] { + return app.onUserBeforeOauth2Register +} + +func (app *BaseApp) OnUserAfterOauth2Register() *hook.Hook[*UserOauth2RegisterEvent] { + return app.onUserAfterOauth2Register +} + +// ------------------------------------------------------------------- +// Record API event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnRecordsListRequest() *hook.Hook[*RecordsListEvent] { + return app.onRecordsListRequest +} + +func (app *BaseApp) OnRecordViewRequest() *hook.Hook[*RecordViewEvent] { + return app.onRecordViewRequest +} + +func (app *BaseApp) OnRecordBeforeCreateRequest() *hook.Hook[*RecordCreateEvent] { + return app.onRecordBeforeCreateRequest +} + +func (app *BaseApp) OnRecordAfterCreateRequest() *hook.Hook[*RecordCreateEvent] { + return app.onRecordAfterCreateRequest +} + +func (app *BaseApp) OnRecordBeforeUpdateRequest() *hook.Hook[*RecordUpdateEvent] { + return app.onRecordBeforeUpdateRequest +} + +func (app *BaseApp) OnRecordAfterUpdateRequest() *hook.Hook[*RecordUpdateEvent] { + return app.onRecordAfterUpdateRequest +} + +func (app *BaseApp) OnRecordBeforeDeleteRequest() *hook.Hook[*RecordDeleteEvent] { + return app.onRecordBeforeDeleteRequest +} + +func (app *BaseApp) OnRecordAfterDeleteRequest() *hook.Hook[*RecordDeleteEvent] { + return app.onRecordAfterDeleteRequest +} + +// ------------------------------------------------------------------- +// Collection API event hooks +// ------------------------------------------------------------------- + +func (app *BaseApp) OnCollectionsListRequest() *hook.Hook[*CollectionsListEvent] { + return app.onCollectionsListRequest +} + +func (app *BaseApp) OnCollectionViewRequest() *hook.Hook[*CollectionViewEvent] { + return app.onCollectionViewRequest +} + +func (app *BaseApp) OnCollectionBeforeCreateRequest() *hook.Hook[*CollectionCreateEvent] { + return app.onCollectionBeforeCreateRequest +} + +func (app *BaseApp) OnCollectionAfterCreateRequest() *hook.Hook[*CollectionCreateEvent] { + return app.onCollectionAfterCreateRequest +} + +func (app *BaseApp) OnCollectionBeforeUpdateRequest() *hook.Hook[*CollectionUpdateEvent] { + return app.onCollectionBeforeUpdateRequest +} + +func (app *BaseApp) OnCollectionAfterUpdateRequest() *hook.Hook[*CollectionUpdateEvent] { + return app.onCollectionAfterUpdateRequest +} + +func (app *BaseApp) OnCollectionBeforeDeleteRequest() *hook.Hook[*CollectionDeleteEvent] { + return app.onCollectionBeforeDeleteRequest +} + +func (app *BaseApp) OnCollectionAfterDeleteRequest() *hook.Hook[*CollectionDeleteEvent] { + return app.onCollectionAfterDeleteRequest +} + +// ------------------------------------------------------------------- +// Helpers +// ------------------------------------------------------------------- + +func (app *BaseApp) initLogsDB() error { + var connectErr error + app.logsDB, connectErr = connectDB(filepath.Join(app.DataDir(), "logs.db")) + if connectErr != nil { + return connectErr + } + + app.logsDao = app.createDao(app.logsDB) + + return nil +} + +func (app *BaseApp) initDataDB() error { + var connectErr error + app.db, connectErr = connectDB(filepath.Join(app.DataDir(), "data.db")) + if connectErr != nil { + return connectErr + } + + app.db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + if app.IsDebug() { + color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), sql) + } + } + + app.db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { + if app.IsDebug() { + color.HiBlack("[%.2fms] %v\n", float64(t.Milliseconds()), sql) + } + } + + app.dao = app.createDao(app.db) + + return nil +} + +func (app *BaseApp) createDao(db dbx.Builder) *daos.Dao { + dao := daos.New(db) + + dao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error { + return app.OnModelBeforeCreate().Trigger(&ModelEvent{eventDao, m}) + } + + dao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) { + app.OnModelAfterCreate().Trigger(&ModelEvent{eventDao, m}) + } + + dao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { + return app.OnModelBeforeUpdate().Trigger(&ModelEvent{eventDao, m}) + } + + dao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) { + app.OnModelAfterUpdate().Trigger(&ModelEvent{eventDao, m}) + } + + dao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { + return app.OnModelBeforeDelete().Trigger(&ModelEvent{eventDao, m}) + } + + dao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) { + app.OnModelAfterDelete().Trigger(&ModelEvent{eventDao, m}) + } + + return dao +} diff --git a/core/base_test.go b/core/base_test.go new file mode 100644 index 00000000..0a589fda --- /dev/null +++ b/core/base_test.go @@ -0,0 +1,438 @@ +package core + +import ( + "os" + "testing" + + "github.com/pocketbase/pocketbase/tools/mailer" +) + +func TestNewBaseApp(t *testing.T) { + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := NewBaseApp(testDataDir, "test_env", true) + + if app.dataDir != testDataDir { + t.Fatalf("expected dataDir %q, got %q", testDataDir, app.dataDir) + } + + if app.encryptionEnv != "test_env" { + t.Fatalf("expected encryptionEnv test_env, got %q", app.dataDir) + } + + if !app.isDebug { + t.Fatalf("expected isDebug true, got %v", app.isDebug) + } + + if app.cache == nil { + t.Fatal("expected cache to be set, got nil") + } + + if app.settings == nil { + t.Fatal("expected settings to be set, got nil") + } + + if app.subscriptionsBroker == nil { + t.Fatal("expected subscriptionsBroker to be set, got nil") + } +} + +func TestBaseAppBootstrap(t *testing.T) { + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := NewBaseApp(testDataDir, "pb_test_env", false) + defer app.ResetBootstrapState() + + // bootstrap + if err := app.Bootstrap(); err != nil { + t.Fatal(err) + } + + if stat, err := os.Stat(testDataDir); err != nil || !stat.IsDir() { + t.Fatal("Expected test data directory to be created.") + } + + if app.dao == nil { + t.Fatal("Expected app.dao to be initialized, got nil.") + } + + if app.dao.BeforeCreateFunc == nil { + t.Fatal("Expected app.dao.BeforeCreateFunc to be set, got nil.") + } + + if app.dao.AfterCreateFunc == nil { + t.Fatal("Expected app.dao.AfterCreateFunc to be set, got nil.") + } + + if app.dao.BeforeUpdateFunc == nil { + t.Fatal("Expected app.dao.BeforeUpdateFunc to be set, got nil.") + } + + if app.dao.AfterUpdateFunc == nil { + t.Fatal("Expected app.dao.AfterUpdateFunc to be set, got nil.") + } + + if app.dao.BeforeDeleteFunc == nil { + t.Fatal("Expected app.dao.BeforeDeleteFunc to be set, got nil.") + } + + if app.dao.AfterDeleteFunc == nil { + t.Fatal("Expected app.dao.AfterDeleteFunc to be set, got nil.") + } + + if app.logsDao == nil { + t.Fatal("Expected app.logsDao to be initialized, got nil.") + } + + if app.settings == nil { + t.Fatal("Expected app.settings to be initialized, got nil.") + } + + // reset + if err := app.ResetBootstrapState(); err != nil { + t.Fatal(err) + } + + if app.dao != nil { + t.Fatalf("Expected app.dao to be nil, got %v.", app.dao) + } + + if app.logsDao != nil { + t.Fatalf("Expected app.logsDao to be nil, got %v.", app.logsDao) + } + + if app.settings != nil { + t.Fatalf("Expected app.settings to be nil, got %v.", app.settings) + } +} + +func TestBaseAppGetters(t *testing.T) { + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := NewBaseApp(testDataDir, "pb_test_env", false) + defer app.ResetBootstrapState() + + if err := app.Bootstrap(); err != nil { + t.Fatal(err) + } + + if app.db != app.DB() { + t.Fatalf("Expected app.DB %v, got %v", app.DB(), app.db) + } + + if app.dao != app.Dao() { + t.Fatalf("Expected app.Dao %v, got %v", app.Dao(), app.dao) + } + + if app.logsDB != app.LogsDB() { + t.Fatalf("Expected app.LogsDB %v, got %v", app.LogsDB(), app.logsDB) + } + + if app.logsDao != app.LogsDao() { + t.Fatalf("Expected app.LogsDao %v, got %v", app.LogsDao(), app.logsDao) + } + + if app.dataDir != app.DataDir() { + t.Fatalf("Expected app.DataDir %v, got %v", app.DataDir(), app.dataDir) + } + + if app.encryptionEnv != app.EncryptionEnv() { + t.Fatalf("Expected app.EncryptionEnv %v, got %v", app.EncryptionEnv(), app.encryptionEnv) + } + + if app.isDebug != app.IsDebug() { + t.Fatalf("Expected app.IsDebug %v, got %v", app.IsDebug(), app.isDebug) + } + + if app.settings != app.Settings() { + t.Fatalf("Expected app.Settings %v, got %v", app.Settings(), app.settings) + } + + if app.cache != app.Cache() { + t.Fatalf("Expected app.Cache %v, got %v", app.Cache(), app.cache) + } + + if app.subscriptionsBroker != app.SubscriptionsBroker() { + t.Fatalf("Expected app.SubscriptionsBroker %v, got %v", app.SubscriptionsBroker(), app.subscriptionsBroker) + } + + if app.onBeforeServe != app.OnBeforeServe() || app.OnBeforeServe() == nil { + t.Fatalf("Getter app.OnBeforeServe does not match or nil (%v vs %v)", app.OnBeforeServe(), app.onBeforeServe) + } + + if app.onModelBeforeCreate != app.OnModelBeforeCreate() || app.OnModelBeforeCreate() == nil { + t.Fatalf("Getter app.OnModelBeforeCreate does not match or nil (%v vs %v)", app.OnModelBeforeCreate(), app.onModelBeforeCreate) + } + + if app.onModelAfterCreate != app.OnModelAfterCreate() || app.OnModelAfterCreate() == nil { + t.Fatalf("Getter app.OnModelAfterCreate does not match or nil (%v vs %v)", app.OnModelAfterCreate(), app.onModelAfterCreate) + } + + if app.onModelBeforeUpdate != app.OnModelBeforeUpdate() || app.OnModelBeforeUpdate() == nil { + t.Fatalf("Getter app.OnModelBeforeUpdate does not match or nil (%v vs %v)", app.OnModelBeforeUpdate(), app.onModelBeforeUpdate) + } + + if app.onModelAfterUpdate != app.OnModelAfterUpdate() || app.OnModelAfterUpdate() == nil { + t.Fatalf("Getter app.OnModelAfterUpdate does not match or nil (%v vs %v)", app.OnModelAfterUpdate(), app.onModelAfterUpdate) + } + + if app.onModelBeforeDelete != app.OnModelBeforeDelete() || app.OnModelBeforeDelete() == nil { + t.Fatalf("Getter app.OnModelBeforeDelete does not match or nil (%v vs %v)", app.OnModelBeforeDelete(), app.onModelBeforeDelete) + } + + if app.onModelAfterDelete != app.OnModelAfterDelete() || app.OnModelAfterDelete() == nil { + t.Fatalf("Getter app.OnModelAfterDelete does not match or nil (%v vs %v)", app.OnModelAfterDelete(), app.onModelAfterDelete) + } + + if app.onMailerBeforeAdminResetPasswordSend != app.OnMailerBeforeAdminResetPasswordSend() || app.OnMailerBeforeAdminResetPasswordSend() == nil { + t.Fatalf("Getter app.OnMailerBeforeAdminResetPasswordSend does not match or nil (%v vs %v)", app.OnMailerBeforeAdminResetPasswordSend(), app.onMailerBeforeAdminResetPasswordSend) + } + + if app.onMailerAfterAdminResetPasswordSend != app.OnMailerAfterAdminResetPasswordSend() || app.OnMailerAfterAdminResetPasswordSend() == nil { + t.Fatalf("Getter app.OnMailerAfterAdminResetPasswordSend does not match or nil (%v vs %v)", app.OnMailerAfterAdminResetPasswordSend(), app.onMailerAfterAdminResetPasswordSend) + } + + if app.onMailerBeforeUserResetPasswordSend != app.OnMailerBeforeUserResetPasswordSend() || app.OnMailerBeforeUserResetPasswordSend() == nil { + t.Fatalf("Getter app.OnMailerBeforeUserResetPasswordSend does not match or nil (%v vs %v)", app.OnMailerBeforeUserResetPasswordSend(), app.onMailerBeforeUserResetPasswordSend) + } + + if app.onMailerAfterUserResetPasswordSend != app.OnMailerAfterUserResetPasswordSend() || app.OnMailerAfterUserResetPasswordSend() == nil { + t.Fatalf("Getter app.OnMailerAfterUserResetPasswordSend does not match or nil (%v vs %v)", app.OnMailerAfterUserResetPasswordSend(), app.onMailerAfterUserResetPasswordSend) + } + + if app.onMailerBeforeUserVerificationSend != app.OnMailerBeforeUserVerificationSend() || app.OnMailerBeforeUserVerificationSend() == nil { + t.Fatalf("Getter app.OnMailerBeforeUserVerificationSend does not match or nil (%v vs %v)", app.OnMailerBeforeUserVerificationSend(), app.onMailerBeforeUserVerificationSend) + } + + if app.onMailerAfterUserVerificationSend != app.OnMailerAfterUserVerificationSend() || app.OnMailerAfterUserVerificationSend() == nil { + t.Fatalf("Getter app.OnMailerAfterUserVerificationSend does not match or nil (%v vs %v)", app.OnMailerAfterUserVerificationSend(), app.onMailerAfterUserVerificationSend) + } + + if app.onMailerBeforeUserChangeEmailSend != app.OnMailerBeforeUserChangeEmailSend() || app.OnMailerBeforeUserChangeEmailSend() == nil { + t.Fatalf("Getter app.OnMailerBeforeUserChangeEmailSend does not match or nil (%v vs %v)", app.OnMailerBeforeUserChangeEmailSend(), app.onMailerBeforeUserChangeEmailSend) + } + + if app.onMailerAfterUserChangeEmailSend != app.OnMailerAfterUserChangeEmailSend() || app.OnMailerAfterUserChangeEmailSend() == nil { + t.Fatalf("Getter app.OnMailerAfterUserChangeEmailSend does not match or nil (%v vs %v)", app.OnMailerAfterUserChangeEmailSend(), app.onMailerAfterUserChangeEmailSend) + } + + if app.onRealtimeConnectRequest != app.OnRealtimeConnectRequest() || app.OnRealtimeConnectRequest() == nil { + t.Fatalf("Getter app.OnRealtimeConnectRequest does not match or nil (%v vs %v)", app.OnRealtimeConnectRequest(), app.onRealtimeConnectRequest) + } + + if app.onRealtimeBeforeSubscribeRequest != app.OnRealtimeBeforeSubscribeRequest() || app.OnRealtimeBeforeSubscribeRequest() == nil { + t.Fatalf("Getter app.OnRealtimeBeforeSubscribeRequest does not match or nil (%v vs %v)", app.OnRealtimeBeforeSubscribeRequest(), app.onRealtimeBeforeSubscribeRequest) + } + + if app.onRealtimeAfterSubscribeRequest != app.OnRealtimeAfterSubscribeRequest() || app.OnRealtimeAfterSubscribeRequest() == nil { + t.Fatalf("Getter app.OnRealtimeAfterSubscribeRequest does not match or nil (%v vs %v)", app.OnRealtimeAfterSubscribeRequest(), app.onRealtimeAfterSubscribeRequest) + } + + if app.onSettingsListRequest != app.OnSettingsListRequest() || app.OnSettingsListRequest() == nil { + t.Fatalf("Getter app.OnSettingsListRequest does not match or nil (%v vs %v)", app.OnSettingsListRequest(), app.onSettingsListRequest) + } + + if app.onSettingsBeforeUpdateRequest != app.OnSettingsBeforeUpdateRequest() || app.OnSettingsBeforeUpdateRequest() == nil { + t.Fatalf("Getter app.OnSettingsBeforeUpdateRequest does not match or nil (%v vs %v)", app.OnSettingsBeforeUpdateRequest(), app.onSettingsBeforeUpdateRequest) + } + + if app.onSettingsAfterUpdateRequest != app.OnSettingsAfterUpdateRequest() || app.OnSettingsAfterUpdateRequest() == nil { + t.Fatalf("Getter app.OnSettingsAfterUpdateRequest does not match or nil (%v vs %v)", app.OnSettingsAfterUpdateRequest(), app.onSettingsAfterUpdateRequest) + } + + if app.onFileDownloadRequest != app.OnFileDownloadRequest() || app.OnFileDownloadRequest() == nil { + t.Fatalf("Getter app.OnFileDownloadRequest does not match or nil (%v vs %v)", app.OnFileDownloadRequest(), app.onFileDownloadRequest) + } + + if app.onAdminsListRequest != app.OnAdminsListRequest() || app.OnAdminsListRequest() == nil { + t.Fatalf("Getter app.OnAdminsListRequest does not match or nil (%v vs %v)", app.OnAdminsListRequest(), app.onAdminsListRequest) + } + + if app.onAdminViewRequest != app.OnAdminViewRequest() || app.OnAdminViewRequest() == nil { + t.Fatalf("Getter app.OnAdminViewRequest does not match or nil (%v vs %v)", app.OnAdminViewRequest(), app.onAdminViewRequest) + } + + if app.onAdminBeforeCreateRequest != app.OnAdminBeforeCreateRequest() || app.OnAdminBeforeCreateRequest() == nil { + t.Fatalf("Getter app.OnAdminBeforeCreateRequest does not match or nil (%v vs %v)", app.OnAdminBeforeCreateRequest(), app.onAdminBeforeCreateRequest) + } + + if app.onAdminAfterCreateRequest != app.OnAdminAfterCreateRequest() || app.OnAdminAfterCreateRequest() == nil { + t.Fatalf("Getter app.OnAdminAfterCreateRequest does not match or nil (%v vs %v)", app.OnAdminAfterCreateRequest(), app.onAdminAfterCreateRequest) + } + + if app.onAdminBeforeUpdateRequest != app.OnAdminBeforeUpdateRequest() || app.OnAdminBeforeUpdateRequest() == nil { + t.Fatalf("Getter app.OnAdminBeforeUpdateRequest does not match or nil (%v vs %v)", app.OnAdminBeforeUpdateRequest(), app.onAdminBeforeUpdateRequest) + } + + if app.onAdminAfterUpdateRequest != app.OnAdminAfterUpdateRequest() || app.OnAdminAfterUpdateRequest() == nil { + t.Fatalf("Getter app.OnAdminAfterUpdateRequest does not match or nil (%v vs %v)", app.OnAdminAfterUpdateRequest(), app.onAdminAfterUpdateRequest) + } + + if app.onAdminBeforeDeleteRequest != app.OnAdminBeforeDeleteRequest() || app.OnAdminBeforeDeleteRequest() == nil { + t.Fatalf("Getter app.OnAdminBeforeDeleteRequest does not match or nil (%v vs %v)", app.OnAdminBeforeDeleteRequest(), app.onAdminBeforeDeleteRequest) + } + + if app.onAdminAfterDeleteRequest != app.OnAdminAfterDeleteRequest() || app.OnAdminAfterDeleteRequest() == nil { + t.Fatalf("Getter app.OnAdminAfterDeleteRequest does not match or nil (%v vs %v)", app.OnAdminAfterDeleteRequest(), app.onAdminAfterDeleteRequest) + } + + if app.onAdminAuthRequest != app.OnAdminAuthRequest() || app.OnAdminAuthRequest() == nil { + t.Fatalf("Getter app.OnAdminAuthRequest does not match or nil (%v vs %v)", app.OnAdminAuthRequest(), app.onAdminAuthRequest) + } + + if app.onUsersListRequest != app.OnUsersListRequest() || app.OnUsersListRequest() == nil { + t.Fatalf("Getter app.OnUsersListRequest does not match or nil (%v vs %v)", app.OnUsersListRequest(), app.onUsersListRequest) + } + + if app.onUserViewRequest != app.OnUserViewRequest() || app.OnUserViewRequest() == nil { + t.Fatalf("Getter app.OnUserViewRequest does not match or nil (%v vs %v)", app.OnUserViewRequest(), app.onUserViewRequest) + } + + if app.onUserBeforeCreateRequest != app.OnUserBeforeCreateRequest() || app.OnUserBeforeCreateRequest() == nil { + t.Fatalf("Getter app.OnUserBeforeCreateRequest does not match or nil (%v vs %v)", app.OnUserBeforeCreateRequest(), app.onUserBeforeCreateRequest) + } + + if app.onUserAfterCreateRequest != app.OnUserAfterCreateRequest() || app.OnUserAfterCreateRequest() == nil { + t.Fatalf("Getter app.OnUserAfterCreateRequest does not match or nil (%v vs %v)", app.OnUserAfterCreateRequest(), app.onUserAfterCreateRequest) + } + + if app.onUserBeforeUpdateRequest != app.OnUserBeforeUpdateRequest() || app.OnUserBeforeUpdateRequest() == nil { + t.Fatalf("Getter app.OnUserBeforeUpdateRequest does not match or nil (%v vs %v)", app.OnUserBeforeUpdateRequest(), app.onUserBeforeUpdateRequest) + } + + if app.onUserAfterUpdateRequest != app.OnUserAfterUpdateRequest() || app.OnUserAfterUpdateRequest() == nil { + t.Fatalf("Getter app.OnUserAfterUpdateRequest does not match or nil (%v vs %v)", app.OnUserAfterUpdateRequest(), app.onUserAfterUpdateRequest) + } + + if app.onUserBeforeDeleteRequest != app.OnUserBeforeDeleteRequest() || app.OnUserBeforeDeleteRequest() == nil { + t.Fatalf("Getter app.OnUserBeforeDeleteRequest does not match or nil (%v vs %v)", app.OnUserBeforeDeleteRequest(), app.onUserBeforeDeleteRequest) + } + + if app.onUserAfterDeleteRequest != app.OnUserAfterDeleteRequest() || app.OnUserAfterDeleteRequest() == nil { + t.Fatalf("Getter app.OnUserAfterDeleteRequest does not match or nil (%v vs %v)", app.OnUserAfterDeleteRequest(), app.onUserAfterDeleteRequest) + } + + if app.onUserAuthRequest != app.OnUserAuthRequest() || app.OnUserAuthRequest() == nil { + t.Fatalf("Getter app.OnUserAuthRequest does not match or nil (%v vs %v)", app.OnUserAuthRequest(), app.onUserAuthRequest) + } + + if app.onUserBeforeOauth2Register != app.OnUserBeforeOauth2Register() || app.OnUserBeforeOauth2Register() == nil { + t.Fatalf("Getter app.OnUserBeforeOauth2Register does not match or nil (%v vs %v)", app.OnUserBeforeOauth2Register(), app.onUserBeforeOauth2Register) + } + + if app.onUserAfterOauth2Register != app.OnUserAfterOauth2Register() || app.OnUserAfterOauth2Register() == nil { + t.Fatalf("Getter app.OnUserAfterOauth2Register does not match or nil (%v vs %v)", app.OnUserAfterOauth2Register(), app.onUserAfterOauth2Register) + } + + if app.onRecordsListRequest != app.OnRecordsListRequest() || app.OnRecordsListRequest() == nil { + t.Fatalf("Getter app.OnRecordsListRequest does not match or nil (%v vs %v)", app.OnRecordsListRequest(), app.onRecordsListRequest) + } + + if app.onRecordViewRequest != app.OnRecordViewRequest() || app.OnRecordViewRequest() == nil { + t.Fatalf("Getter app.OnRecordViewRequest does not match or nil (%v vs %v)", app.OnRecordViewRequest(), app.onRecordViewRequest) + } + + if app.onRecordBeforeCreateRequest != app.OnRecordBeforeCreateRequest() || app.OnRecordBeforeCreateRequest() == nil { + t.Fatalf("Getter app.OnRecordBeforeCreateRequest does not match or nil (%v vs %v)", app.OnRecordBeforeCreateRequest(), app.onRecordBeforeCreateRequest) + } + + if app.onRecordAfterCreateRequest != app.OnRecordAfterCreateRequest() || app.OnRecordAfterCreateRequest() == nil { + t.Fatalf("Getter app.OnRecordAfterCreateRequest does not match or nil (%v vs %v)", app.OnRecordAfterCreateRequest(), app.onRecordAfterCreateRequest) + } + + if app.onRecordBeforeUpdateRequest != app.OnRecordBeforeUpdateRequest() || app.OnRecordBeforeUpdateRequest() == nil { + t.Fatalf("Getter app.OnRecordBeforeUpdateRequest does not match or nil (%v vs %v)", app.OnRecordBeforeUpdateRequest(), app.onRecordBeforeUpdateRequest) + } + + if app.onRecordAfterUpdateRequest != app.OnRecordAfterUpdateRequest() || app.OnRecordAfterUpdateRequest() == nil { + t.Fatalf("Getter app.OnRecordAfterUpdateRequest does not match or nil (%v vs %v)", app.OnRecordAfterUpdateRequest(), app.onRecordAfterUpdateRequest) + } + + if app.onRecordBeforeDeleteRequest != app.OnRecordBeforeDeleteRequest() || app.OnRecordBeforeDeleteRequest() == nil { + t.Fatalf("Getter app.OnRecordBeforeDeleteRequest does not match or nil (%v vs %v)", app.OnRecordBeforeDeleteRequest(), app.onRecordBeforeDeleteRequest) + } + + if app.onRecordAfterDeleteRequest != app.OnRecordAfterDeleteRequest() || app.OnRecordAfterDeleteRequest() == nil { + t.Fatalf("Getter app.OnRecordAfterDeleteRequest does not match or nil (%v vs %v)", app.OnRecordAfterDeleteRequest(), app.onRecordAfterDeleteRequest) + } + + if app.onCollectionsListRequest != app.OnCollectionsListRequest() || app.OnCollectionsListRequest() == nil { + t.Fatalf("Getter app.OnCollectionsListRequest does not match or nil (%v vs %v)", app.OnCollectionsListRequest(), app.onCollectionsListRequest) + } + + if app.onCollectionViewRequest != app.OnCollectionViewRequest() || app.OnCollectionViewRequest() == nil { + t.Fatalf("Getter app.OnCollectionViewRequest does not match or nil (%v vs %v)", app.OnCollectionViewRequest(), app.onCollectionViewRequest) + } + + if app.onCollectionBeforeCreateRequest != app.OnCollectionBeforeCreateRequest() || app.OnCollectionBeforeCreateRequest() == nil { + t.Fatalf("Getter app.OnCollectionBeforeCreateRequest does not match or nil (%v vs %v)", app.OnCollectionBeforeCreateRequest(), app.onCollectionBeforeCreateRequest) + } + + if app.onCollectionAfterCreateRequest != app.OnCollectionAfterCreateRequest() || app.OnCollectionAfterCreateRequest() == nil { + t.Fatalf("Getter app.OnCollectionAfterCreateRequest does not match or nil (%v vs %v)", app.OnCollectionAfterCreateRequest(), app.onCollectionAfterCreateRequest) + } + + if app.onCollectionBeforeUpdateRequest != app.OnCollectionBeforeUpdateRequest() || app.OnCollectionBeforeUpdateRequest() == nil { + t.Fatalf("Getter app.OnCollectionBeforeUpdateRequest does not match or nil (%v vs %v)", app.OnCollectionBeforeUpdateRequest(), app.onCollectionBeforeUpdateRequest) + } + + if app.onCollectionAfterUpdateRequest != app.OnCollectionAfterUpdateRequest() || app.OnCollectionAfterUpdateRequest() == nil { + t.Fatalf("Getter app.OnCollectionAfterUpdateRequest does not match or nil (%v vs %v)", app.OnCollectionAfterUpdateRequest(), app.onCollectionAfterUpdateRequest) + } + + if app.onCollectionBeforeDeleteRequest != app.OnCollectionBeforeDeleteRequest() || app.OnCollectionBeforeDeleteRequest() == nil { + t.Fatalf("Getter app.OnCollectionBeforeDeleteRequest does not match or nil (%v vs %v)", app.OnCollectionBeforeDeleteRequest(), app.onCollectionBeforeDeleteRequest) + } + + if app.onCollectionAfterDeleteRequest != app.OnCollectionAfterDeleteRequest() || app.OnCollectionAfterDeleteRequest() == nil { + t.Fatalf("Getter app.OnCollectionAfterDeleteRequest does not match or nil (%v vs %v)", app.OnCollectionAfterDeleteRequest(), app.onCollectionAfterDeleteRequest) + } +} + +func TestBaseAppNewMailClient(t *testing.T) { + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := NewBaseApp(testDataDir, "pb_test_env", false) + + client1 := app.NewMailClient() + if val, ok := client1.(*mailer.Sendmail); !ok { + t.Fatalf("Expected mailer.Sendmail instance, got %v", val) + } + + app.Settings().Smtp.Enabled = true + + client2 := app.NewMailClient() + if val, ok := client2.(*mailer.SmtpClient); !ok { + t.Fatalf("Expected mailer.SmtpClient instance, got %v", val) + } +} + +func TestBaseAppNewFilesystem(t *testing.T) { + const testDataDir = "./pb_base_app_test_data_dir/" + defer os.RemoveAll(testDataDir) + + app := NewBaseApp(testDataDir, "pb_test_env", false) + + // local + local, localErr := app.NewFilesystem() + if localErr != nil { + t.Fatal(localErr) + } + if local == nil { + t.Fatal("Expected local filesystem instance, got nil") + } + + // misconfigured s3 + app.Settings().S3.Enabled = true + s3, s3Err := app.NewFilesystem() + if s3Err == nil { + t.Fatal("Expected S3 error, got nil") + } + if s3 != nil { + t.Fatalf("Expected nil s3 filesystem, got %v", s3) + } +} diff --git a/core/db_cgo.go b/core/db_cgo.go new file mode 100644 index 00000000..4ff77366 --- /dev/null +++ b/core/db_cgo.go @@ -0,0 +1,26 @@ +//go:build cgo + +package core + +import ( + "fmt" + + "github.com/pocketbase/dbx" + _ "github.com/mattn/go-sqlite3" +) + +func connectDB(dbPath string) (*dbx.DB, error) { + pragmas := "_foreign_keys=1&_journal_mode=WAL&_synchronous=NORMAL&_busy_timeout=8000" + + db, openErr := dbx.MustOpen("sqlite3", fmt.Sprintf("%s?%s", dbPath, pragmas)) + if openErr != nil { + return nil, openErr + } + + // additional pragmas not supported through the dsn string + _, err := db.NewQuery(` + pragma journal_size_limit = 100000000; + `).Execute() + + return db, err +} diff --git a/core/db_nocgo.go b/core/db_nocgo.go new file mode 100644 index 00000000..63939c4f --- /dev/null +++ b/core/db_nocgo.go @@ -0,0 +1,16 @@ +//go:build !cgo + +package core + +import ( + "fmt" + + "github.com/pocketbase/dbx" + _ "modernc.org/sqlite" +) + +func connectDB(dbPath string) (*dbx.DB, error) { + pragmas := "_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_pragma=busy_timeout(8000)&_pragma=journal_size_limit(100000000)" + + return dbx.MustOpen("sqlite", fmt.Sprintf("%s?%s", dbPath, pragmas)) +} diff --git a/core/events.go b/core/events.go new file mode 100644 index 00000000..5c3a854e --- /dev/null +++ b/core/events.go @@ -0,0 +1,230 @@ +package core + +import ( + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/mailer" + "github.com/pocketbase/pocketbase/tools/search" + "github.com/pocketbase/pocketbase/tools/subscriptions" + + "github.com/labstack/echo/v5" +) + +// ------------------------------------------------------------------- +// Serve events data +// ------------------------------------------------------------------- + +type ServeEvent struct { + App App + Router *echo.Echo +} + +// ------------------------------------------------------------------- +// Model DAO events data +// ------------------------------------------------------------------- + +type ModelEvent struct { + Dao *daos.Dao + Model models.Model +} + +// ------------------------------------------------------------------- +// Mailer events data +// ------------------------------------------------------------------- + +type MailerUserEvent struct { + MailClient mailer.Mailer + User *models.User + Meta map[string]any +} + +type MailerAdminEvent struct { + MailClient mailer.Mailer + Admin *models.Admin + Meta map[string]any +} + +// ------------------------------------------------------------------- +// Realtime API events data +// ------------------------------------------------------------------- + +type RealtimeConnectEvent struct { + HttpContext echo.Context + Client subscriptions.Client +} + +type RealtimeSubscribeEvent struct { + HttpContext echo.Context + Client subscriptions.Client + Subscriptions []string +} + +// ------------------------------------------------------------------- +// Settings API events data +// ------------------------------------------------------------------- + +type SettingsListEvent struct { + HttpContext echo.Context + RedactedSettings *Settings +} + +type SettingsUpdateEvent struct { + HttpContext echo.Context + OldSettings *Settings + NewSettings *Settings +} + +// ------------------------------------------------------------------- +// Record API events data +// ------------------------------------------------------------------- + +type RecordsListEvent struct { + HttpContext echo.Context + Collection *models.Collection + Records []*models.Record + Result *search.Result +} + +type RecordViewEvent struct { + HttpContext echo.Context + Record *models.Record +} + +type RecordCreateEvent struct { + HttpContext echo.Context + Record *models.Record +} + +type RecordUpdateEvent struct { + HttpContext echo.Context + Record *models.Record +} + +type RecordDeleteEvent struct { + HttpContext echo.Context + Record *models.Record +} + +// ------------------------------------------------------------------- +// Admin API events data +// ------------------------------------------------------------------- + +type AdminsListEvent struct { + HttpContext echo.Context + Admins []*models.Admin + Result *search.Result +} + +type AdminViewEvent struct { + HttpContext echo.Context + Admin *models.Admin +} + +type AdminCreateEvent struct { + HttpContext echo.Context + Admin *models.Admin +} + +type AdminUpdateEvent struct { + HttpContext echo.Context + Admin *models.Admin +} + +type AdminDeleteEvent struct { + HttpContext echo.Context + Admin *models.Admin +} + +type AdminAuthEvent struct { + HttpContext echo.Context + Admin *models.Admin + Token string +} + +// ------------------------------------------------------------------- +// User API events data +// ------------------------------------------------------------------- + +type UsersListEvent struct { + HttpContext echo.Context + Users []*models.User + Result *search.Result +} + +type UserViewEvent struct { + HttpContext echo.Context + User *models.User +} + +type UserCreateEvent struct { + HttpContext echo.Context + User *models.User +} + +type UserUpdateEvent struct { + HttpContext echo.Context + User *models.User +} + +type UserDeleteEvent struct { + HttpContext echo.Context + User *models.User +} + +type UserAuthEvent struct { + HttpContext echo.Context + User *models.User + Token string + Meta any +} + +type UserOauth2RegisterEvent struct { + HttpContext echo.Context + User *models.User + AuthData *auth.AuthUser +} + +// ------------------------------------------------------------------- +// Collection API events data +// ------------------------------------------------------------------- + +type CollectionsListEvent struct { + HttpContext echo.Context + Collections []*models.Collection + Result *search.Result +} + +type CollectionViewEvent struct { + HttpContext echo.Context + Collection *models.Collection +} + +type CollectionCreateEvent struct { + HttpContext echo.Context + Collection *models.Collection +} + +type CollectionUpdateEvent struct { + HttpContext echo.Context + Collection *models.Collection +} + +type CollectionDeleteEvent struct { + HttpContext echo.Context + Collection *models.Collection +} + +// ------------------------------------------------------------------- +// File API events data +// ------------------------------------------------------------------- + +type FileDownloadEvent struct { + HttpContext echo.Context + Collection *models.Collection + Record *models.Record + FileField *schema.SchemaField + ServedPath string + ServedName string +} diff --git a/core/settings.go b/core/settings.go new file mode 100644 index 00000000..881e207a --- /dev/null +++ b/core/settings.go @@ -0,0 +1,412 @@ +package core + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "sync" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/security" +) + +// Common settings placeholder tokens +const ( + EmailPlaceholderAppUrl string = "%APP_URL%" + EmailPlaceholderToken string = "%TOKEN%" +) + +// Settings defines common app configuration options. +type Settings struct { + mux sync.RWMutex + + Meta MetaConfig `form:"meta" json:"meta"` + Logs LogsConfig `form:"logs" json:"logs"` + Smtp SmtpConfig `form:"smtp" json:"smtp"` + S3 S3Config `form:"s3" json:"s3"` + AdminAuthToken TokenConfig `form:"adminAuthToken" json:"adminAuthToken"` + AdminPasswordResetToken TokenConfig `form:"adminPasswordResetToken" json:"adminPasswordResetToken"` + UserAuthToken TokenConfig `form:"userAuthToken" json:"userAuthToken"` + UserPasswordResetToken TokenConfig `form:"userPasswordResetToken" json:"userPasswordResetToken"` + UserEmailChangeToken TokenConfig `form:"userEmailChangeToken" json:"userEmailChangeToken"` + UserVerificationToken TokenConfig `form:"userVerificationToken" json:"userVerificationToken"` + EmailAuth EmailAuthConfig `form:"emailAuth" json:"emailAuth"` + GoogleAuth AuthProviderConfig `form:"googleAuth" json:"googleAuth"` + FacebookAuth AuthProviderConfig `form:"facebookAuth" json:"facebookAuth"` + GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"` + GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"` +} + +// NewSettings creates and returns a new default Settings instance. +func NewSettings() *Settings { + return &Settings{ + Meta: MetaConfig{ + AppName: "Acme", + AppUrl: "http://localhost:8090", + SenderName: "Support", + SenderAddress: "support@example.com", + UserVerificationUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-verification/" + EmailPlaceholderToken, + UserResetPasswordUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-password-reset/" + EmailPlaceholderToken, + UserConfirmEmailChangeUrl: EmailPlaceholderAppUrl + "/_/#/users/confirm-email-change/" + EmailPlaceholderToken, + }, + Logs: LogsConfig{ + MaxDays: 7, + }, + Smtp: SmtpConfig{ + Enabled: false, + Host: "smtp.example.com", + Port: 587, + Username: "", + Password: "", + Tls: false, + }, + AdminAuthToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 1209600, // 14 days, + }, + AdminPasswordResetToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 1800, // 30 minutes, + }, + UserAuthToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 1209600, // 14 days, + }, + UserPasswordResetToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 1800, // 30 minutes, + }, + UserVerificationToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 604800, // 7 days, + }, + UserEmailChangeToken: TokenConfig{ + Secret: security.RandomString(50), + Duration: 1800, // 30 minutes, + }, + EmailAuth: EmailAuthConfig{ + Enabled: true, + MinPasswordLength: 8, + }, + GoogleAuth: AuthProviderConfig{ + Enabled: false, + AllowRegistrations: true, + }, + FacebookAuth: AuthProviderConfig{ + Enabled: false, + AllowRegistrations: true, + }, + GithubAuth: AuthProviderConfig{ + Enabled: false, + AllowRegistrations: true, + }, + GitlabAuth: AuthProviderConfig{ + Enabled: false, + AllowRegistrations: true, + }, + } +} + +// Validate makes Settings validatable by implementing [validation.Validatable] interface. +func (s *Settings) Validate() error { + s.mux.Lock() + defer s.mux.Unlock() + + return validation.ValidateStruct(s, + validation.Field(&s.Meta), + validation.Field(&s.Logs), + validation.Field(&s.AdminAuthToken), + validation.Field(&s.AdminPasswordResetToken), + validation.Field(&s.UserAuthToken), + validation.Field(&s.UserPasswordResetToken), + validation.Field(&s.UserEmailChangeToken), + validation.Field(&s.UserVerificationToken), + validation.Field(&s.Smtp), + validation.Field(&s.S3), + validation.Field(&s.EmailAuth), + validation.Field(&s.GoogleAuth), + validation.Field(&s.FacebookAuth), + validation.Field(&s.GithubAuth), + validation.Field(&s.GitlabAuth), + ) +} + +// Merge merges `other` settings into the current one. +func (s *Settings) Merge(other *Settings) error { + s.mux.Lock() + defer s.mux.Unlock() + + bytes, err := json.Marshal(other) + if err != nil { + return err + } + + return json.Unmarshal(bytes, s) +} + +// Clone creates a new deep copy of the current settings. +func (c *Settings) Clone() (*Settings, error) { + new := &Settings{} + if err := new.Merge(c); err != nil { + return nil, err + } + return new, nil +} + +// RedactClone creates a new deep copy of the current settings, +// while replacing the secret values with `******`. +func (s *Settings) RedactClone() (*Settings, error) { + clone, err := s.Clone() + if err != nil { + return nil, err + } + + mask := "******" + + sensitiveFields := []*string{ + &clone.Smtp.Password, + &clone.S3.Secret, + &clone.AdminAuthToken.Secret, + &clone.AdminPasswordResetToken.Secret, + &clone.UserAuthToken.Secret, + &clone.UserPasswordResetToken.Secret, + &clone.UserEmailChangeToken.Secret, + &clone.UserVerificationToken.Secret, + &clone.GoogleAuth.ClientSecret, + &clone.FacebookAuth.ClientSecret, + &clone.GithubAuth.ClientSecret, + &clone.GitlabAuth.ClientSecret, + } + + // mask all sensitive fields + for _, v := range sensitiveFields { + if v != nil && *v != "" { + *v = mask + } + } + + return clone, nil +} + +// NamedAuthProviderConfigs returns a map with all registered OAuth2 +// provider configurations (indexed by their name identifier). +func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig { + return map[string]AuthProviderConfig{ + auth.NameGoogle: s.GoogleAuth, + auth.NameFacebook: s.FacebookAuth, + auth.NameGithub: s.GithubAuth, + auth.NameGitlab: s.GitlabAuth, + } +} + +// ------------------------------------------------------------------- + +type TokenConfig struct { + Secret string `form:"secret" json:"secret"` + Duration int64 `form:"duration" json:"duration"` +} + +// Validate makes TokenConfig validatable by implementing [validation.Validatable] interface. +func (c TokenConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Secret, validation.Required, validation.Length(30, 300)), + validation.Field(&c.Duration, validation.Required, validation.Min(5), validation.Max(31536000)), + ) +} + +// ------------------------------------------------------------------- + +type SmtpConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + Host string `form:"host" json:"host"` + Port int `form:"port" json:"port"` + Username string `form:"username" json:"username"` + Password string `form:"password" json:"password"` + + // Whether to enforce TLS encryption for the mail server connection. + // + // When set to false StartTLS command is send, leaving the server + // to decide whether to upgrade the connection or not. + Tls bool `form:"tls" json:"tls"` +} + +// Validate makes SmtpConfig validatable by implementing [validation.Validatable] interface. +func (c SmtpConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Host, is.Host, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.Port, validation.When(c.Enabled, validation.Required), validation.Min(0)), + ) +} + +// ------------------------------------------------------------------- + +type S3Config struct { + Enabled bool `form:"enabled" json:"enabled"` + Bucket string `form:"bucket" json:"bucket"` + Region string `form:"region" json:"region"` + Endpoint string `form:"endpoint" json:"endpoint"` + AccessKey string `form:"accessKey" json:"accessKey"` + Secret string `form:"secret" json:"secret"` +} + +// Validate makes S3Config validatable by implementing [validation.Validatable] interface. +func (c S3Config) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.Endpoint, is.Host, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.Bucket, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.Region, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.AccessKey, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.Secret, validation.When(c.Enabled, validation.Required)), + ) +} + +// ------------------------------------------------------------------- + +type MetaConfig struct { + AppName string `form:"appName" json:"appName"` + AppUrl string `form:"appUrl" json:"appUrl"` + SenderName string `form:"senderName" json:"senderName"` + SenderAddress string `form:"senderAddress" json:"senderAddress"` + UserVerificationUrl string `form:"userVerificationUrl" json:"userVerificationUrl"` + UserResetPasswordUrl string `form:"userResetPasswordUrl" json:"userResetPasswordUrl"` + UserConfirmEmailChangeUrl string `form:"userConfirmEmailChangeUrl" json:"userConfirmEmailChangeUrl"` +} + +// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface. +func (c MetaConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)), + validation.Field(&c.AppUrl, validation.Required, is.URL), + validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)), + validation.Field(&c.SenderAddress, is.Email, validation.Required), + validation.Field( + &c.UserVerificationUrl, + validation.Required, + validation.By(c.checkPlaceholders(EmailPlaceholderToken)), + ), + validation.Field( + &c.UserResetPasswordUrl, + validation.Required, + validation.By(c.checkPlaceholders(EmailPlaceholderToken)), + ), + validation.Field( + &c.UserConfirmEmailChangeUrl, + validation.Required, + validation.By(c.checkPlaceholders(EmailPlaceholderToken)), + ), + ) +} + +func (c *MetaConfig) checkPlaceholders(params ...string) validation.RuleFunc { + return func(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + for _, param := range params { + if !strings.Contains(v, param) { + return validation.NewError("validation_missing_required_param", fmt.Sprintf("Missing required parameter %q", param)) + } + } + + return nil + } +} + +// ------------------------------------------------------------------- + +type LogsConfig struct { + MaxDays int `form:"maxDays" json:"maxDays"` +} + +// Validate makes LogsConfig validatable by implementing [validation.Validatable] interface. +func (c LogsConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.MaxDays, validation.Min(0)), + ) +} + +// ------------------------------------------------------------------- + +type AuthProviderConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + AllowRegistrations bool `form:"allowRegistrations" json:"allowRegistrations"` + ClientId string `form:"clientId" json:"clientId,omitempty"` + ClientSecret string `form:"clientSecret" json:"clientSecret,omitempty"` + AuthUrl string `form:"authUrl" json:"authUrl,omitempty"` + TokenUrl string `form:"tokenUrl" json:"tokenUrl,omitempty"` + UserApiUrl string `form:"userApiUrl" json:"userApiUrl,omitempty"` +} + +// Validate makes `ProviderConfig` validatable by implementing [validation.Validatable] interface. +func (c AuthProviderConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field(&c.ClientId, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.ClientSecret, validation.When(c.Enabled, validation.Required)), + validation.Field(&c.AuthUrl, is.URL), + validation.Field(&c.TokenUrl, is.URL), + validation.Field(&c.UserApiUrl, is.URL), + ) +} + +// SetupProvider loads the current AuthProviderConfig into the specified provider. +func (c AuthProviderConfig) SetupProvider(provider auth.Provider) error { + if !c.Enabled { + return errors.New("The provider is not enabled.") + } + + if c.ClientId != "" { + provider.SetClientId(string(c.ClientId)) + } + + if c.ClientSecret != "" { + provider.SetClientSecret(string(c.ClientSecret)) + } + + if c.AuthUrl != "" { + provider.SetAuthUrl(c.AuthUrl) + } + + if c.UserApiUrl != "" { + provider.SetUserApiUrl(c.UserApiUrl) + } + + if c.TokenUrl != "" { + provider.SetTokenUrl(c.TokenUrl) + } + + return nil +} + +// ------------------------------------------------------------------- + +type EmailAuthConfig struct { + Enabled bool `form:"enabled" json:"enabled"` + ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` + OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` + MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"` +} + +// Validate makes `EmailAuthConfig` validatable by implementing [validation.Validatable] interface. +func (c EmailAuthConfig) Validate() error { + return validation.ValidateStruct(&c, + validation.Field( + &c.ExceptDomains, + validation.When(len(c.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + validation.Field( + &c.OnlyDomains, + validation.When(len(c.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + validation.Field( + &c.MinPasswordLength, + validation.When(c.Enabled, validation.Required), + validation.Min(5), + validation.Max(100), + ), + ) +} diff --git a/core/settings_test.go b/core/settings_test.go new file mode 100644 index 00000000..a5c25a49 --- /dev/null +++ b/core/settings_test.go @@ -0,0 +1,606 @@ +package core_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/auth" +) + +func TestSettingsValidate(t *testing.T) { + s := core.NewSettings() + + // set invalid settings data + s.Meta.AppName = "" + s.Logs.MaxDays = -10 + s.Smtp.Enabled = true + s.Smtp.Host = "" + s.S3.Enabled = true + s.S3.Endpoint = "invalid" + s.AdminAuthToken.Duration = -10 + s.AdminPasswordResetToken.Duration = -10 + s.UserAuthToken.Duration = -10 + s.UserPasswordResetToken.Duration = -10 + s.UserEmailChangeToken.Duration = -10 + s.UserVerificationToken.Duration = -10 + s.EmailAuth.Enabled = true + s.EmailAuth.MinPasswordLength = -10 + s.GoogleAuth.Enabled = true + s.GoogleAuth.ClientId = "" + s.FacebookAuth.Enabled = true + s.FacebookAuth.ClientId = "" + s.GithubAuth.Enabled = true + s.GithubAuth.ClientId = "" + s.GitlabAuth.Enabled = true + s.GitlabAuth.ClientId = "" + + // check if Validate() is triggering the members validate methods. + err := s.Validate() + if err == nil { + t.Fatalf("Expected error, got nil") + } + + expectations := []string{ + `"meta":{`, + `"logs":{`, + `"smtp":{`, + `"s3":{`, + `"adminAuthToken":{`, + `"adminPasswordResetToken":{`, + `"userAuthToken":{`, + `"userPasswordResetToken":{`, + `"userEmailChangeToken":{`, + `"userVerificationToken":{`, + `"emailAuth":{`, + `"googleAuth":{`, + `"facebookAuth":{`, + `"githubAuth":{`, + `"gitlabAuth":{`, + } + + errBytes, _ := json.Marshal(err) + jsonErr := string(errBytes) + for _, expected := range expectations { + if !strings.Contains(jsonErr, expected) { + t.Errorf("Expected error key %s in %v", expected, jsonErr) + } + } +} + +func TestSettingsMerge(t *testing.T) { + s1 := core.NewSettings() + s1.Meta.AppUrl = "old_app_url" + + s2 := core.NewSettings() + s2.Meta.AppName = "test" + s2.Logs.MaxDays = 123 + s2.Smtp.Host = "test" + s2.Smtp.Enabled = true + s2.S3.Enabled = true + s2.S3.Endpoint = "test" + s2.AdminAuthToken.Duration = 1 + s2.AdminPasswordResetToken.Duration = 2 + s2.UserAuthToken.Duration = 3 + s2.UserPasswordResetToken.Duration = 4 + s2.UserEmailChangeToken.Duration = 5 + s2.UserVerificationToken.Duration = 6 + s2.EmailAuth.Enabled = false + s2.EmailAuth.MinPasswordLength = 30 + s2.GoogleAuth.Enabled = true + s2.GoogleAuth.ClientId = "google_test" + s2.FacebookAuth.Enabled = true + s2.FacebookAuth.ClientId = "facebook_test" + s2.GithubAuth.Enabled = true + s2.GithubAuth.ClientId = "github_test" + s2.GitlabAuth.Enabled = true + s2.GitlabAuth.ClientId = "gitlab_test" + + if err := s1.Merge(s2); err != nil { + t.Fatal(err) + } + + s1Encoded, err := json.Marshal(s1) + if err != nil { + t.Fatal(err) + } + + s2Encoded, err := json.Marshal(s2) + if err != nil { + t.Fatal(err) + } + + if string(s1Encoded) != string(s2Encoded) { + t.Fatalf("Expected the same serialization, got %v VS %v", string(s1Encoded), string(s2Encoded)) + } +} + +func TestSettingsClone(t *testing.T) { + s1 := core.NewSettings() + + s2, err := s1.Clone() + if err != nil { + t.Fatal(err) + } + + s1Bytes, err := json.Marshal(s1) + if err != nil { + t.Fatal(err) + } + + s2Bytes, err := json.Marshal(s2) + if err != nil { + t.Fatal(err) + } + + if string(s1Bytes) != string(s2Bytes) { + t.Fatalf("Expected equivalent serialization, got %v VS %v", string(s1Bytes), string(s2Bytes)) + } + + // verify that it is a deep copy + s1.Meta.AppName = "new" + if s1.Meta.AppName == s2.Meta.AppName { + t.Fatalf("Expected s1 and s2 to have different Meta.AppName, got %s", s1.Meta.AppName) + } +} + +func TestSettingsRedactClone(t *testing.T) { + s1 := core.NewSettings() + s1.Meta.AppName = "test123" // control field + s1.Smtp.Password = "test123" + s1.Smtp.Tls = true + s1.S3.Secret = "test123" + s1.AdminAuthToken.Secret = "test123" + s1.AdminPasswordResetToken.Secret = "test123" + s1.UserAuthToken.Secret = "test123" + s1.UserPasswordResetToken.Secret = "test123" + s1.UserEmailChangeToken.Secret = "test123" + s1.UserVerificationToken.Secret = "test123" + s1.GoogleAuth.ClientSecret = "test123" + s1.FacebookAuth.ClientSecret = "test123" + s1.GithubAuth.ClientSecret = "test123" + s1.GitlabAuth.ClientSecret = "test123" + + s2, err := s1.RedactClone() + if err != nil { + t.Fatal(err) + } + + encoded, err := json.Marshal(s2) + if err != nil { + t.Fatal(err) + } + + expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","senderName":"Support","senderAddress":"support@example.com","userVerificationUrl":"%APP_URL%/_/#/users/confirm-verification/%TOKEN%","userResetPasswordUrl":"%APP_URL%/_/#/users/confirm-password-reset/%TOKEN%","userConfirmEmailChangeUrl":"%APP_URL%/_/#/users/confirm-email-change/%TOKEN%"},"logs":{"maxDays":7},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******"},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"userAuthToken":{"secret":"******","duration":1209600},"userPasswordResetToken":{"secret":"******","duration":1800},"userEmailChangeToken":{"secret":"******","duration":1800},"userVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":true,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":8},"googleAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"facebookAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"githubAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"}}` + + if encodedStr := string(encoded); encodedStr != expected { + t.Fatalf("Expected %v, got \n%v", expected, encodedStr) + } +} + +func TestNamedAuthProviderConfigs(t *testing.T) { + s := core.NewSettings() + + s.GoogleAuth.ClientId = "google_test" + s.FacebookAuth.ClientId = "facebook_test" + s.GithubAuth.ClientId = "github_test" + s.GitlabAuth.ClientId = "gitlab_test" + s.GitlabAuth.Enabled = true + + result := s.NamedAuthProviderConfigs() + + encoded, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + + expected := `{"facebook":{"enabled":false,"allowRegistrations":true,"clientId":"facebook_test"},"github":{"enabled":false,"allowRegistrations":true,"clientId":"github_test"},"gitlab":{"enabled":true,"allowRegistrations":true,"clientId":"gitlab_test"},"google":{"enabled":false,"allowRegistrations":true,"clientId":"google_test"}}` + + if encodedStr := string(encoded); encodedStr != expected { + t.Fatalf("Expected the same serialization, got %v", encodedStr) + } +} + +func TestTokenConfigValidate(t *testing.T) { + scenarios := []struct { + config core.TokenConfig + expectError bool + }{ + // zero values + { + core.TokenConfig{}, + true, + }, + // invalid data + { + core.TokenConfig{ + Secret: "test", + Duration: 4, + }, + true, + }, + // valid data + { + core.TokenConfig{ + Secret: "testtesttesttesttesttesttestte", + Duration: 100, + }, + false, + }, + } + + for i, scenario := range scenarios { + result := scenario.config.Validate() + + if result != nil && !scenario.expectError { + t.Errorf("(%d) Didn't expect error, got %v", i, result) + } + + if result == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} + +func TestSmtpConfigValidate(t *testing.T) { + scenarios := []struct { + config core.SmtpConfig + expectError bool + }{ + // zero values (disabled) + { + core.SmtpConfig{}, + false, + }, + // zero values (enabled) + { + core.SmtpConfig{Enabled: true}, + true, + }, + // invalid data + { + core.SmtpConfig{ + Enabled: true, + Host: "test:test:test", + Port: -10, + }, + true, + }, + // valid data + { + core.SmtpConfig{ + Enabled: true, + Host: "example.com", + Port: 100, + Tls: true, + }, + false, + }, + } + + for i, scenario := range scenarios { + result := scenario.config.Validate() + + if result != nil && !scenario.expectError { + t.Errorf("(%d) Didn't expect error, got %v", i, result) + } + + if result == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} + +func TestS3ConfigValidate(t *testing.T) { + scenarios := []struct { + config core.S3Config + expectError bool + }{ + // zero values (disabled) + { + core.S3Config{}, + false, + }, + // zero values (enabled) + { + core.S3Config{Enabled: true}, + true, + }, + // invalid data + { + core.S3Config{ + Enabled: true, + Endpoint: "test:test:test", + }, + true, + }, + // valid data + { + core.S3Config{ + Enabled: true, + Endpoint: "example.com", + Bucket: "test", + Region: "test", + AccessKey: "test", + Secret: "test", + }, + false, + }, + } + + for i, scenario := range scenarios { + result := scenario.config.Validate() + + if result != nil && !scenario.expectError { + t.Errorf("(%d) Didn't expect error, got %v", i, result) + } + + if result == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} + +func TestMetaConfigValidate(t *testing.T) { + scenarios := []struct { + config core.MetaConfig + expectError bool + }{ + // zero values + { + core.MetaConfig{}, + true, + }, + // invalid data + { + core.MetaConfig{ + AppName: strings.Repeat("a", 300), + AppUrl: "test", + SenderName: strings.Repeat("a", 300), + SenderAddress: "invalid_email", + UserVerificationUrl: "test", + UserResetPasswordUrl: "test", + UserConfirmEmailChangeUrl: "test", + }, + true, + }, + // invalid data (missing required placeholders) + { + core.MetaConfig{ + AppName: "test", + AppUrl: "https://example.com", + SenderName: "test", + SenderAddress: "test@example.com", + UserVerificationUrl: "https://example.com", + UserResetPasswordUrl: "https://example.com", + UserConfirmEmailChangeUrl: "https://example.com", + }, + true, + }, + // valid data + { + core.MetaConfig{ + AppName: "test", + AppUrl: "https://example.com", + SenderName: "test", + SenderAddress: "test@example.com", + UserVerificationUrl: "https://example.com/" + core.EmailPlaceholderToken, + UserResetPasswordUrl: "https://example.com/" + core.EmailPlaceholderToken, + UserConfirmEmailChangeUrl: "https://example.com/" + core.EmailPlaceholderToken, + }, + false, + }, + } + + for i, scenario := range scenarios { + result := scenario.config.Validate() + + if result != nil && !scenario.expectError { + t.Errorf("(%d) Didn't expect error, got %v", i, result) + } + + if result == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} + +func TestLogsConfigValidate(t *testing.T) { + scenarios := []struct { + config core.LogsConfig + expectError bool + }{ + // zero values + { + core.LogsConfig{}, + false, + }, + // invalid data + { + core.LogsConfig{MaxDays: -10}, + true, + }, + // valid data + { + core.LogsConfig{MaxDays: 1}, + false, + }, + } + + for i, scenario := range scenarios { + result := scenario.config.Validate() + + if result != nil && !scenario.expectError { + t.Errorf("(%d) Didn't expect error, got %v", i, result) + } + + if result == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} + +func TestAuthProviderConfigValidate(t *testing.T) { + scenarios := []struct { + config core.AuthProviderConfig + expectError bool + }{ + // zero values (disabled) + { + core.AuthProviderConfig{}, + false, + }, + // zero values (enabled) + { + core.AuthProviderConfig{Enabled: true}, + true, + }, + // invalid data + { + core.AuthProviderConfig{ + Enabled: true, + ClientId: "", + ClientSecret: "", + AuthUrl: "test", + TokenUrl: "test", + UserApiUrl: "test", + }, + true, + }, + // valid data (only the required) + { + core.AuthProviderConfig{ + Enabled: true, + ClientId: "test", + ClientSecret: "test", + }, + false, + }, + // valid data (fill all fields) + { + core.AuthProviderConfig{ + Enabled: true, + ClientId: "test", + ClientSecret: "test", + AuthUrl: "https://example.com", + TokenUrl: "https://example.com", + UserApiUrl: "https://example.com", + }, + false, + }, + } + + for i, scenario := range scenarios { + result := scenario.config.Validate() + + if result != nil && !scenario.expectError { + t.Errorf("(%d) Didn't expect error, got %v", i, result) + } + + if result == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} + +func TestAuthProviderConfigSetupProvider(t *testing.T) { + provider := auth.NewGithubProvider() + + // disabled config + c1 := core.AuthProviderConfig{Enabled: false} + if err := c1.SetupProvider(provider); err == nil { + t.Errorf("Expected error, got nil") + } + + c2 := core.AuthProviderConfig{ + Enabled: true, + ClientId: "test_ClientId", + ClientSecret: "test_ClientSecret", + AuthUrl: "test_AuthUrl", + UserApiUrl: "test_UserApiUrl", + TokenUrl: "test_TokenUrl", + } + if err := c2.SetupProvider(provider); err != nil { + t.Error(err) + } + encoded, _ := json.Marshal(c2) + expected := `{"enabled":true,"allowRegistrations":false,"clientId":"test_ClientId","clientSecret":"test_ClientSecret","authUrl":"test_AuthUrl","tokenUrl":"test_TokenUrl","userApiUrl":"test_UserApiUrl"}` + if string(encoded) != expected { + t.Errorf("Expected %s, got %s", expected, string(encoded)) + } +} + +func TestEmailAuthConfigValidate(t *testing.T) { + scenarios := []struct { + config core.EmailAuthConfig + expectError bool + }{ + // zero values (disabled) + { + core.EmailAuthConfig{}, + false, + }, + // zero values (enabled) + { + core.EmailAuthConfig{Enabled: true}, + true, + }, + // invalid data (only the required) + { + core.EmailAuthConfig{ + Enabled: true, + MinPasswordLength: 4, + }, + true, + }, + // valid data (only the required) + { + core.EmailAuthConfig{ + Enabled: true, + MinPasswordLength: 5, + }, + false, + }, + // invalid data (both OnlyDomains and ExceptDomains set) + { + core.EmailAuthConfig{ + Enabled: true, + MinPasswordLength: 5, + OnlyDomains: []string{"example.com", "test.com"}, + ExceptDomains: []string{"example.com", "test.com"}, + }, + true, + }, + // valid data (only onlyDomains set) + { + core.EmailAuthConfig{ + Enabled: true, + MinPasswordLength: 5, + OnlyDomains: []string{"example.com", "test.com"}, + }, + false, + }, + // valid data (only exceptDomains set) + { + core.EmailAuthConfig{ + Enabled: true, + MinPasswordLength: 5, + ExceptDomains: []string{"example.com", "test.com"}, + }, + false, + }, + } + + for i, scenario := range scenarios { + result := scenario.config.Validate() + + if result != nil && !scenario.expectError { + t.Errorf("(%d) Didn't expect error, got %v", i, result) + } + + if result == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} diff --git a/daos/admin.go b/daos/admin.go new file mode 100644 index 00000000..13b196df --- /dev/null +++ b/daos/admin.go @@ -0,0 +1,124 @@ +package daos + +import ( + "errors" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/security" +) + +// AdminQuery returns a new Admin select query. +func (dao *Dao) AdminQuery() *dbx.SelectQuery { + return dao.ModelQuery(&models.Admin{}) +} + +// FindAdminById finds the admin with the provided id. +func (dao *Dao) FindAdminById(id string) (*models.Admin, error) { + model := &models.Admin{} + + err := dao.AdminQuery(). + AndWhere(dbx.HashExp{"id": id}). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + return model, nil +} + +// FindAdminByEmail finds the admin with the provided email address. +func (dao *Dao) FindAdminByEmail(email string) (*models.Admin, error) { + model := &models.Admin{} + + err := dao.AdminQuery(). + AndWhere(dbx.HashExp{"email": email}). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + return model, nil +} + +// FindAdminByEmail finds the admin associated with the provided JWT token. +// +// Returns an error if the JWT token is invalid or expired. +func (dao *Dao) FindAdminByToken(token string, baseTokenKey string) (*models.Admin, error) { + unverifiedClaims, err := security.ParseUnverifiedJWT(token) + if err != nil { + return nil, err + } + + // check required claims + id, _ := unverifiedClaims["id"].(string) + if id == "" { + return nil, errors.New("Missing or invalid token claims.") + } + + admin, err := dao.FindAdminById(id) + if err != nil || admin == nil { + return nil, err + } + + verificationKey := admin.TokenKey + baseTokenKey + + // verify token signature + if _, err := security.ParseJWT(token, verificationKey); err != nil { + return nil, err + } + + return admin, nil +} + +// TotalAdmins returns the number of existing admin records. +func (dao *Dao) TotalAdmins() (int, error) { + var total int + + err := dao.AdminQuery().Select("count(*)").Row(&total) + + return total, err +} + +// IsAdminEmailUnique checks if the provided email address is not +// already in use by other admins. +func (dao *Dao) IsAdminEmailUnique(email string, excludeId string) bool { + if email == "" { + return false + } + + var exists bool + err := dao.AdminQuery(). + Select("count(*)"). + AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})). + AndWhere(dbx.HashExp{"email": email}). + Limit(1). + Row(&exists) + + return err == nil && !exists +} + +// DeleteAdmin deletes the provided Admin model. +// +// Returns an error if there is only 1 admin. +func (dao *Dao) DeleteAdmin(admin *models.Admin) error { + total, err := dao.TotalAdmins() + if err != nil { + return err + } + + if total == 1 { + return errors.New("You cannot delete the only existing admin.") + } + + return dao.Delete(admin) +} + +// SaveAdmin upserts the provided Admin model. +func (dao *Dao) SaveAdmin(admin *models.Admin) error { + return dao.Save(admin) +} diff --git a/daos/admin_test.go b/daos/admin_test.go new file mode 100644 index 00000000..4f56b7e5 --- /dev/null +++ b/daos/admin_test.go @@ -0,0 +1,238 @@ +package daos_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" +) + +func TestAdminQuery(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + expected := "SELECT {{_admins}}.* FROM `_admins`" + + sql := app.Dao().AdminQuery().Build().SQL() + if sql != expected { + t.Errorf("Expected sql %s, got %s", expected, sql) + } +} + +func TestFindAdminById(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + id string + expectError bool + }{ + {"00000000-2b4a-a26b-4d01-42d3c3d77bc8", true}, + {"3f8397cc-2b4a-a26b-4d01-42d3c3d77bc8", false}, + } + + for i, scenario := range scenarios { + admin, err := app.Dao().FindAdminById(scenario.id) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if admin != nil && admin.Id != scenario.id { + t.Errorf("(%d) Expected admin with id %s, got %s", i, scenario.id, admin.Id) + } + } +} + +func TestFindAdminByEmail(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + email string + expectError bool + }{ + {"invalid", true}, + {"missing@example.com", true}, + {"test@example.com", false}, + } + + for i, scenario := range scenarios { + admin, err := app.Dao().FindAdminByEmail(scenario.email) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + continue + } + + if !scenario.expectError && admin.Email != scenario.email { + t.Errorf("(%d) Expected admin with email %s, got %s", i, scenario.email, admin.Email) + } + } +} + +func TestFindAdminByToken(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + token string + baseKey string + expectedEmail string + expectError bool + }{ + // invalid base key (password reset key for auth token) + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + app.Settings().AdminPasswordResetToken.Secret, + "", + true, + }, + // expired token + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MDk5MTY2MX0.uXZ_ywsZeRFSvDNQ9zBoYUXKXw7VEr48Fzx-E06OkS8", + app.Settings().AdminAuthToken.Secret, + "", + true, + }, + // valid token + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg3MzQ2Mjc5Mn0.AtRtXR6FHBrCUGkj5OffhmxLbSZaQ4L_Qgw4gfoHyfo", + app.Settings().AdminAuthToken.Secret, + "test@example.com", + false, + }, + } + + for i, scenario := range scenarios { + admin, err := app.Dao().FindAdminByToken(scenario.token, scenario.baseKey) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + continue + } + + if !scenario.expectError && admin.Email != scenario.expectedEmail { + t.Errorf("(%d) Expected admin model %s, got %s", i, scenario.expectedEmail, admin.Email) + } + } +} + +func TestTotalAdmins(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + result1, err := app.Dao().TotalAdmins() + if err != nil { + t.Fatal(err) + } + if result1 != 2 { + t.Fatalf("Expected 2 admins, got %d", result1) + } + + // delete all + app.Dao().DB().NewQuery("delete from {{_admins}}").Execute() + + result2, err := app.Dao().TotalAdmins() + if err != nil { + t.Fatal(err) + } + if result2 != 0 { + t.Fatalf("Expected 0 admins, got %d", result2) + } +} + +func TestIsAdminEmailUnique(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + email string + excludeId string + expected bool + }{ + {"", "", false}, + {"test@example.com", "", false}, + {"new@example.com", "", true}, + {"test@example.com", "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", true}, + } + + for i, scenario := range scenarios { + result := app.Dao().IsAdminEmailUnique(scenario.email, scenario.excludeId) + if result != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) + } + } +} + +func TestDeleteAdmin(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // try to delete unsaved admin model + deleteErr0 := app.Dao().DeleteAdmin(&models.Admin{}) + if deleteErr0 == nil { + t.Fatal("Expected error, got nil") + } + + admin1, err := app.Dao().FindAdminByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + admin2, err := app.Dao().FindAdminByEmail("test2@example.com") + if err != nil { + t.Fatal(err) + } + + deleteErr1 := app.Dao().DeleteAdmin(admin1) + if deleteErr1 != nil { + t.Fatal(deleteErr1) + } + + // cannot delete the only remaining admin + deleteErr2 := app.Dao().DeleteAdmin(admin2) + if deleteErr2 == nil { + t.Fatal("Expected delete error, got nil") + } + + total, _ := app.Dao().TotalAdmins() + if total != 1 { + t.Fatalf("Expected only 1 admin, got %d", total) + } +} + +func TestSaveAdmin(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create + newAdmin := &models.Admin{} + newAdmin.Email = "new@example.com" + newAdmin.SetPassword("123456") + saveErr1 := app.Dao().SaveAdmin(newAdmin) + if saveErr1 != nil { + t.Fatal(saveErr1) + } + if newAdmin.Id == "" { + t.Fatal("Expected admin id to be set") + } + + // update + existingAdmin, err := app.Dao().FindAdminByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + updatedEmail := "test_update@example.com" + existingAdmin.Email = updatedEmail + saveErr2 := app.Dao().SaveAdmin(existingAdmin) + if saveErr2 != nil { + t.Fatal(saveErr2) + } + existingAdmin, _ = app.Dao().FindAdminById(existingAdmin.Id) + if existingAdmin.Email != updatedEmail { + t.Fatalf("Expected admin email to be %s, got %s", updatedEmail, existingAdmin.Email) + } +} diff --git a/daos/base.go b/daos/base.go new file mode 100644 index 00000000..5c0a2bf5 --- /dev/null +++ b/daos/base.go @@ -0,0 +1,217 @@ +// Package daos handles common PocketBase DB model manipulations. +// +// Think of daos as DB repository and service layer in one. +package daos + +import ( + "errors" + "fmt" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" +) + +// New creates a new Dao instance with the provided db builder. +func New(db dbx.Builder) *Dao { + return &Dao{ + db: db, + } +} + +// Dao handles various db operations. +// Think of Dao as a repository and service layer in one. +type Dao struct { + db dbx.Builder + + BeforeCreateFunc func(eventDao *Dao, m models.Model) error + AfterCreateFunc func(eventDao *Dao, m models.Model) + BeforeUpdateFunc func(eventDao *Dao, m models.Model) error + AfterUpdateFunc func(eventDao *Dao, m models.Model) + BeforeDeleteFunc func(eventDao *Dao, m models.Model) error + AfterDeleteFunc func(eventDao *Dao, m models.Model) +} + +// DB returns the internal db builder (*dbx.DB or *dbx.TX). +func (dao *Dao) DB() dbx.Builder { + return dao.db +} + +// ModelQuery creates a new query with preset Select and From fields +// based on the provided model argument. +func (dao *Dao) ModelQuery(m models.Model) *dbx.SelectQuery { + tableName := m.TableName() + return dao.db.Select(fmt.Sprintf("{{%s}}.*", tableName)).From(tableName) +} + +// FindById finds a single db record with the specified id and +// scans the result into m. +func (dao *Dao) FindById(m models.Model, id string) error { + return dao.ModelQuery(m).Where(dbx.HashExp{"id": id}).Limit(1).One(m) +} + +// RunInTransaction wraps fn into a transaction. +// +// It is safe to nest RunInTransaction calls. +func (dao *Dao) RunInTransaction(fn func(txDao *Dao) error) error { + switch txOrDB := dao.db.(type) { + case *dbx.Tx: + // nested transactions are not supported by default + // so execute the function within the current transaction + return fn(dao) + case *dbx.DB: + return txOrDB.Transactional(func(tx *dbx.Tx) error { + txDao := New(tx) + + txDao.BeforeCreateFunc = func(eventDao *Dao, m models.Model) error { + if dao.BeforeCreateFunc != nil { + return dao.BeforeCreateFunc(eventDao, m) + } + return nil + } + txDao.AfterCreateFunc = func(eventDao *Dao, m models.Model) { + if dao.AfterCreateFunc != nil { + dao.AfterCreateFunc(eventDao, m) + } + } + txDao.BeforeUpdateFunc = func(eventDao *Dao, m models.Model) error { + if dao.BeforeUpdateFunc != nil { + return dao.BeforeUpdateFunc(eventDao, m) + } + return nil + } + txDao.AfterUpdateFunc = func(eventDao *Dao, m models.Model) { + if dao.AfterUpdateFunc != nil { + dao.AfterUpdateFunc(eventDao, m) + } + } + txDao.BeforeDeleteFunc = func(eventDao *Dao, m models.Model) error { + if dao.BeforeDeleteFunc != nil { + return dao.BeforeDeleteFunc(eventDao, m) + } + return nil + } + txDao.AfterDeleteFunc = func(eventDao *Dao, m models.Model) { + if dao.AfterDeleteFunc != nil { + dao.AfterDeleteFunc(eventDao, m) + } + } + + return fn(txDao) + }) + } + + return errors.New("Failed to start transaction (unknown dao.db)") +} + +// Delete deletes the provided model. +func (dao *Dao) Delete(m models.Model) error { + if !m.HasId() { + return errors.New("ID is not set") + } + + if dao.BeforeDeleteFunc != nil { + if err := dao.BeforeDeleteFunc(dao, m); err != nil { + return err + } + } + + deleteErr := dao.db.Model(m).Delete() + if deleteErr != nil { + return deleteErr + } + + if dao.AfterDeleteFunc != nil { + dao.AfterDeleteFunc(dao, m) + } + + return nil +} + +// Save upserts (update or create if primary key is not set) the provided model. +func (dao *Dao) Save(m models.Model) error { + if m.HasId() { + return dao.update(m) + } + + return dao.create(m) +} + +func (dao *Dao) update(m models.Model) error { + if !m.HasId() { + return errors.New("ID is not set") + } + + m.RefreshUpdated() + + if dao.BeforeUpdateFunc != nil { + if err := dao.BeforeUpdateFunc(dao, m); err != nil { + return err + } + } + + if v, ok := any(m).(models.ColumnValueMapper); ok { + dataMap := v.ColumnValueMap() + + _, err := dao.db.Update( + m.TableName(), + dataMap, + dbx.HashExp{"id": m.GetId()}, + ).Execute() + + if err != nil { + return err + } + } else { + err := dao.db.Model(m).Update() + if err != nil { + return err + } + } + + if dao.AfterUpdateFunc != nil { + dao.AfterUpdateFunc(dao, m) + } + + return nil +} + +func (dao *Dao) create(m models.Model) error { + if !m.HasId() { + // auto generate id + m.RefreshId() + } + + if m.GetCreated().IsZero() { + m.RefreshCreated() + } + + if m.GetUpdated().IsZero() { + m.RefreshUpdated() + } + + if dao.BeforeCreateFunc != nil { + if err := dao.BeforeCreateFunc(dao, m); err != nil { + return err + } + } + + if v, ok := any(m).(models.ColumnValueMapper); ok { + dataMap := v.ColumnValueMap() + + _, err := dao.db.Insert(m.TableName(), dataMap).Execute() + if err != nil { + return err + } + } else { + err := dao.db.Model(m).Insert() + if err != nil { + return err + } + } + + if dao.AfterCreateFunc != nil { + dao.AfterCreateFunc(dao, m) + } + + return nil +} diff --git a/daos/base_test.go b/daos/base_test.go new file mode 100644 index 00000000..31d12fc9 --- /dev/null +++ b/daos/base_test.go @@ -0,0 +1,245 @@ +package daos_test + +import ( + "errors" + "testing" + + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" +) + +func TestNew(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + dao := daos.New(testApp.DB()) + + if dao.DB() != testApp.DB() { + t.Fatal("The 2 db instances are different") + } +} + +func TestDaoModelQuery(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + dao := daos.New(testApp.DB()) + + scenarios := []struct { + model models.Model + expected string + }{ + { + &models.Collection{}, + "SELECT {{_collections}}.* FROM `_collections`", + }, + { + &models.User{}, + "SELECT {{_users}}.* FROM `_users`", + }, + { + &models.Request{}, + "SELECT {{_requests}}.* FROM `_requests`", + }, + } + + for i, scenario := range scenarios { + sql := dao.ModelQuery(scenario.model).Build().SQL() + if sql != scenario.expected { + t.Errorf("(%d) Expected select %s, got %s", i, scenario.expected, sql) + } + } +} + +func TestDaoFindById(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + model models.Model + id string + expectError bool + }{ + // missing id + { + &models.Collection{}, + "00000000-075d-49fe-9d09-ea7e951000dc", + true, + }, + // existing collection id + { + &models.Collection{}, + "3f2888f8-075d-49fe-9d09-ea7e951000dc", + false, + }, + // existing user id + { + &models.User{}, + "97cc3d3d-6ba2-383f-b42a-7bc84d27410c", + false, + }, + } + + for i, scenario := range scenarios { + err := testApp.Dao().FindById(scenario.model, scenario.id) + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expectError, err) + } + + if !scenario.expectError && scenario.id != scenario.model.GetId() { + t.Errorf("(%d) Expected model with id %v, got %v", i, scenario.id, scenario.model.GetId()) + } + } +} + +func TestDaoRunInTransaction(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + // failed nested transaction + testApp.Dao().RunInTransaction(func(txDao *daos.Dao) error { + admin, _ := txDao.FindAdminByEmail("test@example.com") + + return txDao.RunInTransaction(func(tx2Dao *daos.Dao) error { + if err := tx2Dao.DeleteAdmin(admin); err != nil { + t.Fatal(err) + } + return errors.New("test error") + }) + }) + + // admin should still exist + admin1, _ := testApp.Dao().FindAdminByEmail("test@example.com") + if admin1 == nil { + t.Fatal("Expected admin test@example.com to not be deleted") + } + + // successful nested transaction + testApp.Dao().RunInTransaction(func(txDao *daos.Dao) error { + admin, _ := txDao.FindAdminByEmail("test@example.com") + + return txDao.RunInTransaction(func(tx2Dao *daos.Dao) error { + return tx2Dao.DeleteAdmin(admin) + }) + }) + + // admin should have been deleted + admin2, _ := testApp.Dao().FindAdminByEmail("test@example.com") + if admin2 != nil { + t.Fatalf("Expected admin test@example.com to be deleted, found %v", admin2) + } +} + +func TestDaoSaveCreate(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + model := &models.Admin{} + model.Email = "test_new@example.com" + model.Avatar = 8 + if err := testApp.Dao().Save(model); err != nil { + t.Fatal(err) + } + + // refresh + model, _ = testApp.Dao().FindAdminByEmail("test_new@example.com") + + if model.Avatar != 8 { + t.Fatalf("Expected model avatar field to be 8, got %v", model.Avatar) + } + + expectedHooks := []string{"OnModelBeforeCreate", "OnModelAfterCreate"} + for _, h := range expectedHooks { + if v, ok := testApp.EventCalls[h]; !ok || v != 1 { + t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) + } + } +} + +func TestDaoSaveUpdate(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + model, _ := testApp.Dao().FindAdminByEmail("test@example.com") + + model.Avatar = 8 + if err := testApp.Dao().Save(model); err != nil { + t.Fatal(err) + } + + // refresh + model, _ = testApp.Dao().FindAdminByEmail("test@example.com") + + if model.Avatar != 8 { + t.Fatalf("Expected model avatar field to be updated to 8, got %v", model.Avatar) + } + + expectedHooks := []string{"OnModelBeforeUpdate", "OnModelAfterUpdate"} + for _, h := range expectedHooks { + if v, ok := testApp.EventCalls[h]; !ok || v != 1 { + t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) + } + } +} + +func TestDaoDelete(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + model, _ := testApp.Dao().FindAdminByEmail("test@example.com") + + if err := testApp.Dao().Delete(model); err != nil { + t.Fatal(err) + } + + model, _ = testApp.Dao().FindAdminByEmail("test@example.com") + if model != nil { + t.Fatalf("Expected model to be deleted, found %v", model) + } + + expectedHooks := []string{"OnModelBeforeDelete", "OnModelAfterDelete"} + for _, h := range expectedHooks { + if v, ok := testApp.EventCalls[h]; !ok || v != 1 { + t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v) + } + } +} + +func TestDaoBeforeHooksError(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + testApp.Dao().BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error { + return errors.New("before_create") + } + testApp.Dao().BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error { + return errors.New("before_update") + } + testApp.Dao().BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error { + return errors.New("before_delete") + } + + existingModel, _ := testApp.Dao().FindAdminByEmail("test@example.com") + + // try to create + // --- + newModel := &models.Admin{} + newModel.Email = "test_new@example.com" + if err := testApp.Dao().Save(newModel); err.Error() != "before_create" { + t.Fatalf("Expected before_create error, got %v", err) + } + + // try to update + // --- + if err := testApp.Dao().Save(existingModel); err.Error() != "before_update" { + t.Fatalf("Expected before_update error, got %v", err) + } + + // try to delete + // --- + if err := testApp.Dao().Delete(existingModel); err.Error() != "before_delete" { + t.Fatalf("Expected before_delete error, got %v", err) + } +} diff --git a/daos/collection.go b/daos/collection.go new file mode 100644 index 00000000..62100333 --- /dev/null +++ b/daos/collection.go @@ -0,0 +1,163 @@ +package daos + +import ( + "errors" + "fmt" + "strings" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" +) + +// CollectionQuery returns a new Collection select query. +func (dao *Dao) CollectionQuery() *dbx.SelectQuery { + return dao.ModelQuery(&models.Collection{}) +} + +// FindCollectionByNameOrId finds the first collection by its name or id. +func (dao *Dao) FindCollectionByNameOrId(nameOrId string) (*models.Collection, error) { + model := &models.Collection{} + + err := dao.CollectionQuery(). + AndWhere(dbx.Or( + dbx.HashExp{"id": nameOrId}, + dbx.HashExp{"name": nameOrId}, + )). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + return model, nil +} + +// IsCollectionNameUnique checks that there is no existing collection +// with the provided name (case insensitive!). +// +// Note: case sensitive check because the name is used also as a table name for the records. +func (dao *Dao) IsCollectionNameUnique(name string, excludeId string) bool { + if name == "" { + return false + } + + var exists bool + err := dao.CollectionQuery(). + Select("count(*)"). + AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})). + AndWhere(dbx.NewExp("LOWER([[name]])={:name}", dbx.Params{"name": strings.ToLower(name)})). + Limit(1). + Row(&exists) + + return err == nil && !exists +} + +// FindCollectionsWithUserFields finds all collections that has +// at least one user schema field. +func (dao *Dao) FindCollectionsWithUserFields() ([]*models.Collection, error) { + result := []*models.Collection{} + + err := dao.CollectionQuery(). + InnerJoin( + "json_each(schema) as jsonField", + dbx.NewExp( + "json_extract(jsonField.value, '$.type') = {:type}", + dbx.Params{"type": schema.FieldTypeUser}, + ), + ). + All(&result) + + return result, err +} + +// FindCollectionReferences returns information for all +// relation schema fields referencing the provided collection. +// +// If the provided collection has reference to itself then it will be +// also included in the result. To exlude it, pass the collection id +// as the excludeId argument. +func (dao *Dao) FindCollectionReferences(collection *models.Collection, excludeId string) (map[*models.Collection][]*schema.SchemaField, error) { + collections := []*models.Collection{} + + err := dao.CollectionQuery(). + AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})). + All(&collections) + if err != nil { + return nil, err + } + + result := map[*models.Collection][]*schema.SchemaField{} + for _, c := range collections { + for _, f := range c.Schema.Fields() { + if f.Type != schema.FieldTypeRelation { + continue + } + f.InitOptions() + options, _ := f.Options.(*schema.RelationOptions) + if options != nil && options.CollectionId == collection.Id { + result[c] = append(result[c], f) + } + } + } + + return result, nil +} + +// DeleteCollection deletes the provided Collection model. +// This method automatically deletes the related collection records table. +// +// NB! The collection cannot be deleted, if: +// - is system collection (aka. collection.System is true) +// - is referenced as part of a relation field in another collection +func (dao *Dao) DeleteCollection(collection *models.Collection) error { + if collection.System { + return errors.New("System collections cannot be deleted.") + } + + // ensure that there aren't any existing references. + // note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction + result, err := dao.FindCollectionReferences(collection, collection.Id) + if err != nil { + return err + } + if total := len(result); total > 0 { + return fmt.Errorf("The collection has external relation field references (%d).", total) + } + + return dao.RunInTransaction(func(txDao *Dao) error { + // delete the related records table + if err := txDao.DeleteTable(collection.Name); err != nil { + return err + } + + return txDao.Delete(collection) + }) +} + +// SaveCollection upserts the provided Collection model and updates +// its related records table schema. +func (dao *Dao) SaveCollection(collection *models.Collection) error { + var oldCollection *models.Collection + + if collection.HasId() { + // get the existing collection state to compare with the new one + // note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction + var findErr error + oldCollection, findErr = dao.FindCollectionByNameOrId(collection.Id) + if findErr != nil { + return findErr + } + } + + return dao.RunInTransaction(func(txDao *Dao) error { + // persist the collection model + if err := txDao.Save(collection); err != nil { + return err + } + + // sync the changes with the related records table + return txDao.SyncRecordTableSchema(collection, oldCollection) + }) +} diff --git a/daos/collection_test.go b/daos/collection_test.go new file mode 100644 index 00000000..7e039e80 --- /dev/null +++ b/daos/collection_test.go @@ -0,0 +1,253 @@ +package daos_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" +) + +func TestCollectionQuery(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + expected := "SELECT {{_collections}}.* FROM `_collections`" + + sql := app.Dao().CollectionQuery().Build().SQL() + if sql != expected { + t.Errorf("Expected sql %s, got %s", expected, sql) + } +} + +func TestFindCollectionByNameOrId(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + nameOrId string + expectError bool + }{ + {"", true}, + {"missing", true}, + {"00000000-075d-49fe-9d09-ea7e951000dc", true}, + {"3f2888f8-075d-49fe-9d09-ea7e951000dc", false}, + {"demo", false}, + } + + for i, scenario := range scenarios { + model, err := app.Dao().FindCollectionByNameOrId(scenario.nameOrId) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if model != nil && model.Id != scenario.nameOrId && model.Name != scenario.nameOrId { + t.Errorf("(%d) Expected model with identifier %s, got %v", i, scenario.nameOrId, model) + } + } +} + +func TestIsCollectionNameUnique(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + name string + excludeId string + expected bool + }{ + {"", "", false}, + {"demo", "", false}, + {"new", "", true}, + {"demo", "3f2888f8-075d-49fe-9d09-ea7e951000dc", true}, + } + + for i, scenario := range scenarios { + result := app.Dao().IsCollectionNameUnique(scenario.name, scenario.excludeId) + if result != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) + } + } +} + +func TestFindCollectionsWithUserFields(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + result, err := app.Dao().FindCollectionsWithUserFields() + if err != nil { + t.Fatal(err) + } + + expectedNames := []string{"demo2", models.ProfileCollectionName} + + if len(result) != len(expectedNames) { + t.Fatalf("Expected collections %v, got %v", expectedNames, result) + } + + for i, col := range result { + if !list.ExistInSlice(col.Name, expectedNames) { + t.Errorf("(%d) Couldn't find %s in %v", i, col.Name, expectedNames) + } + } +} + +func TestFindCollectionReferences(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.Dao().FindCollectionByNameOrId("demo") + if err != nil { + t.Fatal(err) + } + + result, err := app.Dao().FindCollectionReferences(collection, collection.Id) + if err != nil { + t.Fatal(err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 collection, got %d: %v", len(result), result) + } + + expectedFields := []string{"onerel", "manyrels", "rel_cascade"} + + for col, fields := range result { + if col.Name != "demo2" { + t.Fatalf("Expected collection demo2, got %s", col.Name) + } + if len(fields) != len(expectedFields) { + t.Fatalf("Expected fields %v, got %v", expectedFields, fields) + } + for i, f := range fields { + if !list.ExistInSlice(f.Name, expectedFields) { + t.Fatalf("(%d) Didn't expect field %v", i, f) + } + } + } +} + +func TestDeleteCollection(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + c0 := &models.Collection{} + c1, err := app.Dao().FindCollectionByNameOrId("demo") + if err != nil { + t.Fatal(err) + } + c2, err := app.Dao().FindCollectionByNameOrId("demo2") + if err != nil { + t.Fatal(err) + } + c3, err := app.Dao().FindCollectionByNameOrId(models.ProfileCollectionName) + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + model *models.Collection + expectError bool + }{ + {c0, true}, + {c1, true}, // is part of a reference + {c2, false}, + {c3, true}, // system + } + + for i, scenario := range scenarios { + err := app.Dao().DeleteCollection(scenario.model) + hasErr := err != nil + + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v", i, scenario.expectError, hasErr) + } + } + +} + +func TestSaveCollectionCreate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := &models.Collection{ + Name: "new_test", + Schema: schema.NewSchema( + &schema.SchemaField{ + Type: schema.FieldTypeText, + Name: "test", + }, + ), + } + + err := app.Dao().SaveCollection(collection) + if err != nil { + t.Fatal(err) + } + + if collection.Id == "" { + t.Fatal("Expected collection id to be set") + } + + // check if the records table was created + hasTable := app.Dao().HasTable(collection.Name) + if !hasTable { + t.Fatalf("Expected records table %s to be created", collection.Name) + } + + // check if the records table has the schema fields + columns, err := app.Dao().GetTableColumns(collection.Name) + if err != nil { + t.Fatal(err) + } + expectedColumns := []string{"id", "created", "updated", "test"} + if len(columns) != len(expectedColumns) { + t.Fatalf("Expected columns %v, got %v", expectedColumns, columns) + } + for i, c := range columns { + if !list.ExistInSlice(c, expectedColumns) { + t.Fatalf("(%d) Didn't expect record column %s", i, c) + } + } +} + +func TestSaveCollectionUpdate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.Dao().FindCollectionByNameOrId("demo3") + if err != nil { + t.Fatal(err) + } + + // rename an existing schema field and add a new one + oldField := collection.Schema.GetFieldByName("title") + oldField.Name = "title_update" + collection.Schema.AddField(&schema.SchemaField{ + Type: schema.FieldTypeText, + Name: "test", + }) + + saveErr := app.Dao().SaveCollection(collection) + if saveErr != nil { + t.Fatal(saveErr) + } + + // check if the records table has the schema fields + expectedColumns := []string{"id", "created", "updated", "title_update", "test"} + columns, err := app.Dao().GetTableColumns(collection.Name) + if err != nil { + t.Fatal(err) + } + if len(columns) != len(expectedColumns) { + t.Fatalf("Expected columns %v, got %v", expectedColumns, columns) + } + for i, c := range columns { + if !list.ExistInSlice(c, expectedColumns) { + t.Fatalf("(%d) Didn't expect record column %s", i, c) + } + } +} diff --git a/daos/param.go b/daos/param.go new file mode 100644 index 00000000..c17a9c79 --- /dev/null +++ b/daos/param.go @@ -0,0 +1,75 @@ +package daos + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" +) + +// ParamQuery returns a new Param select query. +func (dao *Dao) ParamQuery() *dbx.SelectQuery { + return dao.ModelQuery(&models.Param{}) +} + +// FindParamByKey finds the first Param model with the provided key. +func (dao *Dao) FindParamByKey(key string) (*models.Param, error) { + param := &models.Param{} + + err := dao.ParamQuery(). + AndWhere(dbx.HashExp{"key": key}). + Limit(1). + One(param) + + if err != nil { + return nil, err + } + + return param, nil +} + +// SaveParam creates or updates a Param model by the provided key-value pair. +// The value argument will be encoded as json string. +// +// If `optEncryptionKey` is provided it will encrypt the value before storing it. +func (dao *Dao) SaveParam(key string, value any, optEncryptionKey ...string) error { + param, _ := dao.FindParamByKey(key) + if param == nil { + param = &models.Param{Key: key} + } + + var normalizedValue any + + // encrypt if optEncryptionKey is set + if len(optEncryptionKey) > 0 && optEncryptionKey[0] != "" { + encoded, encodingErr := json.Marshal(value) + if encodingErr != nil { + return encodingErr + } + + encryptVal, encryptErr := security.Encrypt(encoded, optEncryptionKey[0]) + if encryptErr != nil { + return encryptErr + } + + normalizedValue = encryptVal + } else { + normalizedValue = value + } + + encodedValue := types.JsonRaw{} + if err := encodedValue.Scan(normalizedValue); err != nil { + return err + } + + param.Value = encodedValue + + return dao.Save(param) +} + +// DeleteParam deletes the provided Param model. +func (dao *Dao) DeleteParam(param *models.Param) error { + return dao.Delete(param) +} diff --git a/daos/param_test.go b/daos/param_test.go new file mode 100644 index 00000000..b3646085 --- /dev/null +++ b/daos/param_test.go @@ -0,0 +1,150 @@ +package daos_test + +import ( + "encoding/json" + "testing" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestParamQuery(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + expected := "SELECT {{_params}}.* FROM `_params`" + + sql := app.Dao().ParamQuery().Build().SQL() + if sql != expected { + t.Errorf("Expected sql %s, got %s", expected, sql) + } +} + +func TestFindParamByKey(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + key string + expectError bool + }{ + {"", true}, + {"missing", true}, + {models.ParamAppSettings, false}, + } + + for i, scenario := range scenarios { + param, err := app.Dao().FindParamByKey(scenario.key) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if param != nil && param.Key != scenario.key { + t.Errorf("(%d) Expected param with identifier %s, got %v", i, scenario.key, param.Key) + } + } +} + +func TestSaveParam(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + key string + value any + }{ + {"", "demo"}, + {"test", nil}, + {"test", ""}, + {"test", 1}, + {"test", 123}, + {models.ParamAppSettings, map[string]any{"test": 123}}, + } + + for i, scenario := range scenarios { + err := app.Dao().SaveParam(scenario.key, scenario.value) + if err != nil { + t.Errorf("(%d) %v", i, err) + } + + jsonRaw := types.JsonRaw{} + jsonRaw.Scan(scenario.value) + encodedScenarioValue, err := jsonRaw.MarshalJSON() + if err != nil { + t.Errorf("(%d) Encoded error %v", i, err) + } + + // check if the param was really saved + param, _ := app.Dao().FindParamByKey(scenario.key) + encodedParamValue, err := param.Value.MarshalJSON() + if err != nil { + t.Errorf("(%d) Encoded error %v", i, err) + } + + if string(encodedParamValue) != string(encodedScenarioValue) { + t.Errorf("(%d) Expected the two values to be equal, got %v vs %v", i, string(encodedParamValue), string(encodedScenarioValue)) + } + } +} + +func TestSaveParamEncrypted(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + encryptionKey := security.RandomString(32) + data := map[string]int{"test": 123} + expected := map[string]int{} + + err := app.Dao().SaveParam("test", data, encryptionKey) + if err != nil { + t.Fatal(err) + } + + // check if the param was really saved + param, _ := app.Dao().FindParamByKey("test") + + // decrypt + decrypted, decryptErr := security.Decrypt(string(param.Value), encryptionKey) + if decryptErr != nil { + t.Fatal(decryptErr) + } + + // decode + decryptedDecodeErr := json.Unmarshal(decrypted, &expected) + if decryptedDecodeErr != nil { + t.Fatal(decryptedDecodeErr) + } + + // check if the decoded value is correct + if len(expected) != len(data) || expected["test"] != data["test"] { + t.Fatalf("Expected %v, got %v", expected, data) + } +} + +func TestDeleteParam(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // unsaved param + err1 := app.Dao().DeleteParam(&models.Param{}) + if err1 == nil { + t.Fatal("Expected error, got nil") + } + + // existing param + param, _ := app.Dao().FindParamByKey(models.ParamAppSettings) + err2 := app.Dao().DeleteParam(param) + if err2 != nil { + t.Fatalf("Expected nil, got error %v", err2) + } + + // check if it was really deleted + paramCheck, _ := app.Dao().FindParamByKey(models.ParamAppSettings) + if paramCheck != nil { + t.Fatalf("Expected param to be deleted, got %v", paramCheck) + } +} diff --git a/daos/record.go b/daos/record.go new file mode 100644 index 00000000..1ad21867 --- /dev/null +++ b/daos/record.go @@ -0,0 +1,351 @@ +package daos + +import ( + "errors" + "fmt" + "strings" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" +) + +// RecordQuery returns a new Record select query. +func (dao *Dao) RecordQuery(collection *models.Collection) *dbx.SelectQuery { + tableName := collection.Name + selectCols := fmt.Sprintf("%s.*", dao.DB().QuoteSimpleColumnName(tableName)) + + return dao.DB().Select(selectCols).From(tableName) +} + +// FindRecordById finds the Record model by its id. +func (dao *Dao) FindRecordById( + collection *models.Collection, + recordId string, + filter func(q *dbx.SelectQuery) error, +) (*models.Record, error) { + tableName := collection.Name + + query := dao.RecordQuery(collection). + AndWhere(dbx.HashExp{tableName + ".id": recordId}) + + if filter != nil { + if err := filter(query); err != nil { + return nil, err + } + } + + row := dbx.NullStringMap{} + if err := query.Limit(1).One(row); err != nil { + return nil, err + } + + return models.NewRecordFromNullStringMap(collection, row), nil +} + +// FindRecordsByIds finds all Record models by the provided ids. +// If no records are found, returns an empty slice. +func (dao *Dao) FindRecordsByIds( + collection *models.Collection, + recordIds []string, + filter func(q *dbx.SelectQuery) error, +) ([]*models.Record, error) { + tableName := collection.Name + + query := dao.RecordQuery(collection). + AndWhere(dbx.In(tableName+".id", list.ToInterfaceSlice(recordIds)...)) + + if filter != nil { + if err := filter(query); err != nil { + return nil, err + } + } + + rows := []dbx.NullStringMap{} + if err := query.All(&rows); err != nil { + return nil, err + } + + return models.NewRecordsFromNullStringMaps(collection, rows), nil +} + +// FindRecordsByExpr finds all records by the provided db expression. +// If no records are found, returns an empty slice. +// +// Example: +// expr := dbx.HashExp{"email": "test@example.com"} +// dao.FindRecordsByExpr(collection, expr) +func (dao *Dao) FindRecordsByExpr(collection *models.Collection, expr dbx.Expression) ([]*models.Record, error) { + if expr == nil { + return nil, errors.New("Missing filter expression") + } + + rows := []dbx.NullStringMap{} + + err := dao.RecordQuery(collection). + AndWhere(expr). + All(&rows) + + if err != nil { + return nil, err + } + + return models.NewRecordsFromNullStringMaps(collection, rows), nil +} + +// FindFirstRecordByData returns the first found record matching +// the provided key-value pair. +func (dao *Dao) FindFirstRecordByData(collection *models.Collection, key string, value any) (*models.Record, error) { + row := dbx.NullStringMap{} + + err := dao.RecordQuery(collection). + AndWhere(dbx.HashExp{key: value}). + Limit(1). + One(row) + + if err != nil { + return nil, err + } + + return models.NewRecordFromNullStringMap(collection, row), nil +} + +// IsRecordValueUnique checks if the provided key-value pair is a unique Record value. +// +// NB! Array values (eg. from multiple select fields) are matched +// as a serialized json strings (eg. `["a","b"]`), so the value uniqueness +// depends on the elements order. Or in other words the following values +// are considered different: `[]string{"a","b"}` and `[]string{"b","a"}` +func (dao *Dao) IsRecordValueUnique( + collection *models.Collection, + key string, + value any, + excludeId string, +) bool { + var exists bool + + var normalizedVal any + switch val := value.(type) { + case []string: + normalizedVal = append(types.JsonArray{}, list.ToInterfaceSlice(val)...) + case []any: + normalizedVal = append(types.JsonArray{}, val...) + default: + normalizedVal = val + } + + err := dao.RecordQuery(collection). + Select("count(*)"). + AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})). + AndWhere(dbx.HashExp{key: normalizedVal}). + Limit(1). + Row(&exists) + + return err == nil && !exists +} + +// FindUserRelatedRecords returns all records that has a reference +// to the provided User model (via the user shema field). +func (dao *Dao) FindUserRelatedRecords(user *models.User) ([]*models.Record, error) { + collections, err := dao.FindCollectionsWithUserFields() + if err != nil { + return nil, err + } + + result := []*models.Record{} + for _, collection := range collections { + userFields := []*schema.SchemaField{} + + // prepare fields options + if err := collection.Schema.InitFieldsOptions(); err != nil { + return nil, err + } + + // extract user fields + for _, field := range collection.Schema.Fields() { + if field.Type == schema.FieldTypeUser { + userFields = append(userFields, field) + } + } + + // fetch records associated to the user + exprs := []dbx.Expression{} + for _, field := range userFields { + exprs = append(exprs, dbx.HashExp{field.Name: user.Id}) + } + rows := []dbx.NullStringMap{} + if err := dao.RecordQuery(collection).AndWhere(dbx.Or(exprs...)).All(&rows); err != nil { + return nil, err + } + records := models.NewRecordsFromNullStringMaps(collection, rows) + + result = append(result, records...) + } + + return result, nil +} + +// SaveRecord upserts the provided Record model. +func (dao *Dao) SaveRecord(record *models.Record) error { + return dao.Save(record) +} + +// DeleteRecord deletes the provided Record model. +// +// This method will also cascade the delete operation to all linked +// relational records (delete or set to NULL, depending on the rel settings). +// +// The delete operation may fail if the record is part of a required +// reference in another record (aka. cannot be deleted or set to NULL). +func (dao *Dao) DeleteRecord(record *models.Record) error { + // check for references + // note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction + refs, err := dao.FindCollectionReferences(record.Collection(), "") + if err != nil { + return err + } + + // check if related records has to be deleted (if `CascadeDelete` is set) + // OR + // just unset the record id from any relation field values (if they are not required) + // ----------------------------------------------------------- + return dao.RunInTransaction(func(txDao *Dao) error { + for refCollection, fields := range refs { + for _, field := range fields { + options, _ := field.Options.(*schema.RelationOptions) + + rows := []dbx.NullStringMap{} + + // note: the select is not using the transaction dao to prevent SQLITE_LOCKED error when mixing read&write in a single transaction + err := dao.RecordQuery(refCollection). + AndWhere(dbx.Not(dbx.HashExp{"id": record.Id})). + AndWhere(dbx.Like(field.Name, record.Id).Match(true, true)). + All(&rows) + if err != nil { + return err + } + + refRecords := models.NewRecordsFromNullStringMaps(refCollection, rows) + for _, refRecord := range refRecords { + ids := refRecord.GetStringSliceDataValue(field.Name) + + // unset the record id + for i := len(ids) - 1; i >= 0; i-- { + if ids[i] == record.Id { + ids = append(ids[:i], ids[i+1:]...) + break + } + } + + // cascade delete the reference + // (only if there are no other active references in case of multiple select) + if options.CascadeDelete && len(ids) == 0 { + if err := txDao.DeleteRecord(refRecord); err != nil { + return err + } + // no further action are needed (the reference is deleted) + continue + } + + if field.Required && len(ids) == 0 { + return fmt.Errorf("The record cannot be deleted because it is part of a required reference in record %s (%s collection).", refRecord.Id, refCollection.Name) + } + + // save the reference changes + refRecord.SetDataValue(field.Name, field.PrepareValue(ids)) + if err := txDao.SaveRecord(refRecord); err != nil { + return err + } + } + } + } + + return txDao.Delete(record) + }) +} + +// SyncRecordTableSchema compares the two provided collections +// and applies the necessary related record table changes. +// +// If `oldCollection` is null, then only `newCollection` is used to create the record table. +func (dao *Dao) SyncRecordTableSchema(newCollection *models.Collection, oldCollection *models.Collection) error { + // create + if oldCollection == nil { + cols := map[string]string{ + schema.ReservedFieldNameId: "TEXT PRIMARY KEY", + schema.ReservedFieldNameCreated: `TEXT DEFAULT "" NOT NULL`, + schema.ReservedFieldNameUpdated: `TEXT DEFAULT "" NOT NULL`, + } + + tableName := newCollection.Name + + // add schema field definitions + for _, field := range newCollection.Schema.Fields() { + cols[field.Name] = field.ColDefinition() + } + + // create table + _, tableErr := dao.DB().CreateTable(tableName, cols).Execute() + if tableErr != nil { + return tableErr + } + + // add index on the base `created` column + _, indexErr := dao.DB().CreateIndex(tableName, tableName+"_created_idx", "created").Execute() + if indexErr != nil { + return indexErr + } + + return nil + } + + // update + return dao.RunInTransaction(func(txDao *Dao) error { + oldTableName := oldCollection.Name + newTableName := newCollection.Name + oldSchema := oldCollection.Schema + newSchema := newCollection.Schema + + // check for renamed table + if strings.ToLower(oldTableName) != strings.ToLower(newTableName) { + _, err := dao.DB().RenameTable(oldTableName, newTableName).Execute() + if err != nil { + return err + } + } + + // check for deleted columns + for _, oldField := range oldSchema.Fields() { + if f := newSchema.GetFieldById(oldField.Id); f != nil { + continue // exist + } + + _, err := txDao.DB().DropColumn(newTableName, oldField.Name).Execute() + if err != nil { + return err + } + } + + // check for new or renamed columns + for _, field := range newSchema.Fields() { + oldField := oldSchema.GetFieldById(field.Id) + if oldField != nil { + // rename + _, err := txDao.DB().RenameColumn(newTableName, oldField.Name, field.Name).Execute() + if err != nil { + return err + } + } else { + // add + _, err := txDao.DB().AddColumn(newTableName, field.Name, field.ColDefinition()).Execute() + if err != nil { + return err + } + } + } + + return nil + }) +} diff --git a/daos/record_expand.go b/daos/record_expand.go new file mode 100644 index 00000000..9a023bc5 --- /dev/null +++ b/daos/record_expand.go @@ -0,0 +1,155 @@ +package daos + +import ( + "errors" + "fmt" + "strings" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" +) + +// MaxExpandDepth specifies the max allowed nested expand depth path. +const MaxExpandDepth = 6 + +// ExpandFetchFunc defines the function that is used to fetch the expanded relation records. +type ExpandFetchFunc func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) + +// ExpandRecord expands the relations of a single Record model. +func (dao *Dao) ExpandRecord(record *models.Record, expands []string, fetchFunc ExpandFetchFunc) error { + return dao.ExpandRecords([]*models.Record{record}, expands, fetchFunc) +} + +// ExpandRecords expands the relations of the provided Record models list. +func (dao *Dao) ExpandRecords(records []*models.Record, expands []string, fetchFunc ExpandFetchFunc) error { + normalized := normalizeExpands(expands) + + for _, expand := range normalized { + if err := dao.expandRecords(records, expand, fetchFunc, 1); err != nil { + return err + } + } + + return nil +} + +// notes: +// - fetchFunc must be non-nil func +// - all records are expected to be from the same collection +// - if MaxExpandDepth is reached, the function returns nil ignoring the remaining expand path +func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetchFunc ExpandFetchFunc, recursionLevel int) error { + if fetchFunc == nil { + return errors.New("Relation records fetchFunc is not set.") + } + + if expandPath == "" || recursionLevel > MaxExpandDepth || len(records) == 0 { + return nil + } + + parts := strings.SplitN(expandPath, ".", 2) + + // extract the relation field (if exist) + mainCollection := records[0].Collection() + relField := mainCollection.Schema.GetFieldByName(parts[0]) + if relField == nil { + return fmt.Errorf("Couldn't find field %q in collection %q.", parts[0], mainCollection.Name) + } + relField.InitOptions() + relFieldOptions, _ := relField.Options.(*schema.RelationOptions) + + relCollection, err := dao.FindCollectionByNameOrId(relFieldOptions.CollectionId) + if err != nil { + return fmt.Errorf("Couldn't find collection %q.", relFieldOptions.CollectionId) + } + + // extract the id of the relations to expand + relIds := []string{} + for _, record := range records { + relIds = append(relIds, record.GetStringSliceDataValue(relField.Name)...) + } + + // fetch rels + rels, relsErr := fetchFunc(relCollection, relIds) + if relsErr != nil { + return relsErr + } + + // expand nested fields + if len(parts) > 1 { + err := dao.expandRecords(rels, parts[1], fetchFunc, recursionLevel+1) + if err != nil { + return err + } + } + + // reindex with the rel id + indexedRels := map[string]*models.Record{} + for _, rel := range rels { + indexedRels[rel.GetId()] = rel + } + + for _, model := range records { + relIds := model.GetStringSliceDataValue(relField.Name) + + validRels := []*models.Record{} + for _, id := range relIds { + if rel, ok := indexedRels[id]; ok { + validRels = append(validRels, rel) + } + } + + if len(validRels) == 0 { + continue // no valid relations + } + + expandData := model.GetExpand() + + // normalize and set the expanded relations + if relFieldOptions.MaxSelect == 1 { + expandData[relField.Name] = validRels[0] + } else { + expandData[relField.Name] = validRels + } + model.SetExpand(expandData) + } + + return nil +} + +// normalizeExpands normalizes expand strings and merges self containing paths +// (eg. ["a.b.c", "a.b", " test ", " ", "test"] -> ["a.b.c", "test"]). +func normalizeExpands(paths []string) []string { + result := []string{} + + // normalize paths + normalized := []string{} + for _, p := range paths { + p := strings.ReplaceAll(p, " ", "") // replace spaces + p = strings.Trim(p, ".") // trim incomplete paths + if p == "" { + continue + } + normalized = append(normalized, p) + } + + // merge containing paths + for i, p1 := range normalized { + var skip bool + for j, p2 := range normalized { + if i == j { + continue + } + if strings.HasPrefix(p2, p1+".") { + // skip because there is more detailed expand path + skip = true + break + } + } + if !skip { + result = append(result, p1) + } + } + + return list.ToUniqueStringSlice(result) +} diff --git a/daos/record_expand_test.go b/daos/record_expand_test.go new file mode 100644 index 00000000..d85b996f --- /dev/null +++ b/daos/record_expand_test.go @@ -0,0 +1,258 @@ +package daos_test + +import ( + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" +) + +func TestExpandRecords(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + col, _ := app.Dao().FindCollectionByNameOrId("demo4") + + scenarios := []struct { + recordIds []string + expands []string + fetchFunc daos.ExpandFetchFunc + expectExpandProps int + expectError bool + }{ + // empty records + { + []string{}, + []string{"onerel", "manyrels.onerel.manyrels"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 0, + false, + }, + // empty expand + { + []string{"b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", "df55c8ff-45ef-4c82-8aed-6e2183fe1125"}, + []string{}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 0, + false, + }, + // empty fetchFunc + { + []string{"b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", "df55c8ff-45ef-4c82-8aed-6e2183fe1125"}, + []string{"onerel", "manyrels.onerel.manyrels"}, + nil, + 0, + true, + }, + // fetchFunc with error + { + []string{"b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", "df55c8ff-45ef-4c82-8aed-6e2183fe1125"}, + []string{"onerel", "manyrels.onerel.manyrels"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return nil, errors.New("test error") + }, + 0, + true, + }, + // invalid missing first level expand + { + []string{"b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", "df55c8ff-45ef-4c82-8aed-6e2183fe1125"}, + []string{"invalid"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 0, + true, + }, + // invalid missing second level expand + { + []string{"b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", "df55c8ff-45ef-4c82-8aed-6e2183fe1125"}, + []string{"manyrels.invalid"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 0, + true, + }, + // expand normalizations + { + []string{ + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + "df55c8ff-45ef-4c82-8aed-6e2183fe1125", + "b84cd893-7119-43c9-8505-3c4e22da28a9", + "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2", + }, + []string{"manyrels.onerel.manyrels.onerel", "manyrels.onerel", "onerel", "onerel.", " onerel ", ""}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 9, + false, + }, + // single expand + { + []string{ + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + "df55c8ff-45ef-4c82-8aed-6e2183fe1125", + "b84cd893-7119-43c9-8505-3c4e22da28a9", // no manyrels + "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2", // no manyrels + }, + []string{"manyrels"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 2, + false, + }, + // maxExpandDepth reached + { + []string{"b8ba58f9-e2d7-42a0-b0e7-a11efd98236b"}, + []string{"manyrels.onerel.manyrels.onerel.manyrels.onerel.manyrels.onerel.manyrels"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 6, + false, + }, + } + + for i, s := range scenarios { + ids := list.ToUniqueStringSlice(s.recordIds) + records, _ := app.Dao().FindRecordsByIds(col, ids, nil) + err := app.Dao().ExpandRecords(records, s.expands, s.fetchFunc) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + encoded, _ := json.Marshal(records) + encodedStr := string(encoded) + totalExpandProps := strings.Count(encodedStr, "@expand") + + if s.expectExpandProps != totalExpandProps { + t.Errorf("(%d) Expected %d @expand props in %v, got %d", i, s.expectExpandProps, encodedStr, totalExpandProps) + } + } +} + +func TestExpandRecord(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + col, _ := app.Dao().FindCollectionByNameOrId("demo4") + + scenarios := []struct { + recordId string + expands []string + fetchFunc daos.ExpandFetchFunc + expectExpandProps int + expectError bool + }{ + // empty expand + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 0, + false, + }, + // empty fetchFunc + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{"onerel", "manyrels.onerel.manyrels"}, + nil, + 0, + true, + }, + // fetchFunc with error + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{"onerel", "manyrels.onerel.manyrels"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return nil, errors.New("test error") + }, + 0, + true, + }, + // invalid missing first level expand + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{"invalid"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 0, + true, + }, + // invalid missing second level expand + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{"manyrels.invalid"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 0, + true, + }, + // expand normalizations + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{"manyrels.onerel.manyrels", "manyrels.onerel", "onerel", " onerel "}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 3, + false, + }, + // single expand + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{"manyrels"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 1, + false, + }, + // maxExpandDepth reached + { + "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", + []string{"manyrels.onerel.manyrels.onerel.manyrels.onerel.manyrels.onerel.manyrels"}, + func(c *models.Collection, ids []string) ([]*models.Record, error) { + return app.Dao().FindRecordsByIds(c, ids, nil) + }, + 6, + false, + }, + } + + for i, s := range scenarios { + record, _ := app.Dao().FindFirstRecordByData(col, "id", s.recordId) + err := app.Dao().ExpandRecord(record, s.expands, s.fetchFunc) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + encoded, _ := json.Marshal(record) + encodedStr := string(encoded) + totalExpandProps := strings.Count(encodedStr, "@expand") + + if s.expectExpandProps != totalExpandProps { + t.Errorf("(%d) Expected %d @expand props in %v, got %d", i, s.expectExpandProps, encodedStr, totalExpandProps) + } + } +} diff --git a/daos/record_test.go b/daos/record_test.go new file mode 100644 index 00000000..6b3f30f0 --- /dev/null +++ b/daos/record_test.go @@ -0,0 +1,473 @@ +package daos_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" +) + +func TestRecordQuery(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + + expected := fmt.Sprintf("SELECT `%s`.* FROM `%s`", collection.Name, collection.Name) + + sql := app.Dao().RecordQuery(collection).Build().SQL() + if sql != expected { + t.Errorf("Expected sql %s, got %s", expected, sql) + } +} + +func TestFindRecordById(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + + scenarios := []struct { + id string + filter func(q *dbx.SelectQuery) error + expectError bool + }{ + {"00000000-bafd-48f7-b8b7-090638afe209", nil, true}, + {"b5c2ffc2-bafd-48f7-b8b7-090638afe209", nil, false}, + {"b5c2ffc2-bafd-48f7-b8b7-090638afe209", func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"title": "missing"}) + return nil + }, true}, + {"b5c2ffc2-bafd-48f7-b8b7-090638afe209", func(q *dbx.SelectQuery) error { + return errors.New("test error") + }, true}, + {"b5c2ffc2-bafd-48f7-b8b7-090638afe209", func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.HashExp{"title": "lorem"}) + return nil + }, false}, + } + + for i, scenario := range scenarios { + record, err := app.Dao().FindRecordById(collection, scenario.id, scenario.filter) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if record != nil && record.Id != scenario.id { + t.Errorf("(%d) Expected record with id %s, got %s", i, scenario.id, record.Id) + } + } +} + +func TestFindRecordsByIds(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + + scenarios := []struct { + ids []string + filter func(q *dbx.SelectQuery) error + expectTotal int + expectError bool + }{ + {[]string{}, nil, 0, false}, + {[]string{"00000000-bafd-48f7-b8b7-090638afe209"}, nil, 0, false}, + {[]string{"b5c2ffc2-bafd-48f7-b8b7-090638afe209"}, nil, 1, false}, + { + []string{"b5c2ffc2-bafd-48f7-b8b7-090638afe209", "848a1dea-5ddd-42d6-a00d-030547bffcfe"}, + nil, + 2, + false, + }, + { + []string{"b5c2ffc2-bafd-48f7-b8b7-090638afe209", "848a1dea-5ddd-42d6-a00d-030547bffcfe"}, + func(q *dbx.SelectQuery) error { + return errors.New("test error") + }, + 0, + true, + }, + { + []string{"b5c2ffc2-bafd-48f7-b8b7-090638afe209", "848a1dea-5ddd-42d6-a00d-030547bffcfe"}, + func(q *dbx.SelectQuery) error { + q.AndWhere(dbx.Like("title", "test").Match(true, true)) + return nil + }, + 1, + false, + }, + } + + for i, scenario := range scenarios { + records, err := app.Dao().FindRecordsByIds(collection, scenario.ids, scenario.filter) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if len(records) != scenario.expectTotal { + t.Errorf("(%d) Expected %d records, got %d", i, scenario.expectTotal, len(records)) + continue + } + + for _, r := range records { + if !list.ExistInSlice(r.Id, scenario.ids) { + t.Errorf("(%d) Couldn't find id %s in %v", i, r.Id, scenario.ids) + } + } + } +} + +func TestFindRecordsByExpr(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + + scenarios := []struct { + expression dbx.Expression + expectIds []string + expectError bool + }{ + { + nil, + []string{}, + true, + }, + { + dbx.HashExp{"id": 123}, + []string{}, + false, + }, + { + dbx.Like("title", "test").Match(true, true), + []string{ + "848a1dea-5ddd-42d6-a00d-030547bffcfe", + "577bd676-aacb-4072-b7da-99d00ee210a4", + }, + false, + }, + } + + for i, scenario := range scenarios { + records, err := app.Dao().FindRecordsByExpr(collection, scenario.expression) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if len(records) != len(scenario.expectIds) { + t.Errorf("(%d) Expected %d records, got %d", i, len(scenario.expectIds), len(records)) + continue + } + + for _, r := range records { + if !list.ExistInSlice(r.Id, scenario.expectIds) { + t.Errorf("(%d) Couldn't find id %s in %v", i, r.Id, scenario.expectIds) + } + } + } +} + +func TestFindFirstRecordByData(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + + scenarios := []struct { + key string + value any + expectId string + expectError bool + }{ + { + "", + "848a1dea-5ddd-42d6-a00d-030547bffcfe", + "", + true, + }, + { + "id", + "invalid", + "", + true, + }, + { + "id", + "848a1dea-5ddd-42d6-a00d-030547bffcfe", + "848a1dea-5ddd-42d6-a00d-030547bffcfe", + false, + }, + { + "title", + "lorem", + "b5c2ffc2-bafd-48f7-b8b7-090638afe209", + false, + }, + } + + for i, scenario := range scenarios { + record, err := app.Dao().FindFirstRecordByData(collection, scenario.key, scenario.value) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + continue + } + + if !scenario.expectError && record.Id != scenario.expectId { + t.Errorf("(%d) Expected record with id %s, got %v", i, scenario.expectId, record.Id) + } + } +} + +func TestIsRecordValueUnique(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + + testManyRelsId1 := "df55c8ff-45ef-4c82-8aed-6e2183fe1125" + testManyRelsId2 := "b84cd893-7119-43c9-8505-3c4e22da28a9" + + scenarios := []struct { + key string + value any + excludeId string + expected bool + }{ + {"", "", "", false}, + {"missing", "unique", "", false}, + {"title", "unique", "", true}, + {"title", "demo1", "", false}, + {"title", "demo1", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2", true}, + {"manyrels", []string{testManyRelsId2}, "", false}, + {"manyrels", []any{testManyRelsId2}, "", false}, + // with exclude + {"manyrels", []string{testManyRelsId1, testManyRelsId2}, "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b", true}, + // reverse order + {"manyrels", []string{testManyRelsId2, testManyRelsId1}, "", true}, + } + + for i, scenario := range scenarios { + result := app.Dao().IsRecordValueUnique(collection, scenario.key, scenario.value, scenario.excludeId) + + if result != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) + } + } +} + +func TestFindUserRelatedRecords(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + u0 := &models.User{} + u1, _ := app.Dao().FindUserByEmail("test3@example.com") + u2, _ := app.Dao().FindUserByEmail("test2@example.com") + + scenarios := []struct { + user *models.User + expectedIds []string + }{ + {u0, []string{}}, + {u1, []string{ + "94568ca2-0bee-49d7-b749-06cb97956fd9", // demo2 + "fc69274d-ca5c-416a-b9ef-561b101cfbb1", // profile + }}, + {u2, []string{ + "b2d5e39d-f569-4cc1-b593-3f074ad026bf", // profile + }}, + } + + for i, scenario := range scenarios { + records, err := app.Dao().FindUserRelatedRecords(scenario.user) + if err != nil { + t.Fatal(err) + } + + if len(records) != len(scenario.expectedIds) { + t.Errorf("(%d) Expected %d records, got %d (%v)", i, len(scenario.expectedIds), len(records), records) + continue + } + + for _, r := range records { + if !list.ExistInSlice(r.Id, scenario.expectedIds) { + t.Errorf("(%d) Couldn't find %s in %v", i, r.Id, scenario.expectedIds) + } + } + } +} + +func TestSaveRecord(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + + // create + // --- + r1 := models.NewRecord(collection) + r1.SetDataValue("title", "test_new") + err1 := app.Dao().SaveRecord(r1) + if err1 != nil { + t.Fatal(err1) + } + newR1, _ := app.Dao().FindFirstRecordByData(collection, "title", "test_new") + if newR1 == nil || newR1.Id != r1.Id || newR1.GetStringDataValue("title") != r1.GetStringDataValue("title") { + t.Errorf("Expected to find record %v, got %v", r1, newR1) + } + + // update + // --- + r2, _ := app.Dao().FindFirstRecordByData(collection, "id", "b5c2ffc2-bafd-48f7-b8b7-090638afe209") + r2.SetDataValue("title", "test_update") + err2 := app.Dao().SaveRecord(r2) + if err2 != nil { + t.Fatal(err2) + } + newR2, _ := app.Dao().FindFirstRecordByData(collection, "title", "test_update") + if newR2 == nil || newR2.Id != r2.Id || newR2.GetStringDataValue("title") != r2.GetStringDataValue("title") { + t.Errorf("Expected to find record %v, got %v", r2, newR2) + } +} + +func TestDeleteRecord(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo, _ := app.Dao().FindCollectionByNameOrId("demo") + demo2, _ := app.Dao().FindCollectionByNameOrId("demo2") + + // delete unsaved record + // --- + rec1 := models.NewRecord(demo) + err1 := app.Dao().DeleteRecord(rec1) + if err1 == nil { + t.Fatal("(rec1) Didn't expect to succeed deleting new record") + } + + // delete existing record while being part of a non-cascade required relation + // --- + rec2, _ := app.Dao().FindFirstRecordByData(demo, "id", "848a1dea-5ddd-42d6-a00d-030547bffcfe") + err2 := app.Dao().DeleteRecord(rec2) + if err2 == nil { + t.Fatalf("(rec2) Expected error, got nil") + } + + // delete existing record + // --- + rec3, _ := app.Dao().FindFirstRecordByData(demo, "id", "577bd676-aacb-4072-b7da-99d00ee210a4") + err3 := app.Dao().DeleteRecord(rec3) + if err3 != nil { + t.Fatalf("(rec3) Expected nil, got error %v", err3) + } + + // check if it was really deleted + rec3, _ = app.Dao().FindRecordById(demo, rec3.Id, nil) + if rec3 != nil { + t.Fatalf("(rec3) Expected record to be deleted, got %v", rec3) + } + + // check if the operation cascaded + rel, _ := app.Dao().FindFirstRecordByData(demo2, "id", "63c2ab80-84ab-4057-a592-4604a731f78f") + if rel != nil { + t.Fatalf("(rec3) Expected the delete to cascade, found relation %v", rel) + } +} + +func TestSyncRecordTableSchema(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + oldCollection, err := app.Dao().FindCollectionByNameOrId("demo") + if err != nil { + t.Fatal(err) + } + updatedCollection, err := app.Dao().FindCollectionByNameOrId("demo") + updatedCollection.Name = "demo_renamed" + updatedCollection.Schema.RemoveField(updatedCollection.Schema.GetFieldByName("file").Id) + updatedCollection.Schema.AddField( + &schema.SchemaField{ + Name: "new_field", + Type: schema.FieldTypeEmail, + }, + ) + updatedCollection.Schema.AddField( + &schema.SchemaField{ + Id: updatedCollection.Schema.GetFieldByName("title").Id, + Name: "title_renamed", + Type: schema.FieldTypeEmail, + }, + ) + + scenarios := []struct { + newCollection *models.Collection + oldCollection *models.Collection + expectedTableName string + expectedColumns []string + }{ + { + &models.Collection{ + Name: "new_table", + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "test", + Type: schema.FieldTypeText, + }, + ), + }, + nil, + "new_table", + []string{"id", "created", "updated", "test"}, + }, + // no changes + { + oldCollection, + oldCollection, + "demo", + []string{"id", "created", "updated", "title", "file"}, + }, + // renamed table, deleted column, renamed columnd and new column + { + updatedCollection, + oldCollection, + "demo_renamed", + []string{"id", "created", "updated", "title_renamed", "new_field"}, + }, + } + + for i, scenario := range scenarios { + err := app.Dao().SyncRecordTableSchema(scenario.newCollection, scenario.oldCollection) + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + + if !app.Dao().HasTable(scenario.newCollection.Name) { + t.Errorf("(%d) Expected table %s to exist", i, scenario.newCollection.Name) + } + + cols, _ := app.Dao().GetTableColumns(scenario.newCollection.Name) + if len(cols) != len(scenario.expectedColumns) { + t.Errorf("(%d) Expected columns %v, got %v", i, scenario.expectedColumns, cols) + } + + for _, c := range cols { + if !list.ExistInSlice(c, scenario.expectedColumns) { + t.Errorf("(%d) Couldn't find column %s in %v", i, c, scenario.expectedColumns) + } + } + } +} diff --git a/daos/request.go b/daos/request.go new file mode 100644 index 00000000..6616b9f3 --- /dev/null +++ b/daos/request.go @@ -0,0 +1,70 @@ +package daos + +import ( + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/types" +) + +// RequestQuery returns a new Request logs select query. +func (dao *Dao) RequestQuery() *dbx.SelectQuery { + return dao.ModelQuery(&models.Request{}) +} + +// FindRequestById finds a single Request log by its id. +func (dao *Dao) FindRequestById(id string) (*models.Request, error) { + model := &models.Request{} + + err := dao.RequestQuery(). + AndWhere(dbx.HashExp{"id": id}). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + return model, nil +} + +type RequestsStatsItem struct { + Total int `db:"total" json:"total"` + Date types.DateTime `db:"date" json:"date"` +} + +// RequestsStats returns hourly grouped requests logs statistics. +func (dao *Dao) RequestsStats(expr dbx.Expression) ([]*RequestsStatsItem, error) { + result := []*RequestsStatsItem{} + + query := dao.RequestQuery(). + Select("count(id) as total", "strftime('%Y-%m-%d %H:00:00', created) as date"). + GroupBy("date") + + if expr != nil { + query.AndWhere(expr) + } + + err := query.All(&result) + + return result, err +} + +// DeleteOldRequests delete all requests that are created before createdBefore. +func (dao *Dao) DeleteOldRequests(createdBefore time.Time) error { + m := models.Request{} + tableName := m.TableName() + + formattedDate := createdBefore.UTC().Format(types.DefaultDateLayout) + expr := dbx.NewExp("[[created]] <= {:date}", dbx.Params{"date": formattedDate}) + + _, err := dao.DB().Delete(tableName, expr).Execute() + + return err +} + +// SaveRequest upserts the provided Request model. +func (dao *Dao) SaveRequest(request *models.Request) error { + return dao.Save(request) +} diff --git a/daos/request_test.go b/daos/request_test.go new file mode 100644 index 00000000..97a1c28e --- /dev/null +++ b/daos/request_test.go @@ -0,0 +1,148 @@ +package daos_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestRequestQuery(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + expected := "SELECT {{_requests}}.* FROM `_requests`" + + sql := app.Dao().RequestQuery().Build().SQL() + if sql != expected { + t.Errorf("Expected sql %s, got %s", expected, sql) + } +} + +func TestFindRequestById(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + tests.MockRequestLogsData(app) + + scenarios := []struct { + id string + expectError bool + }{ + {"", true}, + {"invalid", true}, + {"00000000-9f38-44fb-bf82-c8f53b310d91", true}, + {"873f2133-9f38-44fb-bf82-c8f53b310d91", false}, + } + + for i, scenario := range scenarios { + admin, err := app.LogsDao().FindRequestById(scenario.id) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if admin != nil && admin.Id != scenario.id { + t.Errorf("(%d) Expected admin with id %s, got %s", i, scenario.id, admin.Id) + } + } +} + +func TestRequestsStats(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + tests.MockRequestLogsData(app) + + expected := `[{"total":1,"date":"2022-05-01 10:00:00.000"},{"total":1,"date":"2022-05-02 10:00:00.000"}]` + + now := time.Now().UTC().Format(types.DefaultDateLayout) + exp := dbx.NewExp("[[created]] <= {:date}", dbx.Params{"date": now}) + result, err := app.LogsDao().RequestsStats(exp) + if err != nil { + t.Fatal(err) + } + + encoded, _ := json.Marshal(result) + if string(encoded) != expected { + t.Fatalf("Expected %s, got %s", expected, string(encoded)) + } +} + +func TestDeleteOldRequests(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + tests.MockRequestLogsData(app) + + scenarios := []struct { + date string + expectedTotal int + }{ + {"2022-01-01 10:00:00.000", 2}, // no requests to delete before that time + {"2022-05-01 11:00:00.000", 1}, // only 1 request should have left + {"2022-05-03 11:00:00.000", 0}, // no more requests should have left + {"2022-05-04 11:00:00.000", 0}, // no more requests should have left + } + + for i, scenario := range scenarios { + date, dateErr := time.Parse(types.DefaultDateLayout, scenario.date) + if dateErr != nil { + t.Errorf("(%d) Date error %v", i, dateErr) + } + + deleteErr := app.LogsDao().DeleteOldRequests(date) + if deleteErr != nil { + t.Errorf("(%d) Delete error %v", i, deleteErr) + } + + // check total remaining requests + var total int + countErr := app.LogsDao().RequestQuery().Select("count(*)").Row(&total) + if countErr != nil { + t.Errorf("(%d) Count error %v", i, countErr) + } + + if total != scenario.expectedTotal { + t.Errorf("(%d) Expected %d remaining requests, got %d", i, scenario.expectedTotal, total) + } + } +} + +func TestSaveRequest(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + tests.MockRequestLogsData(app) + + // create new request + newRequest := &models.Request{} + newRequest.Method = "get" + newRequest.Meta = types.JsonMap{} + createErr := app.LogsDao().SaveRequest(newRequest) + if createErr != nil { + t.Fatal(createErr) + } + + // check if it was really created + existingRequest, fetchErr := app.LogsDao().FindRequestById(newRequest.Id) + if fetchErr != nil { + t.Fatal(fetchErr) + } + + existingRequest.Method = "post" + updateErr := app.LogsDao().SaveRequest(existingRequest) + if updateErr != nil { + t.Fatal(updateErr) + } + // refresh instance to check if it was really updated + existingRequest, _ = app.LogsDao().FindRequestById(existingRequest.Id) + if existingRequest.Method != "post" { + t.Fatalf("Expected request method to be %s, got %s", "post", existingRequest.Method) + } +} diff --git a/daos/table.go b/daos/table.go new file mode 100644 index 00000000..e950f3a2 --- /dev/null +++ b/daos/table.go @@ -0,0 +1,37 @@ +package daos + +import ( + "github.com/pocketbase/dbx" +) + +// HasTable checks if a table with the provided name exists (case insensitive). +func (dao *Dao) HasTable(tableName string) bool { + var exists bool + + err := dao.DB().Select("count(*)"). + From("sqlite_schema"). + AndWhere(dbx.HashExp{"type": "table"}). + AndWhere(dbx.NewExp("LOWER([[name]])=LOWER({:tableName})", dbx.Params{"tableName": tableName})). + Limit(1). + Row(&exists) + + return err == nil && exists +} + +// GetTableColumns returns all column names of a single table by its name. +func (dao *Dao) GetTableColumns(tableName string) ([]string, error) { + columns := []string{} + + err := dao.DB().NewQuery("SELECT name FROM PRAGMA_TABLE_INFO({:tableName})"). + Bind(dbx.Params{"tableName": tableName}). + Column(&columns) + + return columns, err +} + +// DeleteTable drops the specified table. +func (dao *Dao) DeleteTable(tableName string) error { + _, err := dao.DB().DropTable(tableName).Execute() + + return err +} diff --git a/daos/table_test.go b/daos/table_test.go new file mode 100644 index 00000000..4c6571cc --- /dev/null +++ b/daos/table_test.go @@ -0,0 +1,81 @@ +package daos_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" +) + +func TestHasTable(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expected bool + }{ + {"", false}, + {"test", false}, + {"_admins", true}, + {"demo3", true}, + {"DEMO3", true}, // table names are case insensitives by default + } + + for i, scenario := range scenarios { + result := app.Dao().HasTable(scenario.tableName) + if result != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) + } + } +} + +func TestGetTableColumns(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expected []string + }{ + {"", nil}, + {"_params", []string{"id", "key", "value", "created", "updated"}}, + } + + for i, scenario := range scenarios { + columns, _ := app.Dao().GetTableColumns(scenario.tableName) + + if len(columns) != len(scenario.expected) { + t.Errorf("(%d) Expected columns %v, got %v", i, scenario.expected, columns) + } + + for _, c := range columns { + if !list.ExistInSlice(c, scenario.expected) { + t.Errorf("(%d) Didn't expect column %s", i, c) + } + } + } +} + +func TestDeleteTable(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + tableName string + expectError bool + }{ + {"", true}, + {"test", true}, + {"_admins", false}, + {"demo3", false}, + } + + for i, scenario := range scenarios { + err := app.Dao().DeleteTable(scenario.tableName) + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v", i, scenario.expectError, hasErr) + } + } +} diff --git a/daos/user.go b/daos/user.go new file mode 100644 index 00000000..29fa8f8a --- /dev/null +++ b/daos/user.go @@ -0,0 +1,281 @@ +package daos + +import ( + "database/sql" + "errors" + "fmt" + "log" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/security" +) + +// UserQuery returns a new User model select query. +func (dao *Dao) UserQuery() *dbx.SelectQuery { + return dao.ModelQuery(&models.User{}) +} + +// LoadProfile loads the profile record associated to the provided user. +func (dao *Dao) LoadProfile(user *models.User) error { + collection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName) + if err != nil { + return err + } + + profile, err := dao.FindFirstRecordByData(collection, models.ProfileCollectionUserFieldName, user.Id) + if err != nil && err != sql.ErrNoRows { + return err + } + + user.Profile = profile + + return nil +} + +// LoadProfiles loads the profile records associated to the provied users list. +func (dao *Dao) LoadProfiles(users []*models.User) error { + collection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName) + if err != nil { + return err + } + + // extract user ids + ids := []string{} + usersMap := map[string]*models.User{} + for _, user := range users { + ids = append(ids, user.Id) + usersMap[user.Id] = user + } + + profiles, err := dao.FindRecordsByExpr(collection, dbx.HashExp{ + models.ProfileCollectionUserFieldName: list.ToInterfaceSlice(ids), + }) + if err != nil { + return err + } + + // populate each user.Profile member + for _, profile := range profiles { + userId := profile.GetStringDataValue(models.ProfileCollectionUserFieldName) + user, ok := usersMap[userId] + if !ok { + continue + } + user.Profile = profile + } + + return nil +} + +// FindUserById finds a single User model by its id. +// +// This method also auto loads the related user profile record +// into the found model. +func (dao *Dao) FindUserById(id string) (*models.User, error) { + model := &models.User{} + + err := dao.UserQuery(). + AndWhere(dbx.HashExp{"id": id}). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + // try to load the user profile (if exist) + if err := dao.LoadProfile(model); err != nil { + log.Println(err) + } + + return model, nil +} + +// FindUserByEmail finds a single User model by its email address. +// +// This method also auto loads the related user profile record +// into the found model. +func (dao *Dao) FindUserByEmail(email string) (*models.User, error) { + model := &models.User{} + + err := dao.UserQuery(). + AndWhere(dbx.HashExp{"email": email}). + Limit(1). + One(model) + + if err != nil { + return nil, err + } + + // try to load the user profile (if exist) + if err := dao.LoadProfile(model); err != nil { + log.Println(err) + } + + return model, nil +} + +// FindUserByToken finds the user associated with the provided JWT token. +// Returns an error if the JWT token is invalid or expired. +// +// This method also auto loads the related user profile record +// into the found model. +func (dao *Dao) FindUserByToken(token string, baseTokenKey string) (*models.User, error) { + unverifiedClaims, err := security.ParseUnverifiedJWT(token) + if err != nil { + return nil, err + } + + // check required claims + id, _ := unverifiedClaims["id"].(string) + if id == "" { + return nil, errors.New("Missing or invalid token claims.") + } + + user, err := dao.FindUserById(id) + if err != nil || user == nil { + return nil, err + } + + verificationKey := user.TokenKey + baseTokenKey + + // verify token signature + if _, err := security.ParseJWT(token, verificationKey); err != nil { + return nil, err + } + + return user, nil +} + +// IsUserEmailUnique checks if the provided email address is not +// already in use by other users. +func (dao *Dao) IsUserEmailUnique(email string, excludeId string) bool { + if email == "" { + return false + } + + var exists bool + err := dao.UserQuery(). + Select("count(*)"). + AndWhere(dbx.Not(dbx.HashExp{"id": excludeId})). + AndWhere(dbx.HashExp{"email": email}). + Limit(1). + Row(&exists) + + return err == nil && !exists +} + +// DeleteUser deletes the provided User model. +// +// This method will also cascade the delete operation to all +// Record models that references the provided User model +// (delete or set to NULL, depending on the related user shema field settings). +// +// The delete operation may fail if the user is part of a required +// reference in another Record model (aka. cannot be deleted or set to NULL). +func (dao *Dao) DeleteUser(user *models.User) error { + // fetch related records + // note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction + relatedRecords, err := dao.FindUserRelatedRecords(user) + if err != nil { + return err + } + + return dao.RunInTransaction(func(txDao *Dao) error { + // check if related records has to be deleted (if `CascadeDelete` is set) + // OR + // just unset the user related fields (if they are not required) + // ----------------------------------------------------------- + recordsLoop: + for _, record := range relatedRecords { + var needSave bool + + for _, field := range record.Collection().Schema.Fields() { + if field.Type != schema.FieldTypeUser { + continue // not a user field + } + + ids := record.GetStringSliceDataValue(field.Name) + + // unset the user id + for i := len(ids) - 1; i >= 0; i-- { + if ids[i] == user.Id { + ids = append(ids[:i], ids[i+1:]...) + break + } + } + + options, _ := field.Options.(*schema.UserOptions) + + // cascade delete + // (only if there are no other user references in case of multiple select) + if options.CascadeDelete && len(ids) == 0 { + if err := txDao.DeleteRecord(record); err != nil { + return err + } + // no need to further iterate the user fields (the record is deleted) + continue recordsLoop + } + + if field.Required && len(ids) == 0 { + return fmt.Errorf("Failed delete the user because a record exist with required user reference to the current model (%q, %q).", record.Id, record.Collection().Name) + } + + // apply the reference changes + record.SetDataValue(field.Name, field.PrepareValue(ids)) + needSave = true + } + + if needSave { + if err := txDao.SaveRecord(record); err != nil { + return err + } + } + } + // ----------------------------------------------------------- + + return txDao.Delete(user) + }) +} + +// SaveUser upserts the provided User model. +// +// An empty profile record will be created if the user +// doesn't have a profile record set yet. +func (dao *Dao) SaveUser(user *models.User) error { + profileCollection, err := dao.FindCollectionByNameOrId(models.ProfileCollectionName) + if err != nil { + return err + } + + // fetch the related user profile record (if exist) + var userProfile *models.Record + if user.HasId() { + userProfile, _ = dao.FindFirstRecordByData( + profileCollection, + models.ProfileCollectionUserFieldName, + user.Id, + ) + } + + return dao.RunInTransaction(func(txDao *Dao) error { + if err := txDao.Save(user); err != nil { + return err + } + + // create default/empty profile record if doesn't exist + if userProfile == nil { + userProfile = models.NewRecord(profileCollection) + userProfile.SetDataValue(models.ProfileCollectionUserFieldName, user.Id) + if err := txDao.Save(userProfile); err != nil { + return err + } + user.Profile = userProfile + } + + return nil + }) +} diff --git a/daos/user_test.go b/daos/user_test.go new file mode 100644 index 00000000..8d818dd2 --- /dev/null +++ b/daos/user_test.go @@ -0,0 +1,274 @@ +package daos_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" +) + +func TestUserQuery(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + expected := "SELECT {{_users}}.* FROM `_users`" + + sql := app.Dao().UserQuery().Build().SQL() + if sql != expected { + t.Errorf("Expected sql %s, got %s", expected, sql) + } +} + +func TestLoadProfile(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // try to load missing profile (shouldn't return an error) + // --- + newUser := &models.User{} + err1 := app.Dao().LoadProfile(newUser) + if err1 != nil { + t.Fatalf("Expected nil, got error %v", err1) + } + + // try to load existing profile + // --- + existingUser, _ := app.Dao().FindUserByEmail("test@example.com") + existingUser.Profile = nil // reset + + err2 := app.Dao().LoadProfile(existingUser) + if err2 != nil { + t.Fatal(err2) + } + + if existingUser.Profile == nil { + t.Fatal("Expected user profile to be loaded, got nil") + } + + if existingUser.Profile.GetStringDataValue("name") != "test" { + t.Fatalf("Expected profile.name to be 'test', got %s", existingUser.Profile.GetStringDataValue("name")) + } +} + +func TestLoadProfiles(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + u0 := &models.User{} + u1, _ := app.Dao().FindUserByEmail("test@example.com") + u2, _ := app.Dao().FindUserByEmail("test2@example.com") + + users := []*models.User{u0, u1, u2} + + err := app.Dao().LoadProfiles(users) + if err != nil { + t.Fatal(err) + } + + if u0.Profile != nil { + t.Errorf("Expected profile to be nil for u0, got %v", u0.Profile) + } + if u1.Profile == nil { + t.Errorf("Expected profile to be set for u1, got nil") + } + if u2.Profile == nil { + t.Errorf("Expected profile to be set for u2, got nil") + } +} + +func TestFindUserById(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + id string + expectError bool + }{ + {"00000000-2b4a-a26b-4d01-42d3c3d77bc8", true}, + {"97cc3d3d-6ba2-383f-b42a-7bc84d27410c", false}, + } + + for i, scenario := range scenarios { + user, err := app.Dao().FindUserById(scenario.id) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + } + + if user != nil && user.Id != scenario.id { + t.Errorf("(%d) Expected user with id %s, got %s", i, scenario.id, user.Id) + } + } +} + +func TestFindUserByEmail(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + email string + expectError bool + }{ + {"invalid", true}, + {"missing@example.com", true}, + {"test@example.com", false}, + } + + for i, scenario := range scenarios { + user, err := app.Dao().FindUserByEmail(scenario.email) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + continue + } + + if !scenario.expectError && user.Email != scenario.email { + t.Errorf("(%d) Expected user with email %s, got %s", i, scenario.email, user.Email) + } + } +} + +func TestFindUserByToken(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + token string + baseKey string + expectedEmail string + expectError bool + }{ + // invalid base key (password reset key for auth token) + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + app.Settings().UserPasswordResetToken.Secret, + "", + true, + }, + // expired token + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxNjQwOTkxNjYxfQ.RrSG5NwysI38DEZrIQiz3lUgI6sEuYGTll_jLRbBSiw", + app.Settings().UserAuthToken.Secret, + "", + true, + }, + // valid token + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZXhwIjoxODkzNDc0MDAwfQ.Wq5ac1q1f5WntIzEngXk22ydMj-eFgvfSRg7dhmPKic", + app.Settings().UserAuthToken.Secret, + "test@example.com", + false, + }, + } + + for i, scenario := range scenarios { + user, err := app.Dao().FindUserByToken(scenario.token, scenario.baseKey) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + continue + } + + if !scenario.expectError && user.Email != scenario.expectedEmail { + t.Errorf("(%d) Expected user model %s, got %s", i, scenario.expectedEmail, user.Email) + } + } +} + +func TestIsUserEmailUnique(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + email string + excludeId string + expected bool + }{ + {"", "", false}, + {"test@example.com", "", false}, + {"new@example.com", "", true}, + {"test@example.com", "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", true}, + } + + for i, scenario := range scenarios { + result := app.Dao().IsUserEmailUnique(scenario.email, scenario.excludeId) + if result != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, result) + } + } +} + +func TestDeleteUser(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // try to delete unsaved user + // --- + err1 := app.Dao().DeleteUser(&models.User{}) + if err1 == nil { + t.Fatal("Expected error, got nil") + } + + // try to delete existing user + // --- + user, _ := app.Dao().FindUserByEmail("test3@example.com") + err2 := app.Dao().DeleteUser(user) + if err2 != nil { + t.Fatalf("Expected nil, got error %v", err2) + } + + // check if the delete operation was cascaded to the profiles collection (record delete) + profilesCol, _ := app.Dao().FindCollectionByNameOrId(models.ProfileCollectionName) + profile, _ := app.Dao().FindRecordById(profilesCol, user.Profile.Id, nil) + if profile != nil { + t.Fatalf("Expected user profile to be deleted, got %v", profile) + } + + // check if delete operation was cascaded to the related demo2 collection (null set) + demo2Col, _ := app.Dao().FindCollectionByNameOrId("demo2") + record, _ := app.Dao().FindRecordById(demo2Col, "94568ca2-0bee-49d7-b749-06cb97956fd9", nil) + if record == nil { + t.Fatal("Expected to found related record, got nil") + } + if record.GetStringDataValue("user") != "" { + t.Fatalf("Expected user field to be set to empty string, got %v", record.GetStringDataValue("user")) + } +} + +func TestSaveUser(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create + // --- + u1 := &models.User{} + u1.Email = "new@example.com" + u1.SetPassword("123456") + err1 := app.Dao().SaveUser(u1) + if err1 != nil { + t.Fatal(err1) + } + u1, refreshErr1 := app.Dao().FindUserByEmail("new@example.com") + if refreshErr1 != nil { + t.Fatalf("Expected user with email new@example.com to have been created, got error %v", refreshErr1) + } + if u1.Profile == nil { + t.Fatalf("Expected creating a user to create also an empty profile record") + } + + // update + // --- + u2, _ := app.Dao().FindUserByEmail("test@example.com") + u2.Email = "test_update@example.com" + err2 := app.Dao().SaveUser(u2) + if err2 != nil { + t.Fatal(err2) + } + u2, refreshErr2 := app.Dao().FindUserByEmail("test_update@example.com") + if u2 == nil { + t.Fatalf("Couldn't find user with email test_update@example.com (%v)", refreshErr2) + } +} diff --git a/examples/base/main.go b/examples/base/main.go new file mode 100644 index 00000000..04b452be --- /dev/null +++ b/examples/base/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" +) + +func main() { + app := pocketbase.New() + + app.OnBeforeServe().Add(func(e *core.ServeEvent) error { + // serves static files from the provided public dir (if exists) + subFs := echo.MustSubFS(e.Router.Filesystem, "pb_public") + e.Router.GET("/*", apis.StaticDirectoryHandler(subFs, false)) + + return nil + }) + + if err := app.Start(); err != nil { + log.Fatal(err) + } +} diff --git a/forms/admin_login.go b/forms/admin_login.go new file mode 100644 index 00000000..842552ed --- /dev/null +++ b/forms/admin_login.go @@ -0,0 +1,50 @@ +package forms + +import ( + "errors" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" +) + +// AdminLogin defines an admin email/pass login form. +type AdminLogin struct { + app core.App + + Email string `form:"email" json:"email"` + Password string `form:"password" json:"password"` +} + +// NewAdminLogin creates new admin login form for the provided app. +func NewAdminLogin(app core.App) *AdminLogin { + return &AdminLogin{app: app} +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *AdminLogin) Validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.Email), + validation.Field(&form.Password, validation.Required, validation.Length(1, 255)), + ) +} + +// Submit validates and submits the admin form. +// On success returns the authorized admin model. +func (form *AdminLogin) Submit() (*models.Admin, error) { + if err := form.Validate(); err != nil { + return nil, err + } + + admin, err := form.app.Dao().FindAdminByEmail(form.Email) + if err != nil { + return nil, err + } + + if admin.ValidatePassword(form.Password) { + return admin, nil + } + + return nil, errors.New("Invalid login credentials.") +} diff --git a/forms/admin_login_test.go b/forms/admin_login_test.go new file mode 100644 index 00000000..9e5d09c8 --- /dev/null +++ b/forms/admin_login_test.go @@ -0,0 +1,80 @@ +package forms_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" +) + +func TestAdminLoginValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + form := forms.NewAdminLogin(app) + + scenarios := []struct { + email string + password string + expectError bool + }{ + {"", "", true}, + {"", "123", true}, + {"test@example.com", "", true}, + {"test", "123", true}, + {"test@example.com", "123", false}, + } + + for i, s := range scenarios { + form.Email = s.email + form.Password = s.password + + err := form.Validate() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + } +} + +func TestAdminLoginSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + form := forms.NewAdminLogin(app) + + scenarios := []struct { + email string + password string + expectError bool + }{ + {"", "", true}, + {"", "1234567890", true}, + {"test@example.com", "", true}, + {"test", "1234567890", true}, + {"missing@example.com", "1234567890", true}, + {"test@example.com", "123456789", true}, + {"test@example.com", "1234567890", false}, + } + + for i, s := range scenarios { + form.Email = s.email + form.Password = s.password + + admin, err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + if !s.expectError && admin == nil { + t.Errorf("(%d) Expected admin model to be returned, got nil", i) + } + + if admin != nil && admin.Email != s.email { + t.Errorf("(%d) Expected admin with email %s to be returned, got %v", i, s.email, admin) + } + } +} diff --git a/forms/admin_password_reset_confirm.go b/forms/admin_password_reset_confirm.go new file mode 100644 index 00000000..759bc77d --- /dev/null +++ b/forms/admin_password_reset_confirm.go @@ -0,0 +1,76 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms/validators" + "github.com/pocketbase/pocketbase/models" +) + +// AdminPasswordResetConfirm defines an admin password reset confirmation form. +type AdminPasswordResetConfirm struct { + app core.App + + Token string `form:"token" json:"token"` + Password string `form:"password" json:"password"` + PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` +} + +// NewAdminPasswordResetConfirm creates new admin password reset confirmation form. +func NewAdminPasswordResetConfirm(app core.App) *AdminPasswordResetConfirm { + return &AdminPasswordResetConfirm{ + app: app, + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *AdminPasswordResetConfirm) Validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), + validation.Field(&form.Password, validation.Required, validation.Length(10, 100)), + validation.Field(&form.PasswordConfirm, validation.Required, validation.By(validators.Compare(form.Password))), + ) +} + +func (form *AdminPasswordResetConfirm) checkToken(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + admin, err := form.app.Dao().FindAdminByToken( + v, + form.app.Settings().AdminPasswordResetToken.Secret, + ) + if err != nil || admin == nil { + return validation.NewError("validation_invalid_token", "Invalid or expired token.") + } + + return nil +} + +// Submit validates and submits the admin password reset confirmation form. +// On success returns the updated admin model associated to `form.Token`. +func (form *AdminPasswordResetConfirm) Submit() (*models.Admin, error) { + if err := form.Validate(); err != nil { + return nil, err + } + + admin, err := form.app.Dao().FindAdminByToken( + form.Token, + form.app.Settings().AdminPasswordResetToken.Secret, + ) + if err != nil { + return nil, err + } + + if err := admin.SetPassword(form.Password); err != nil { + return nil, err + } + + if err := form.app.Dao().SaveAdmin(admin); err != nil { + return nil, err + } + + return admin, nil +} diff --git a/forms/admin_password_reset_confirm_test.go b/forms/admin_password_reset_confirm_test.go new file mode 100644 index 00000000..ebdaed78 --- /dev/null +++ b/forms/admin_password_reset_confirm_test.go @@ -0,0 +1,120 @@ +package forms_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestAdminPasswordResetConfirmValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + form := forms.NewAdminPasswordResetConfirm(app) + + scenarios := []struct { + token string + password string + passwordConfirm string + expectError bool + }{ + {"", "", "", true}, + {"", "123", "", true}, + {"", "", "123", true}, + {"test", "", "", true}, + {"test", "123", "", true}, + {"test", "123", "123", true}, + { + // expired + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", + "1234567890", + "1234567890", + true, + }, + { + // valid + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg5MzQ3NDAwMH0.72IhlL_5CpNGE0ZKM7sV9aAKa3wxQaMZdDiHBo0orpw", + "1234567890", + "1234567890", + false, + }, + } + + for i, s := range scenarios { + form.Token = s.token + form.Password = s.password + form.PasswordConfirm = s.passwordConfirm + + err := form.Validate() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + } +} + +func TestAdminPasswordResetConfirmSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + form := forms.NewAdminPasswordResetConfirm(app) + + scenarios := []struct { + token string + password string + passwordConfirm string + expectError bool + }{ + {"", "", "", true}, + {"", "123", "", true}, + {"", "", "123", true}, + {"test", "", "", true}, + {"test", "123", "", true}, + {"test", "123", "123", true}, + { + // expired + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTY0MTAxMzIwMH0.Gp_1b5WVhqjj2o3nJhNUlJmpdiwFLXN72LbMP-26gjA", + "1234567890", + "1234567890", + true, + }, + { + // valid + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNGE5N2NjLTNmODMtNGQwMS1hMjZiLTNkNzdiYzg0MmQzYyIsInR5cGUiOiJhZG1pbiIsImV4cCI6MTg5MzQ3NDAwMH0.72IhlL_5CpNGE0ZKM7sV9aAKa3wxQaMZdDiHBo0orpw", + "1234567890", + "1234567890", + false, + }, + } + + for i, s := range scenarios { + form.Token = s.token + form.Password = s.password + form.PasswordConfirm = s.passwordConfirm + + admin, err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + if s.expectError { + continue + } + + claims, _ := security.ParseUnverifiedJWT(s.token) + tokenAdminId, _ := claims["id"] + + if admin.Id != tokenAdminId { + t.Errorf("(%d) Expected admin with id %s to be returned, got %v", i, tokenAdminId, admin) + } + + if !admin.ValidatePassword(form.Password) { + t.Errorf("(%d) Expected the admin password to have been updated to %q", i, form.Password) + } + } +} diff --git a/forms/admin_password_reset_request.go b/forms/admin_password_reset_request.go new file mode 100644 index 00000000..283b734e --- /dev/null +++ b/forms/admin_password_reset_request.go @@ -0,0 +1,70 @@ +package forms + +import ( + "errors" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tools/types" +) + +// AdminPasswordResetRequest defines an admin password reset request form. +type AdminPasswordResetRequest struct { + app core.App + resendThreshold float64 + + Email string `form:"email" json:"email"` +} + +// NewAdminPasswordResetRequest creates new admin password reset request form. +func NewAdminPasswordResetRequest(app core.App) *AdminPasswordResetRequest { + return &AdminPasswordResetRequest{ + app: app, + resendThreshold: 120, // 2 min + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +// +// This method doesn't verify that admin with `form.Email` exists (this is done on Submit). +func (form *AdminPasswordResetRequest) Validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.Email, + validation.Required, + validation.Length(1, 255), + is.Email, + ), + ) +} + +// Submit validates and submits the form. +// On success sends a password reset email to the `form.Email` admin. +func (form *AdminPasswordResetRequest) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + admin, err := form.app.Dao().FindAdminByEmail(form.Email) + if err != nil { + return err + } + + now := time.Now().UTC() + lastResetSentAt := admin.LastResetSentAt.Time() + if now.Sub(lastResetSentAt).Seconds() < form.resendThreshold { + return errors.New("You have already requested a password reset.") + } + + if err := mails.SendAdminPasswordReset(form.app, admin); err != nil { + return err + } + + // update last sent timestamp + admin.LastResetSentAt = types.NowDateTime() + + return form.app.Dao().SaveAdmin(admin) +} diff --git a/forms/admin_password_reset_request_test.go b/forms/admin_password_reset_request_test.go new file mode 100644 index 00000000..245d48b0 --- /dev/null +++ b/forms/admin_password_reset_request_test.go @@ -0,0 +1,84 @@ +package forms_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" +) + +func TestAdminPasswordResetRequestValidate(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + form := forms.NewAdminPasswordResetRequest(testApp) + + scenarios := []struct { + email string + expectError bool + }{ + {"", true}, + {"", true}, + {"invalid", true}, + {"missing@example.com", false}, // doesn't check for existing admin + {"test@example.com", false}, + } + + for i, s := range scenarios { + form.Email = s.email + + err := form.Validate() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + } +} + +func TestAdminPasswordResetRequestSubmit(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + form := forms.NewAdminPasswordResetRequest(testApp) + + scenarios := []struct { + email string + expectError bool + }{ + {"", true}, + {"", true}, + {"invalid", true}, + {"missing@example.com", true}, + {"test@example.com", false}, + {"test@example.com", true}, // already requested + } + + for i, s := range scenarios { + testApp.TestMailer.TotalSend = 0 // reset + form.Email = s.email + + adminBefore, _ := testApp.Dao().FindAdminByEmail(s.email) + + err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + adminAfter, _ := testApp.Dao().FindAdminByEmail(s.email) + + if !s.expectError && (adminBefore.LastResetSentAt == adminAfter.LastResetSentAt || adminAfter.LastResetSentAt.IsZero()) { + t.Errorf("(%d) Expected admin.LastResetSentAt to change, got %q", i, adminAfter.LastResetSentAt) + } + + expectedMails := 1 + if s.expectError { + expectedMails = 0 + } + if testApp.TestMailer.TotalSend != expectedMails { + t.Errorf("(%d) Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) + } + } +} diff --git a/forms/admin_upsert.go b/forms/admin_upsert.go new file mode 100644 index 00000000..0932a4e3 --- /dev/null +++ b/forms/admin_upsert.go @@ -0,0 +1,91 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms/validators" + "github.com/pocketbase/pocketbase/models" +) + +// AdminUpsert defines an admin upsert (create/update) form. +type AdminUpsert struct { + app core.App + admin *models.Admin + isCreate bool + + Avatar int `form:"avatar" json:"avatar"` + Email string `form:"email" json:"email"` + Password string `form:"password" json:"password"` + PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` +} + +// NewAdminUpsert creates new upsert form for the provided admin model +// (pass an empty admin model instance (`&models.Admin{}`) for create). +func NewAdminUpsert(app core.App, admin *models.Admin) *AdminUpsert { + form := &AdminUpsert{ + app: app, + admin: admin, + isCreate: !admin.HasId(), + } + + // load defaults + form.Avatar = admin.Avatar + form.Email = admin.Email + + return form +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *AdminUpsert) Validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.Avatar, + validation.Min(0), + validation.Max(9), + ), + validation.Field( + &form.Email, + validation.Required, + validation.Length(1, 255), + is.Email, + validation.By(form.checkUniqueEmail), + ), + validation.Field( + &form.Password, + validation.When(form.isCreate, validation.Required), + validation.Length(10, 100), + ), + validation.Field( + &form.PasswordConfirm, + validation.When(form.Password != "", validation.Required), + validation.By(validators.Compare(form.Password)), + ), + ) +} + +func (form *AdminUpsert) checkUniqueEmail(value any) error { + v, _ := value.(string) + + if form.app.Dao().IsAdminEmailUnique(v, form.admin.Id) { + return nil + } + + return validation.NewError("validation_admin_email_exists", "Admin email already exists.") +} + +// Submit validates the form and upserts the form's admin model. +func (form *AdminUpsert) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + form.admin.Avatar = form.Avatar + form.admin.Email = form.Email + + if form.Password != "" { + form.admin.SetPassword(form.Password) + } + + return form.app.Dao().SaveAdmin(form.admin) +} diff --git a/forms/admin_upsert_test.go b/forms/admin_upsert_test.go new file mode 100644 index 00000000..c7305283 --- /dev/null +++ b/forms/admin_upsert_test.go @@ -0,0 +1,285 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" +) + +func TestNewAdminUpsert(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + admin := &models.Admin{} + admin.Avatar = 3 + admin.Email = "new@example.com" + + form := forms.NewAdminUpsert(app, admin) + + // test defaults + if form.Avatar != admin.Avatar { + t.Errorf("Expected Avatar %d, got %d", admin.Avatar, form.Avatar) + } + if form.Email != admin.Email { + t.Errorf("Expected Email %q, got %q", admin.Email, form.Email) + } +} + +func TestAdminUpsertValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + id string + avatar int + email string + password string + passwordConfirm string + expectedErrors int + }{ + { + "", + -1, + "", + "", + "", + 3, + }, + { + "", + 10, + "invalid", + "12345678", + "87654321", + 4, + }, + { + // existing email + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + 3, + "test2@example.com", + "1234567890", + "1234567890", + 1, + }, + { + // mismatching passwords + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + 3, + "test@example.com", + "1234567890", + "1234567891", + 1, + }, + { + // create without setting password + "", + 9, + "test_create@example.com", + "", + "", + 1, + }, + { + // create with existing email + "", + 9, + "test@example.com", + "1234567890!", + "1234567890!", + 1, + }, + { + // update without setting password + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + 3, + "test_update@example.com", + "", + "", + 0, + }, + { + // create with password + "", + 9, + "test_create@example.com", + "1234567890!", + "1234567890!", + 0, + }, + { + // update with password + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + 4, + "test_update@example.com", + "1234567890", + "1234567890", + 0, + }, + } + + for i, s := range scenarios { + admin := &models.Admin{} + if s.id != "" { + admin, _ = app.Dao().FindAdminById(s.id) + } + + form := forms.NewAdminUpsert(app, admin) + form.Avatar = s.avatar + form.Email = s.email + form.Password = s.password + form.PasswordConfirm = s.passwordConfirm + + result := form.Validate() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + if len(errs) != s.expectedErrors { + t.Errorf("(%d) Expected %d errors, got %d (%v)", i, s.expectedErrors, len(errs), errs) + } + } +} + +func TestAdminUpsertSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + id string + jsonData string + expectError bool + }{ + { + // create empty + "", + `{}`, + true, + }, + { + // update empty + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + `{}`, + false, + }, + { + // create failure - existing email + "", + `{ + "email": "test@example.com", + "password": "1234567890", + "passwordConfirm": "1234567890" + }`, + true, + }, + { + // create failure - passwords mismatch + "", + `{ + "email": "test_new@example.com", + "password": "1234567890", + "passwordConfirm": "1234567891" + }`, + true, + }, + { + // create success + "", + `{ + "email": "test_new@example.com", + "password": "1234567890", + "passwordConfirm": "1234567890" + }`, + false, + }, + { + // update failure - existing email + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + `{ + "email": "test2@example.com" + }`, + true, + }, + { + // update failure - mismatching passwords + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + `{ + "password": "1234567890", + "passwordConfirm": "1234567891" + }`, + true, + }, + { + // update succcess - new email + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + `{ + "email": "test_update@example.com" + }`, + false, + }, + { + // update succcess - new password + "2b4a97cc-3f83-4d01-a26b-3d77bc842d3c", + `{ + "password": "1234567890", + "passwordConfirm": "1234567890" + }`, + false, + }, + } + + for i, s := range scenarios { + isCreate := true + admin := &models.Admin{} + if s.id != "" { + isCreate = false + admin, _ = app.Dao().FindAdminById(s.id) + } + initialTokenKey := admin.TokenKey + + form := forms.NewAdminUpsert(app, admin) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + foundAdmin, _ := app.Dao().FindAdminByEmail(form.Email) + + if !s.expectError && isCreate && foundAdmin == nil { + t.Errorf("(%d) Expected admin to be created, got nil", i) + continue + } + + if s.expectError { + continue // skip persistence check + } + + if foundAdmin.Email != form.Email { + t.Errorf("(%d) Expected email %s, got %s", i, form.Email, foundAdmin.Email) + } + + if foundAdmin.Avatar != form.Avatar { + t.Errorf("(%d) Expected avatar %d, got %d", i, form.Avatar, foundAdmin.Avatar) + } + + if form.Password != "" && initialTokenKey == foundAdmin.TokenKey { + t.Errorf("(%d) Expected token key to be renewed when setting a new password", i) + } + } +} diff --git a/forms/collection_upsert.go b/forms/collection_upsert.go new file mode 100644 index 00000000..6aaf44be --- /dev/null +++ b/forms/collection_upsert.go @@ -0,0 +1,215 @@ +package forms + +import ( + "regexp" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/resolvers" + "github.com/pocketbase/pocketbase/tools/search" +) + +var collectionNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_]*$`) + +// CollectionUpsert defines a collection upsert (create/update) form. +type CollectionUpsert struct { + app core.App + collection *models.Collection + isCreate bool + + Name string `form:"name" json:"name"` + System bool `form:"system" json:"system"` + Schema schema.Schema `form:"schema" json:"schema"` + ListRule *string `form:"listRule" json:"listRule"` + ViewRule *string `form:"viewRule" json:"viewRule"` + CreateRule *string `form:"createRule" json:"createRule"` + UpdateRule *string `form:"updateRule" json:"updateRule"` + DeleteRule *string `form:"deleteRule" json:"deleteRule"` +} + +// NewCollectionUpsert creates new collection upsert form for the provided Collection model +// (pass an empty Collection model instance (`&models.Collection{}`) for create). +func NewCollectionUpsert(app core.App, collection *models.Collection) *CollectionUpsert { + form := &CollectionUpsert{ + app: app, + collection: collection, + isCreate: !collection.HasId(), + } + + // load defaults + form.Name = collection.Name + form.System = collection.System + form.ListRule = collection.ListRule + form.ViewRule = collection.ViewRule + form.CreateRule = collection.CreateRule + form.UpdateRule = collection.UpdateRule + form.DeleteRule = collection.DeleteRule + + clone, _ := collection.Schema.Clone() + if clone != nil { + form.Schema = *clone + } else { + form.Schema = schema.Schema{} + } + + return form +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *CollectionUpsert) Validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.System, + validation.By(form.ensureNoSystemFlagChange), + ), + validation.Field( + &form.Name, + validation.Required, + validation.Length(1, 255), + validation.Match(collectionNameRegex), + validation.By(form.ensureNoSystemNameChange), + validation.By(form.checkUniqueName), + ), + // validates using the type's own validation rules + some collection's specific + validation.Field( + &form.Schema, + validation.By(form.ensureNoSystemFieldsChange), + validation.By(form.ensureNoFieldsTypeChange), + validation.By(form.ensureNoFieldsNameReuse), + ), + validation.Field(&form.ListRule, validation.By(form.checkRule)), + validation.Field(&form.ViewRule, validation.By(form.checkRule)), + validation.Field(&form.CreateRule, validation.By(form.checkRule)), + validation.Field(&form.UpdateRule, validation.By(form.checkRule)), + validation.Field(&form.DeleteRule, validation.By(form.checkRule)), + ) +} + +func (form *CollectionUpsert) checkUniqueName(value any) error { + v, _ := value.(string) + + if !form.app.Dao().IsCollectionNameUnique(v, form.collection.Id) { + return validation.NewError("validation_collection_name_exists", "Collection name must be unique (case insensitive).") + } + + if (form.isCreate || strings.ToLower(v) != strings.ToLower(form.collection.Name)) && form.app.Dao().HasTable(v) { + return validation.NewError("validation_collection_name_table_exists", "The collection name must be also unique table name.") + } + + return nil +} + +func (form *CollectionUpsert) ensureNoSystemNameChange(value any) error { + v, _ := value.(string) + + if form.isCreate || !form.collection.System || v == form.collection.Name { + return nil + } + + return validation.NewError("validation_system_collection_name_change", "System collections cannot be renamed.") +} + +func (form *CollectionUpsert) ensureNoSystemFlagChange(value any) error { + v, _ := value.(bool) + + if form.isCreate || v == form.collection.System { + return nil + } + + return validation.NewError("validation_system_collection_flag_change", "System collection state cannot be changed.") +} + +func (form *CollectionUpsert) ensureNoFieldsTypeChange(value any) error { + v, _ := value.(schema.Schema) + + for _, field := range v.Fields() { + oldField := form.collection.Schema.GetFieldById(field.Id) + + if oldField != nil && oldField.Type != field.Type { + return validation.NewError("validation_field_type_change", "Field type cannot be changed.") + } + } + + return nil +} + +func (form *CollectionUpsert) ensureNoSystemFieldsChange(value any) error { + v, _ := value.(schema.Schema) + + for _, oldField := range form.collection.Schema.Fields() { + if !oldField.System { + continue + } + + newField := v.GetFieldById(oldField.Id) + + if newField == nil || oldField.String() != newField.String() { + return validation.NewError("validation_system_field_change", "System fields cannot be deleted or changed.") + } + } + + return nil +} + +func (form *CollectionUpsert) ensureNoFieldsNameReuse(value any) error { + v, _ := value.(schema.Schema) + + for _, field := range v.Fields() { + oldField := form.collection.Schema.GetFieldByName(field.Name) + + if oldField != nil && oldField.Id != field.Id { + return validation.NewError("validation_field_old_field_exist", "Cannot use existing schema field names when renaming fields.") + } + } + + return nil +} + +func (form *CollectionUpsert) checkRule(value any) error { + v, _ := value.(*string) + + if v == nil || *v == "" { + return nil // nothing to check + } + + dummy := &models.Collection{Schema: form.Schema} + r := resolvers.NewRecordFieldResolver(form.app.Dao(), dummy, nil) + + _, err := search.FilterData(*v).BuildExpr(r) + if err != nil { + return validation.NewError("validation_collection_rule", "Invalid filter rule.") + } + + return nil +} + +// Submit validates the form and upserts the form's Collection model. +// +// On success the related record table schema will be auto updated. +func (form *CollectionUpsert) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + // system flag can be set only for create + if form.isCreate { + form.collection.System = form.System + } + + // system collections cannot be renamed + if form.isCreate || !form.collection.System { + form.collection.Name = form.Name + } + + form.collection.Schema = form.Schema + form.collection.ListRule = form.ListRule + form.collection.ViewRule = form.ViewRule + form.collection.CreateRule = form.CreateRule + form.collection.UpdateRule = form.UpdateRule + form.collection.DeleteRule = form.DeleteRule + + return form.app.Dao().SaveCollection(form.collection) +} diff --git a/forms/collection_upsert_test.go b/forms/collection_upsert_test.go new file mode 100644 index 00000000..ec1a9e0d --- /dev/null +++ b/forms/collection_upsert_test.go @@ -0,0 +1,452 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tests" + "github.com/spf13/cast" +) + +func TestNewCollectionUpsert(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection := &models.Collection{} + collection.Name = "test" + collection.System = true + listRule := "testview" + collection.ListRule = &listRule + viewRule := "test_view" + collection.ViewRule = &viewRule + createRule := "test_create" + collection.CreateRule = &createRule + updateRule := "test_update" + collection.UpdateRule = &updateRule + deleteRule := "test_delete" + collection.DeleteRule = &deleteRule + collection.Schema = schema.NewSchema(&schema.SchemaField{ + Name: "test", + Type: schema.FieldTypeText, + }) + + form := forms.NewCollectionUpsert(app, collection) + + if form.Name != collection.Name { + t.Errorf("Expected Name %q, got %q", collection.Name, form.Name) + } + + if form.System != collection.System { + t.Errorf("Expected System %v, got %v", collection.System, form.System) + } + + if form.ListRule != collection.ListRule { + t.Errorf("Expected ListRule %v, got %v", collection.ListRule, form.ListRule) + } + + if form.ViewRule != collection.ViewRule { + t.Errorf("Expected ViewRule %v, got %v", collection.ViewRule, form.ViewRule) + } + + if form.CreateRule != collection.CreateRule { + t.Errorf("Expected CreateRule %v, got %v", collection.CreateRule, form.CreateRule) + } + + if form.UpdateRule != collection.UpdateRule { + t.Errorf("Expected UpdateRule %v, got %v", collection.UpdateRule, form.UpdateRule) + } + + if form.DeleteRule != collection.DeleteRule { + t.Errorf("Expected DeleteRule %v, got %v", collection.DeleteRule, form.DeleteRule) + } + + // store previous state and modify the collection schema to verify + // that the form.Schema is a deep clone + loadedSchema, _ := collection.Schema.MarshalJSON() + collection.Schema.AddField(&schema.SchemaField{ + Name: "new_field", + Type: schema.FieldTypeBool, + }) + + formSchema, _ := form.Schema.MarshalJSON() + + if string(formSchema) != string(loadedSchema) { + t.Errorf("Expected Schema %v, got %v", string(loadedSchema), string(formSchema)) + } +} + +func TestCollectionUpsertValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + {"{}", []string{"name", "schema"}}, + { + `{ + "name": "test ?!@#$", + "system": true, + "schema": [ + {"name":"","type":"text"} + ], + "listRule": "missing = '123'", + "viewRule": "missing = '123'", + "createRule": "missing = '123'", + "updateRule": "missing = '123'", + "deleteRule": "missing = '123'" + }`, + []string{"name", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule"}, + }, + { + `{ + "name": "test", + "system": true, + "schema": [ + {"name":"test","type":"text"} + ], + "listRule": "test='123'", + "viewRule": "test='123'", + "createRule": "test='123'", + "updateRule": "test='123'", + "deleteRule": "test='123'" + }`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewCollectionUpsert(app, &models.Collection{}) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Validate() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +func TestCollectionUpsertSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + existingName string + jsonData string + expectedErrors []string + }{ + // empty create + {"", "{}", []string{"name", "schema"}}, + // empty update + {"demo", "{}", []string{}}, + // create failure + { + "", + `{ + "name": "test ?!@#$", + "system": true, + "schema": [ + {"name":"","type":"text"} + ], + "listRule": "missing = '123'", + "viewRule": "missing = '123'", + "createRule": "missing = '123'", + "updateRule": "missing = '123'", + "deleteRule": "missing = '123'" + }`, + []string{"name", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule"}, + }, + // create failure - existing name + { + "", + `{ + "name": "demo", + "system": true, + "schema": [ + {"name":"test","type":"text"} + ], + "listRule": "test='123'", + "viewRule": "test='123'", + "createRule": "test='123'", + "updateRule": "test='123'", + "deleteRule": "test='123'" + }`, + []string{"name"}, + }, + // create failure - existing internal table + { + "", + `{ + "name": "_users", + "schema": [ + {"name":"test","type":"text"} + ] + }`, + []string{"name"}, + }, + // create failure - name starting with underscore + { + "", + `{ + "name": "_test_new", + "schema": [ + {"name":"test","type":"text"} + ] + }`, + []string{"name"}, + }, + // create failure - duplicated field names (case insensitive) + { + "", + `{ + "name": "test_new", + "schema": [ + {"name":"test","type":"text"}, + {"name":"tESt","type":"text"} + ] + }`, + []string{"schema"}, + }, + // create success + { + "", + `{ + "name": "test_new", + "system": true, + "schema": [ + {"id":"a123456","name":"test1","type":"text"}, + {"id":"b123456","name":"test2","type":"email"} + ], + "listRule": "test1='123'", + "viewRule": "test1='123'", + "createRule": "test1='123'", + "updateRule": "test1='123'", + "deleteRule": "test1='123'" + }`, + []string{}, + }, + // update failure - changing field type + { + "test_new", + `{ + "schema": [ + {"id":"a123456","name":"test1","type":"url"}, + {"id":"b123456","name":"test2","type":"bool"} + ] + }`, + []string{"schema"}, + }, + // update failure - rename fields to existing field names (aka. reusing field names) + { + "test_new", + `{ + "schema": [ + {"id":"a123456","name":"test2","type":"text"}, + {"id":"b123456","name":"test1","type":"email"} + ] + }`, + []string{"schema"}, + }, + // update failure - existing name + { + "demo", + `{"name": "demo2"}`, + []string{"name"}, + }, + // update failure - changing system collection + { + models.ProfileCollectionName, + `{ + "name": "update", + "system": false, + "schema": [ + {"id":"koih1lqx","name":"userId","type":"text"} + ], + "listRule": "userId = '123'", + "viewRule": "userId = '123'", + "createRule": "userId = '123'", + "updateRule": "userId = '123'", + "deleteRule": "userId = '123'" + }`, + []string{"name", "system", "schema"}, + }, + // update failure - all fields + { + "demo", + `{ + "name": "test ?!@#$", + "system": true, + "schema": [ + {"name":"","type":"text"} + ], + "listRule": "missing = '123'", + "viewRule": "missing = '123'", + "createRule": "missing = '123'", + "updateRule": "missing = '123'", + "deleteRule": "missing = '123'" + }`, + []string{"name", "system", "schema", "listRule", "viewRule", "createRule", "updateRule", "deleteRule"}, + }, + // update success - update all fields + { + "demo", + `{ + "name": "demo_update", + "schema": [ + {"id":"_2hlxbmp","name":"test","type":"text"} + ], + "listRule": "test='123'", + "viewRule": "test='123'", + "createRule": "test='123'", + "updateRule": "test='123'", + "deleteRule": "test='123'" + }`, + []string{}, + }, + // update failure - rename the schema field of the last updated collection + // (fail due to filters old field references) + { + "demo_update", + `{ + "schema": [ + {"id":"_2hlxbmp","name":"test_renamed","type":"text"} + ] + }`, + []string{"listRule", "viewRule", "createRule", "updateRule", "deleteRule"}, + }, + // update success - rename the schema field of the last updated collection + // (cleared filter references) + { + "demo_update", + `{ + "schema": [ + {"id":"_2hlxbmp","name":"test_renamed","type":"text"} + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null + }`, + []string{}, + }, + // update success - system collection + { + models.ProfileCollectionName, + `{ + "listRule": "userId='123'", + "viewRule": "userId='123'", + "createRule": "userId='123'", + "updateRule": "userId='123'", + "deleteRule": "userId='123'" + }`, + []string{}, + }, + } + + for i, s := range scenarios { + collection := &models.Collection{} + if s.existingName != "" { + var err error + collection, err = app.Dao().FindCollectionByNameOrId(s.existingName) + if err != nil { + t.Fatal(err) + } + } + + form := forms.NewCollectionUpsert(app, collection) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Submit() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + + if len(s.expectedErrors) > 0 { + continue + } + + collection, _ = app.Dao().FindCollectionByNameOrId(form.Name) + if collection == nil { + t.Errorf("(%d) Expected to find collection %q, got nil", i, form.Name) + continue + } + + if form.Name != collection.Name { + t.Errorf("(%d) Expected Name %q, got %q", i, collection.Name, form.Name) + } + + if form.System != collection.System { + t.Errorf("(%d) Expected System %v, got %v", i, collection.System, form.System) + } + + if cast.ToString(form.ListRule) != cast.ToString(collection.ListRule) { + t.Errorf("(%d) Expected ListRule %v, got %v", i, collection.ListRule, form.ListRule) + } + + if cast.ToString(form.ViewRule) != cast.ToString(collection.ViewRule) { + t.Errorf("(%d) Expected ViewRule %v, got %v", i, collection.ViewRule, form.ViewRule) + } + + if cast.ToString(form.CreateRule) != cast.ToString(collection.CreateRule) { + t.Errorf("(%d) Expected CreateRule %v, got %v", i, collection.CreateRule, form.CreateRule) + } + + if cast.ToString(form.UpdateRule) != cast.ToString(collection.UpdateRule) { + t.Errorf("(%d) Expected UpdateRule %v, got %v", i, collection.UpdateRule, form.UpdateRule) + } + + if cast.ToString(form.DeleteRule) != cast.ToString(collection.DeleteRule) { + t.Errorf("(%d) Expected DeleteRule %v, got %v", i, collection.DeleteRule, form.DeleteRule) + } + + formSchema, _ := form.Schema.MarshalJSON() + collectionSchema, _ := collection.Schema.MarshalJSON() + if string(formSchema) != string(collectionSchema) { + t.Errorf("(%d) Expected Schema %v, got %v", i, string(collectionSchema), string(formSchema)) + } + } +} diff --git a/forms/realtime_subscribe.go b/forms/realtime_subscribe.go new file mode 100644 index 00000000..8d3cabcf --- /dev/null +++ b/forms/realtime_subscribe.go @@ -0,0 +1,23 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +// RealtimeSubscribe defines a RealtimeSubscribe request form. +type RealtimeSubscribe struct { + ClientId string `form:"clientId" json:"clientId"` + Subscriptions []string `form:"subscriptions" json:"subscriptions"` +} + +// NewRealtimeSubscribe creates new RealtimeSubscribe request form. +func NewRealtimeSubscribe() *RealtimeSubscribe { + return &RealtimeSubscribe{} +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *RealtimeSubscribe) Validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.ClientId, validation.Required, validation.Length(1, 255)), + ) +} diff --git a/forms/realtime_subscribe_test.go b/forms/realtime_subscribe_test.go new file mode 100644 index 00000000..d1df830b --- /dev/null +++ b/forms/realtime_subscribe_test.go @@ -0,0 +1,31 @@ +package forms_test + +import ( + "strings" + "testing" + + "github.com/pocketbase/pocketbase/forms" +) + +func TestRealtimeSubscribeValidate(t *testing.T) { + scenarios := []struct { + clientId string + expectError bool + }{ + {"", true}, + {strings.Repeat("a", 256), true}, + {"test", false}, + } + + for i, s := range scenarios { + form := forms.NewRealtimeSubscribe() + form.ClientId = s.clientId + + err := form.Validate() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + } +} diff --git a/forms/record_upsert.go b/forms/record_upsert.go new file mode 100644 index 00000000..05312358 --- /dev/null +++ b/forms/record_upsert.go @@ -0,0 +1,368 @@ +package forms + +import ( + "encoding/json" + "errors" + "net/http" + "regexp" + "strconv" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/forms/validators" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/spf13/cast" +) + +// RecordUpsert defines a Record upsert form. +type RecordUpsert struct { + app core.App + record *models.Record + + isCreate bool + filesToDelete []string // names list + filesToUpload []*rest.UploadedFile + + Data map[string]any `json:"data"` +} + +// NewRecordUpsert creates a new Record upsert form. +// (pass a new Record model instance (`models.NewRecord(...)`) for create). +func NewRecordUpsert(app core.App, record *models.Record) *RecordUpsert { + form := &RecordUpsert{ + app: app, + record: record, + isCreate: !record.HasId(), + filesToDelete: []string{}, + filesToUpload: []*rest.UploadedFile{}, + } + + form.Data = map[string]any{} + for _, field := range record.Collection().Schema.Fields() { + form.Data[field.Name] = record.GetDataValue(field.Name) + + } + + return form +} + +func (form *RecordUpsert) getContentType(r *http.Request) string { + t := r.Header.Get("Content-Type") + for i, c := range t { + if c == ' ' || c == ';' { + return t[:i] + } + } + return t +} + +func (form *RecordUpsert) extractRequestData(r *http.Request) (map[string]any, error) { + switch form.getContentType(r) { + case "application/json": + return form.extractJsonData(r) + case "multipart/form-data": + return form.extractMultipartFormData(r) + default: + return nil, errors.New("Unsupported request Content-Type.") + } +} + +func (form *RecordUpsert) extractJsonData(r *http.Request) (map[string]any, error) { + result := map[string]any{} + + err := rest.ReadJsonBodyCopy(r, &result) + + return result, err +} + +func (form *RecordUpsert) extractMultipartFormData(r *http.Request) (map[string]any, error) { + result := map[string]any{} + + // parse form data (if not already) + if err := r.ParseMultipartForm(rest.DefaultMaxMemory); err != nil { + return result, err + } + + arrayValueSupportTypes := schema.ArraybleFieldTypes() + + for key, values := range r.PostForm { + if len(values) == 0 { + result[key] = nil + continue + } + + field := form.record.Collection().Schema.GetFieldByName(key) + if field != nil && list.ExistInSlice(field.Type, arrayValueSupportTypes) { + result[key] = values + } else { + result[key] = values[0] + } + } + + return result, nil +} + +func (form *RecordUpsert) normalizeData() error { + for _, field := range form.record.Collection().Schema.Fields() { + if v, ok := form.Data[field.Name]; ok { + form.Data[field.Name] = field.PrepareValue(v) + } + } + + return nil +} + +// LoadData loads and normalizes json OR multipart/form-data request data. +// +// File upload is supported only via multipart/form-data. +// +// To REPLACE previously uploaded file(s) you can suffix the field name +// with the file index (eg. `myfile.0`) and set the new value. +// For single file upload fields, you can skip the index and directly +// assign the file value to the field name (eg. `myfile`). +// +// To DELETE previously uploaded file(s) you can suffix the field name +// with the file index (eg. `myfile.0`) and set it to null or empty string. +// For single file upload fields, you can skip the index and directly +// reset the field using its field name (eg. `myfile`). +func (form *RecordUpsert) LoadData(r *http.Request) error { + requestData, err := form.extractRequestData(r) + if err != nil { + return err + } + + // extend base data with the extracted one + extendedData := form.record.Data() + rawData, err := json.Marshal(requestData) + if err != nil { + return err + } + if err := json.Unmarshal(rawData, &extendedData); err != nil { + return err + } + + for _, field := range form.record.Collection().Schema.Fields() { + key := field.Name + value, _ := extendedData[key] + value = field.PrepareValue(value) + + if field.Type == schema.FieldTypeFile { + options, _ := field.Options.(*schema.FileOptions) + oldNames := list.ToUniqueStringSlice(form.Data[key]) + + // delete previously uploaded file(s) + if options.MaxSelect == 1 { + // search for unset zero indexed key as a fallback + indexedKeyValue, hasIndexedKey := extendedData[key+".0"] + + if cast.ToString(value) == "" || (hasIndexedKey && cast.ToString(indexedKeyValue) == "") { + if len(oldNames) > 0 { + form.filesToDelete = append(form.filesToDelete, oldNames...) + } + form.Data[key] = nil + } + } else if options.MaxSelect > 1 { + // search for individual file index to delete (eg. "file.0") + keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`) + indexesToDelete := []int{} + for indexedKey := range extendedData { + if keyExp.MatchString(indexedKey) && cast.ToString(extendedData[indexedKey]) == "" { + index, indexErr := strconv.Atoi(indexedKey[len(key)+1:]) + if indexErr != nil || index >= len(oldNames) { + continue + } + indexesToDelete = append(indexesToDelete, index) + } + } + + // slice to fill only with the non-deleted indexes + nonDeleted := []string{} + for i, name := range oldNames { + // not marked for deletion + if !list.ExistInSlice(i, indexesToDelete) { + nonDeleted = append(nonDeleted, name) + continue + } + + // store the id to actually delete the file later + form.filesToDelete = append(form.filesToDelete, name) + } + form.Data[key] = nonDeleted + } + + // check if there are any new uploaded form files + files, err := rest.FindUploadedFiles(r, key) + if err != nil { + continue // skip invalid or missing file(s) + } + + // refresh oldNames list + oldNames = list.ToUniqueStringSlice(form.Data[key]) + + if options.MaxSelect == 1 { + // delete previous file(s) before replacing + if len(oldNames) > 0 { + form.filesToDelete = list.ToUniqueStringSlice(append(form.filesToDelete, oldNames...)) + } + form.filesToUpload = append(form.filesToUpload, files[0]) + form.Data[key] = files[0].Name() + } else if options.MaxSelect > 1 { + // append the id of each uploaded file instance + form.filesToUpload = append(form.filesToUpload, files...) + for _, file := range files { + oldNames = append(oldNames, file.Name()) + } + form.Data[key] = oldNames + } + } else { + form.Data[key] = value + } + } + + return form.normalizeData() +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *RecordUpsert) Validate() error { + dataValidator := validators.NewRecordDataValidator( + form.app.Dao(), + form.record, + form.filesToUpload, + ) + + return dataValidator.Validate(form.Data) +} + +// DrySubmit performs a form submit within a transaction and reverts it. +// For actual record persistence, check the `form.Submit()` method. +// +// This method doesn't handle file uploads/deletes or trigger any app events! +func (form *RecordUpsert) DrySubmit(callback func(txDao *daos.Dao) error) error { + if err := form.Validate(); err != nil { + return err + } + + // bulk load form data + if err := form.record.Load(form.Data); err != nil { + return err + } + + return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + tx, ok := txDao.DB().(*dbx.Tx) + if !ok { + return errors.New("failed to get transaction db") + } + defer tx.Rollback() + txDao.BeforeCreateFunc = nil + txDao.AfterCreateFunc = nil + txDao.BeforeUpdateFunc = nil + txDao.AfterUpdateFunc = nil + + if err := txDao.SaveRecord(form.record); err != nil { + return err + } + + return callback(txDao) + }) +} + +// Submit validates the form and upserts the form Record model. +func (form *RecordUpsert) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + // bulk load form data + if err := form.record.Load(form.Data); err != nil { + return err + } + + return form.app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + // persist record model + if err := txDao.SaveRecord(form.record); err != nil { + return err + } + + // upload new files (if any) + if err := form.processFilesToUpload(); err != nil { + return err + } + + // delete old files (if any) + if err := form.processFilesToDelete(); err != nil { + // for now fail silently to avoid reupload when `form.Submit()` + // is called manually (aka. not from an api request)... + } + + return nil + }) +} + +func (form *RecordUpsert) processFilesToUpload() error { + if len(form.filesToUpload) == 0 { + return nil // nothing to upload + } + + if !form.record.HasId() { + return errors.New("The record is not persisted yet.") + } + + fs, err := form.app.NewFilesystem() + if err != nil { + return err + } + defer fs.Close() + + for i := len(form.filesToUpload) - 1; i >= 0; i-- { + file := form.filesToUpload[i] + path := form.record.BaseFilesPath() + "/" + file.Name() + + if err := fs.Upload(file.Bytes(), path); err == nil { + form.filesToUpload = append(form.filesToUpload[:i], form.filesToUpload[i+1:]...) + } + } + + if len(form.filesToUpload) > 0 { + return errors.New("Failed to upload all files.") + } + + return nil +} + +func (form *RecordUpsert) processFilesToDelete() error { + if len(form.filesToDelete) == 0 { + return nil // nothing to delete + } + + if !form.record.HasId() { + return errors.New("The record is not persisted yet.") + } + + fs, err := form.app.NewFilesystem() + if err != nil { + return err + } + defer fs.Close() + + for i := len(form.filesToDelete) - 1; i >= 0; i-- { + filename := form.filesToDelete[i] + path := form.record.BaseFilesPath() + "/" + filename + + if err := fs.Delete(path); err == nil { + form.filesToDelete = append(form.filesToDelete[:i], form.filesToDelete[i+1:]...) + } + + // try to delete the related file thumbs (if any) + fs.DeletePrefix(form.record.BaseFilesPath() + "/thumbs_" + filename + "/") + } + + if len(form.filesToDelete) > 0 { + return errors.New("Failed to delete all files.") + } + + return nil +} diff --git a/forms/record_upsert_test.go b/forms/record_upsert_test.go new file mode 100644 index 00000000..a082a914 --- /dev/null +++ b/forms/record_upsert_test.go @@ -0,0 +1,498 @@ +package forms_test + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/list" +) + +func TestNewRecordUpsert(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + record := models.NewRecord(collection) + record.SetDataValue("title", "test_value") + + form := forms.NewRecordUpsert(app, record) + + val, _ := form.Data["title"] + if val != "test_value" { + t.Errorf("Expected record data to be load, got %v", form.Data) + } +} + +func TestRecordUpsertLoadDataUnsupported(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + testData := "title=test123" + + form := forms.NewRecordUpsert(app, record) + req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(testData)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) + + if err := form.LoadData(req); err == nil { + t.Fatal("Expected LoadData to fail, got nil") + } +} + +func TestRecordUpsertLoadDataJson(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + testData := map[string]any{ + "title": "test123", + "unknown": "test456", + // file fields unset/delete + "onefile": nil, + "manyfiles.0": "", + "manyfiles.1": "test.png", // should be ignored + "onlyimages": nil, // should be ignored + } + + form := forms.NewRecordUpsert(app, record) + jsonBody, _ := json.Marshal(testData) + req := httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(jsonBody)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + loadErr := form.LoadData(req) + if loadErr != nil { + t.Fatal(loadErr) + } + + if v, ok := form.Data["title"]; !ok || v != "test123" { + t.Fatalf("Expect title field to be %q, got %q", "test123", v) + } + + if v, ok := form.Data["unknown"]; ok { + t.Fatalf("Didn't expect unknown field to be set, got %v", v) + } + + onefile, ok := form.Data["onefile"] + if !ok { + t.Fatal("Expect onefile field to be set") + } + if onefile != nil { + t.Fatalf("Expect onefile field to be nil, got %v", onefile) + } + + manyfiles, ok := form.Data["manyfiles"] + if !ok || manyfiles == nil { + t.Fatal("Expect manyfiles field to be set") + } + manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles)) + if manyfilesRemains != 1 { + t.Fatalf("Expect only 1 manyfiles to remain, got %v", manyfiles) + } + + // cannot reset multiple file upload field with just using the field name + onlyimages, ok := form.Data["onlyimages"] + if !ok || onlyimages == nil { + t.Fatal("Expect onlyimages field to be set and not be altered") + } + onlyimagesRemains := len(list.ToUniqueStringSlice(onlyimages)) + expectedRemains := 2 // 2 existing + if onlyimagesRemains != expectedRemains { + t.Fatalf("Expect onlyimages to be %d, got %d (%v)", expectedRemains, onlyimagesRemains, onlyimages) + } +} + +func TestRecordUpsertLoadDataMultipart(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + formData, mp, err := tests.MockMultipartData(map[string]string{ + "title": "test123", + "unknown": "test456", + // file fields unset/delete + "onefile": "", + "manyfiles.0": "", + "manyfiles.1": "test.png", // should be ignored + "onlyimages": "", // should be ignored + }, "onlyimages") + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(app, record) + req := httptest.NewRequest(http.MethodGet, "/", formData) + req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + loadErr := form.LoadData(req) + if loadErr != nil { + t.Fatal(loadErr) + } + + if v, ok := form.Data["title"]; !ok || v != "test123" { + t.Fatalf("Expect title field to be %q, got %q", "test123", v) + } + + if v, ok := form.Data["unknown"]; ok { + t.Fatalf("Didn't expect unknown field to be set, got %v", v) + } + + onefile, ok := form.Data["onefile"] + if !ok { + t.Fatal("Expect onefile field to be set") + } + if onefile != nil { + t.Fatalf("Expect onefile field to be nil, got %v", onefile) + } + + manyfiles, ok := form.Data["manyfiles"] + if !ok || manyfiles == nil { + t.Fatal("Expect manyfiles field to be set") + } + manyfilesRemains := len(list.ToUniqueStringSlice(manyfiles)) + if manyfilesRemains != 1 { + t.Fatalf("Expect only 1 manyfiles to remain, got %v", manyfiles) + } + + onlyimages, ok := form.Data["onlyimages"] + if !ok || onlyimages == nil { + t.Fatal("Expect onlyimages field to be set and not be altered") + } + onlyimagesRemains := len(list.ToUniqueStringSlice(onlyimages)) + expectedRemains := 3 // 2 existing + 1 new upload + if onlyimagesRemains != expectedRemains { + t.Fatalf("Expect onlyimages to be %d, got %d (%v)", expectedRemains, onlyimagesRemains, onlyimages) + } +} + +func TestRecordUpsertValidateFailure(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + // try with invalid test data to check whether the RecordDataValidator is triggered + formData, mp, err := tests.MockMultipartData(map[string]string{ + "unknown": "test456", // should be ignored + "title": "a", + "onerel": "00000000-84ab-4057-a592-4604a731f78f", + }, "manyfiles", "manyfiles") + if err != nil { + t.Fatal(err) + } + + expectedErrors := []string{"title", "onerel", "manyfiles"} + + form := forms.NewRecordUpsert(app, record) + req := httptest.NewRequest(http.MethodGet, "/", formData) + req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + form.LoadData(req) + + result := form.Validate() + + // parse errors + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Fatalf("Failed to parse errors %v", result) + } + + // check errors + if len(errs) > len(expectedErrors) { + t.Fatalf("Expected error keys %v, got %v", expectedErrors, errs) + } + for _, k := range expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("Missing expected error key %q in %v", k, errs) + } + } +} + +func TestRecordUpsertValidateSuccess(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + record, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + formData, mp, err := tests.MockMultipartData(map[string]string{ + "unknown": "test456", // should be ignored + "title": "abc", + "onerel": "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2", + }, "manyfiles", "onefile") + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(app, record) + req := httptest.NewRequest(http.MethodGet, "/", formData) + req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + form.LoadData(req) + + result := form.Validate() + if result != nil { + t.Fatal(result) + } +} + +func TestRecordUpsertDrySubmitFailure(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + recordBefore, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + formData, mp, err := tests.MockMultipartData(map[string]string{ + "title": "a", + "onerel": "00000000-84ab-4057-a592-4604a731f78f", + }) + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(app, recordBefore) + req := httptest.NewRequest(http.MethodGet, "/", formData) + req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + form.LoadData(req) + + callbackCalls := 0 + + // ensure that validate is triggered + // --- + result := form.DrySubmit(func(txDao *daos.Dao) error { + callbackCalls++ + return nil + }) + if result == nil { + t.Fatal("Expected error, got nil") + } + if callbackCalls != 0 { + t.Fatalf("Expected callbackCalls to be 0, got %d", callbackCalls) + } + + // ensure that the record changes weren't persisted + // --- + recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id) + if err != nil { + t.Fatal(err) + } + + if recordAfter.GetStringDataValue("title") == "a" { + t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetStringDataValue("title"), "a") + } + + if recordAfter.GetStringDataValue("onerel") == "00000000-84ab-4057-a592-4604a731f78f" { + t.Fatalf("Expected record.onerel to be %s, got %s", recordBefore.GetStringDataValue("onerel"), recordAfter.GetStringDataValue("onerel")) + } +} + +func TestRecordUpsertDrySubmitSuccess(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + recordBefore, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + formData, mp, err := tests.MockMultipartData(map[string]string{ + "title": "dry_test", + "onefile": "", + }, "manyfiles") + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(app, recordBefore) + req := httptest.NewRequest(http.MethodGet, "/", formData) + req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + form.LoadData(req) + + callbackCalls := 0 + + result := form.DrySubmit(func(txDao *daos.Dao) error { + callbackCalls++ + return nil + }) + if result != nil { + t.Fatalf("Expected nil, got error %v", result) + } + + // ensure callback was called + if callbackCalls != 1 { + t.Fatalf("Expected callbackCalls to be 1, got %d", callbackCalls) + } + + // ensure that the record changes weren't persisted + // --- + recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id) + if err != nil { + t.Fatal(err) + } + + if recordAfter.GetStringDataValue("title") == "dry_test" { + t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetStringDataValue("title"), "dry_test") + } + if recordAfter.GetStringDataValue("onefile") == "" { + t.Fatal("Expected record.onefile to be set, got empty string") + } + + // file wasn't removed + if !hasRecordFile(app, recordAfter, recordAfter.GetStringDataValue("onefile")) { + t.Fatal("onefile file should not have been deleted") + } +} + +func TestRecordUpsertSubmitFailure(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + recordBefore, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + formData, mp, err := tests.MockMultipartData(map[string]string{ + "title": "a", + "onefile": "", + }) + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(app, recordBefore) + req := httptest.NewRequest(http.MethodGet, "/", formData) + req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + form.LoadData(req) + + // ensure that validate is triggered + // --- + result := form.Submit() + if result == nil { + t.Fatal("Expected error, got nil") + } + + // ensure that the record changes weren't persisted + // --- + recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id) + if err != nil { + t.Fatal(err) + } + + if recordAfter.GetStringDataValue("title") == "a" { + t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetStringDataValue("title"), "a") + } + + if recordAfter.GetStringDataValue("onefile") == "" { + t.Fatal("Expected record.onefile to be set, got empty string") + } + + // file wasn't removed + if !hasRecordFile(app, recordAfter, recordAfter.GetStringDataValue("onefile")) { + t.Fatal("onefile file should not have been deleted") + } +} + +func TestRecordUpsertSubmitSuccess(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo4") + recordBefore, err := app.Dao().FindFirstRecordByData(collection, "id", "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2") + if err != nil { + t.Fatal(err) + } + + formData, mp, err := tests.MockMultipartData(map[string]string{ + "title": "test_save", + "onefile": "", + }, "manyfiles.1", "manyfiles") // replace + new file + if err != nil { + t.Fatal(err) + } + + form := forms.NewRecordUpsert(app, recordBefore) + req := httptest.NewRequest(http.MethodGet, "/", formData) + req.Header.Set(echo.HeaderContentType, mp.FormDataContentType()) + form.LoadData(req) + + result := form.Submit() + if result != nil { + t.Fatalf("Expected nil, got error %v", result) + } + + // ensure that the record changes were persisted + // --- + recordAfter, err := app.Dao().FindFirstRecordByData(collection, "id", recordBefore.Id) + if err != nil { + t.Fatal(err) + } + + if recordAfter.GetStringDataValue("title") != "test_save" { + t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetStringDataValue("title"), "test_save") + } + + if hasRecordFile(app, recordAfter, recordAfter.GetStringDataValue("onefile")) { + t.Fatal("Expected record.onefile to be deleted") + } + + manyfiles := (recordAfter.GetStringSliceDataValue("manyfiles")) + if len(manyfiles) != 3 { + t.Fatalf("Expected 3 manyfiles, got %d (%v)", len(manyfiles), manyfiles) + } + for _, f := range manyfiles { + if !hasRecordFile(app, recordAfter, f) { + t.Fatalf("Expected file %q to exist", f) + } + } +} + +func hasRecordFile(app core.App, record *models.Record, filename string) bool { + fs, _ := app.NewFilesystem() + defer fs.Close() + + fileKey := filepath.Join( + record.Collection().Id, + record.Id, + filename, + ) + + exists, _ := fs.Exists(fileKey) + + return exists +} diff --git a/forms/settings_upsert.go b/forms/settings_upsert.go new file mode 100644 index 00000000..22dde5ca --- /dev/null +++ b/forms/settings_upsert.go @@ -0,0 +1,59 @@ +package forms + +import ( + "os" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" +) + +// SettingsUpsert defines app settings upsert form. +type SettingsUpsert struct { + *core.Settings + + app core.App +} + +// NewSettingsUpsert creates new settings upsert form from the provided app. +func NewSettingsUpsert(app core.App) *SettingsUpsert { + form := &SettingsUpsert{app: app} + + // load the application settings into the form + form.Settings, _ = app.Settings().Clone() + + return form +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *SettingsUpsert) Validate() error { + return form.Settings.Validate() +} + +// Submit validates the form and upserts the loaded settings. +// +// On success the app settings will be refreshed with the form ones. +func (form *SettingsUpsert) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + encryptionKey := os.Getenv(form.app.EncryptionEnv()) + + saveErr := form.app.Dao().SaveParam( + models.ParamAppSettings, + form.Settings, + encryptionKey, + ) + if saveErr != nil { + return saveErr + } + + // explicitly trigger old logs deletion + form.app.LogsDao().DeleteOldRequests( + time.Now().AddDate(0, 0, -1*form.Settings.Logs.MaxDays), + ) + + // merge the application settings with the form ones + return form.app.Settings().Merge(form.Settings) +} diff --git a/forms/settings_upsert_test.go b/forms/settings_upsert_test.go new file mode 100644 index 00000000..01c838cd --- /dev/null +++ b/forms/settings_upsert_test.go @@ -0,0 +1,130 @@ +package forms_test + +import ( + "encoding/json" + "os" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestNewSettingsUpsert(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + app.Settings().Meta.AppName = "name_update" + + form := forms.NewSettingsUpsert(app) + + formSettings, _ := json.Marshal(form.Settings) + appSettings, _ := json.Marshal(app.Settings()) + + if string(formSettings) != string(appSettings) { + t.Errorf("Expected settings \n%s, got \n%s", string(appSettings), string(formSettings)) + } +} + +func TestSettingsUpsertValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + form := forms.NewSettingsUpsert(app) + + // check if settings validations are triggered + // (there are already individual tests for each setting) + form.Meta.AppName = "" + form.Logs.MaxDays = -10 + + // parse errors + err := form.Validate() + jsonResult, _ := json.Marshal(err) + + expected := `{"logs":{"maxDays":"must be no less than 0"},"meta":{"appName":"cannot be blank"}}` + + if string(jsonResult) != expected { + t.Errorf("Expected %v, got %v", expected, string(jsonResult)) + } +} + +func TestSettingsUpsertSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + encryption bool + expectedErrors []string + }{ + // empty (plain) + {"{}", false, nil}, + // empty (encrypt) + {"{}", true, nil}, + // failure - invalid data + { + `{"emailAuth": {"minPasswordLength": 1}, "logs": {"maxDays": -1}}`, + false, + []string{"emailAuth", "logs"}, + }, + // success - valid data (plain) + { + `{"emailAuth": {"minPasswordLength": 6}, "logs": {"maxDays": 0}}`, + false, + nil, + }, + // success - valid data (encrypt) + { + `{"emailAuth": {"minPasswordLength": 6}, "logs": {"maxDays": 0}}`, + true, + nil, + }, + } + + for i, s := range scenarios { + if s.encryption { + os.Setenv(app.EncryptionEnv(), security.RandomString(32)) + } else { + os.Unsetenv(app.EncryptionEnv()) + } + + form := forms.NewSettingsUpsert(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Submit() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + + if len(s.expectedErrors) > 0 { + continue + } + + formSettings, _ := json.Marshal(form.Settings) + appSettings, _ := json.Marshal(app.Settings()) + + if string(formSettings) != string(appSettings) { + t.Errorf("Expected app settings \n%s, got \n%s", string(appSettings), string(formSettings)) + } + } +} diff --git a/forms/user_email_change_confirm.go b/forms/user_email_change_confirm.go new file mode 100644 index 00000000..cc5a44b6 --- /dev/null +++ b/forms/user_email_change_confirm.go @@ -0,0 +1,113 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/security" +) + +// UserEmailChangeConfirm defines a user email change confirmation form. +type UserEmailChangeConfirm struct { + app core.App + + Token string `form:"token" json:"token"` + Password string `form:"password" json:"password"` +} + +// NewUserEmailChangeConfirm creates new user email change confirmation form. +func NewUserEmailChangeConfirm(app core.App) *UserEmailChangeConfirm { + return &UserEmailChangeConfirm{ + app: app, + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *UserEmailChangeConfirm) Validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.Token, + validation.Required, + validation.By(form.checkToken), + ), + validation.Field( + &form.Password, + validation.Required, + validation.Length(1, 100), + validation.By(form.checkPassword), + ), + ) +} + +func (form *UserEmailChangeConfirm) checkToken(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + _, _, err := form.parseToken(v) + + return err +} + +func (form *UserEmailChangeConfirm) checkPassword(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + user, _, _ := form.parseToken(form.Token) + if user == nil || !user.ValidatePassword(v) { + return validation.NewError("validation_invalid_password", "Missing or invalid user password.") + } + + return nil +} + +func (form *UserEmailChangeConfirm) parseToken(token string) (*models.User, string, error) { + // check token payload + claims, _ := security.ParseUnverifiedJWT(token) + newEmail, _ := claims["newEmail"].(string) + if newEmail == "" { + return nil, "", validation.NewError("validation_invalid_token_payload", "Invalid token payload - newEmail must be set.") + } + + // ensure that there aren't other users with the new email + if !form.app.Dao().IsUserEmailUnique(newEmail, "") { + return nil, "", validation.NewError("validation_existing_token_email", "The new email address is already registered: "+newEmail) + } + + // verify that the token is not expired and its signiture is valid + user, err := form.app.Dao().FindUserByToken( + token, + form.app.Settings().UserEmailChangeToken.Secret, + ) + if err != nil || user == nil { + return nil, "", validation.NewError("validation_invalid_token", "Invalid or expired token.") + } + + return user, newEmail, nil +} + +// Submit validates and submits the user email change confirmation form. +// On success returns the updated user model associated to `form.Token`. +func (form *UserEmailChangeConfirm) Submit() (*models.User, error) { + if err := form.Validate(); err != nil { + return nil, err + } + + user, newEmail, err := form.parseToken(form.Token) + if err != nil { + return nil, err + } + + user.Email = newEmail + user.Verified = true + user.RefreshTokenKey() // invalidate old tokens + + if err := form.app.Dao().SaveUser(user); err != nil { + return nil, err + } + + return user, nil +} diff --git a/forms/user_email_change_confirm_test.go b/forms/user_email_change_confirm_test.go new file mode 100644 index 00000000..cc5d7502 --- /dev/null +++ b/forms/user_email_change_confirm_test.go @@ -0,0 +1,121 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestUserEmailChangeConfirmValidateAndSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty payload + {"{}", []string{"token", "password"}}, + // empty data + { + `{"token": "", "password": ""}`, + []string{"token", "password"}, + }, + // invalid token payload + { + `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxODYxOTE2NDYxfQ.VjT3wc3IES--1Vye-1KRuk8RpO5mfdhVp2aKGbNluZ0", + "password": "123456" + }`, + []string{"token", "password"}, + }, + // expired token + { + `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0X25ld0BleGFtcGxlLmNvbSIsImV4cCI6MTY0MDk5MTY2MX0.oPxbpJjcBpdZVBFbIW35FEXTCMkzJ7-RmQdHrz7zP3s", + "password": "123456" + }`, + []string{"token", "password"}, + }, + // existing new email + { + `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsImV4cCI6MTg2MTkxNjQ2MX0.RwHRZma5YpCwxHdj3y2obeBNy_GQrG6lT9CQHIUz6Ys", + "password": "123456" + }`, + []string{"token", "password"}, + }, + // wrong confirmation password + { + `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0X25ld0BleGFtcGxlLmNvbSIsImV4cCI6MTg2MTkxNjQ2MX0.nS2qDonX25tOf9-6bKCwJXOm1CE88z_EVAA2B72NYM0", + "password": "1234" + }`, + []string{"password"}, + }, + // valid data + { + `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmV3RW1haWwiOiJ0ZXN0X25ld0BleGFtcGxlLmNvbSIsImV4cCI6MTg2MTkxNjQ2MX0.nS2qDonX25tOf9-6bKCwJXOm1CE88z_EVAA2B72NYM0", + "password": "123456" + }`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewUserEmailChangeConfirm(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + user, err := form.Submit() + + // parse errors + errs, ok := err.(validation.Errors) + if !ok && err != nil { + t.Errorf("(%d) Failed to parse errors %v", i, err) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + + if len(s.expectedErrors) > 0 { + continue + } + + claims, _ := security.ParseUnverifiedJWT(form.Token) + newEmail, _ := claims["newEmail"].(string) + + // check whether the user was updated + // --- + if user.Email != newEmail { + t.Errorf("(%d) Expected user email %q, got %q", i, newEmail, user.Email) + } + + if !user.Verified { + t.Errorf("(%d) Expected user to be verified, got false", i) + } + + // shouldn't validate second time due to refreshed user token + if err := form.Validate(); err == nil { + t.Errorf("(%d) Expected error, got nil", i) + } + } +} diff --git a/forms/user_email_change_request.go b/forms/user_email_change_request.go new file mode 100644 index 00000000..3a02043f --- /dev/null +++ b/forms/user_email_change_request.go @@ -0,0 +1,57 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/models" +) + +// UserEmailChangeConfirm defines a user email change request form. +type UserEmailChangeRequest struct { + app core.App + user *models.User + + NewEmail string `form:"newEmail" json:"newEmail"` +} + +// NewUserEmailChangeRequest creates a new user email change request form. +func NewUserEmailChangeRequest(app core.App, user *models.User) *UserEmailChangeRequest { + return &UserEmailChangeRequest{ + app: app, + user: user, + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *UserEmailChangeRequest) Validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.NewEmail, + validation.Required, + validation.Length(1, 255), + is.Email, + validation.By(form.checkUniqueEmail), + ), + ) +} + +func (form *UserEmailChangeRequest) checkUniqueEmail(value any) error { + v, _ := value.(string) + + if !form.app.Dao().IsUserEmailUnique(v, "") { + return validation.NewError("validation_user_email_exists", "User email already exists.") + } + + return nil +} + +// Submit validates and sends the change email request. +func (form *UserEmailChangeRequest) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + return mails.SendUserChangeEmail(form.app, form.user, form.NewEmail) +} diff --git a/forms/user_email_change_request_test.go b/forms/user_email_change_request_test.go new file mode 100644 index 00000000..e81df9e0 --- /dev/null +++ b/forms/user_email_change_request_test.go @@ -0,0 +1,87 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" +) + +func TestUserEmailChangeRequestValidateAndSubmit(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + user, err := testApp.Dao().FindUserByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty payload + {"{}", []string{"newEmail"}}, + // empty data + { + `{"newEmail": ""}`, + []string{"newEmail"}, + }, + // invalid email + { + `{"newEmail": "invalid"}`, + []string{"newEmail"}, + }, + // existing email token + { + `{"newEmail": "test@example.com"}`, + []string{"newEmail"}, + }, + // valid new email + { + `{"newEmail": "test_new@example.com"}`, + []string{}, + }, + } + + for i, s := range scenarios { + testApp.TestMailer.TotalSend = 0 // reset + form := forms.NewUserEmailChangeRequest(testApp, user) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + err := form.Submit() + + // parse errors + errs, ok := err.(validation.Errors) + if !ok && err != nil { + t.Errorf("(%d) Failed to parse errors %v", i, err) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + + expectedMails := 1 + if len(s.expectedErrors) > 0 { + expectedMails = 0 + } + if testApp.TestMailer.TotalSend != expectedMails { + t.Errorf("(%d) Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) + } + } +} diff --git a/forms/user_email_login.go b/forms/user_email_login.go new file mode 100644 index 00000000..49e49d15 --- /dev/null +++ b/forms/user_email_login.go @@ -0,0 +1,52 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" +) + +// UserEmailLogin defines a user email/pass login form. +type UserEmailLogin struct { + app core.App + + Email string `form:"email" json:"email"` + Password string `form:"password" json:"password"` +} + +// NewUserEmailLogin creates a new user email/pass login form. +func NewUserEmailLogin(app core.App) *UserEmailLogin { + form := &UserEmailLogin{ + app: app, + } + + return form +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *UserEmailLogin) Validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Email, validation.Required, validation.Length(1, 255), is.Email), + validation.Field(&form.Password, validation.Required, validation.Length(1, 255)), + ) +} + +// Submit validates and submits the form. +// On success returns the authorized user model. +func (form *UserEmailLogin) Submit() (*models.User, error) { + if err := form.Validate(); err != nil { + return nil, err + } + + user, err := form.app.Dao().FindUserByEmail(form.Email) + if err != nil { + return nil, err + } + + if !user.ValidatePassword(form.Password) { + return nil, validation.NewError("invalid_login", "Invalid login credentials.") + } + + return user, nil +} diff --git a/forms/user_email_login_test.go b/forms/user_email_login_test.go new file mode 100644 index 00000000..2d4b867d --- /dev/null +++ b/forms/user_email_login_test.go @@ -0,0 +1,106 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" +) + +func TestUserEmailLoginValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty payload + {"{}", []string{"email", "password"}}, + // empty data + { + `{"email": "","password": ""}`, + []string{"email", "password"}, + }, + // invalid email + { + `{"email": "invalid","password": "123"}`, + []string{"email"}, + }, + // valid email + { + `{"email": "test@example.com","password": "123"}`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewUserEmailLogin(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + err := form.Validate() + + // parse errors + errs, ok := err.(validation.Errors) + if !ok && err != nil { + t.Errorf("(%d) Failed to parse errors %v", i, err) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +func TestUserEmailLoginSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + email string + password string + expectError bool + }{ + // invalid email + {"invalid", "123456", true}, + // missing user + {"missing@example.com", "123456", true}, + // invalid password + {"test@example.com", "123", true}, + // valid email and password + {"test@example.com", "123456", false}, + } + + for i, s := range scenarios { + form := forms.NewUserEmailLogin(app) + form.Email = s.email + form.Password = s.password + + user, err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if !s.expectError && user.Email != s.email { + t.Errorf("(%d) Expected user with email %q, got %q", i, s.email, user.Email) + } + } +} diff --git a/forms/user_oauth2_login.go b/forms/user_oauth2_login.go new file mode 100644 index 00000000..ab8f86c4 --- /dev/null +++ b/forms/user_oauth2_login.go @@ -0,0 +1,133 @@ +package forms + +import ( + "errors" + "fmt" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/auth" + "github.com/pocketbase/pocketbase/tools/security" + "golang.org/x/oauth2" +) + +// UserOauth2Login defines a user Oauth2 login form. +type UserOauth2Login struct { + app core.App + + // The name of the OAuth2 client provider (eg. "google") + Provider string `form:"provider" json:"provider"` + + // The authorization code returned from the initial request. + Code string `form:"code" json:"code"` + + // The code verifier sent with the initial request as part of the code_challenge. + CodeVerifier string `form:"codeVerifier" json:"codeVerifier"` + + // The redirect url sent with the initial request. + RedirectUrl string `form:"redirectUrl" json:"redirectUrl"` +} + +// NewUserOauth2Login creates a new user Oauth2 login form. +func NewUserOauth2Login(app core.App) *UserOauth2Login { + return &UserOauth2Login{app: app} +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *UserOauth2Login) Validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Provider, validation.Required, validation.By(form.checkProviderName)), + validation.Field(&form.Code, validation.Required), + validation.Field(&form.CodeVerifier, validation.Required), + validation.Field(&form.RedirectUrl, validation.Required, is.URL), + ) +} + +func (form *UserOauth2Login) checkProviderName(value any) error { + name, _ := value.(string) + + config, ok := form.app.Settings().NamedAuthProviderConfigs()[name] + if !ok || !config.Enabled { + return validation.NewError("validation_invalid_provider", fmt.Sprintf("%q is missing or is not enabled.", name)) + } + + return nil +} + +// Submit validates and submits the form. +// On success returns the authorized user model and the fetched provider's data. +func (form *UserOauth2Login) Submit() (*models.User, *auth.AuthUser, error) { + if err := form.Validate(); err != nil { + return nil, nil, err + } + + provider, err := auth.NewProviderByName(form.Provider) + if err != nil { + return nil, nil, err + } + + config, _ := form.app.Settings().NamedAuthProviderConfigs()[form.Provider] + config.SetupProvider(provider) + + provider.SetRedirectUrl(form.RedirectUrl) + + // fetch token + token, err := provider.FetchToken( + form.Code, + oauth2.SetAuthURLParam("code_verifier", form.CodeVerifier), + ) + if err != nil { + return nil, nil, err + } + + // fetch auth user + authData, err := provider.FetchAuthUser(token) + if err != nil { + return nil, nil, err + } + + // login/register the auth user + user, _ := form.app.Dao().FindUserByEmail(authData.Email) + if user != nil { + // update the existing user's verified state + if !user.Verified { + user.Verified = true + if err := form.app.Dao().SaveUser(user); err != nil { + return nil, authData, err + } + } + } else { + if !config.AllowRegistrations { + // registration of new users is not allowed via the Oauth2 provider + return nil, authData, errors.New("Cannot find user with the authorized email.") + } + + // create new user + user = &models.User{Verified: true} + upsertForm := NewUserUpsert(form.app, user) + upsertForm.Email = authData.Email + upsertForm.Password = security.RandomString(30) + upsertForm.PasswordConfirm = upsertForm.Password + + event := &core.UserOauth2RegisterEvent{ + User: user, + AuthData: authData, + } + + if err := form.app.OnUserBeforeOauth2Register().Trigger(event); err != nil { + return nil, authData, err + } + + if err := upsertForm.Submit(); err != nil { + return nil, authData, err + } + + if err := form.app.OnUserAfterOauth2Register().Trigger(event); err != nil { + return nil, authData, err + } + } + + return user, authData, nil +} diff --git a/forms/user_oauth2_login_test.go b/forms/user_oauth2_login_test.go new file mode 100644 index 00000000..13f3b0d0 --- /dev/null +++ b/forms/user_oauth2_login_test.go @@ -0,0 +1,75 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" +) + +func TestUserOauth2LoginValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty payload + {"{}", []string{"provider", "code", "codeVerifier", "redirectUrl"}}, + // empty data + { + `{"provider":"","code":"","codeVerifier":"","redirectUrl":""}`, + []string{"provider", "code", "codeVerifier", "redirectUrl"}, + }, + // missing provider + { + `{"provider":"missing","code":"123","codeVerifier":"123","redirectUrl":"https://example.com"}`, + []string{"provider"}, + }, + // disabled provider + { + `{"provider":"github","code":"123","codeVerifier":"123","redirectUrl":"https://example.com"}`, + []string{"provider"}, + }, + // enabled provider + { + `{"provider":"gitlab","code":"123","codeVerifier":"123","redirectUrl":"https://example.com"}`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewUserOauth2Login(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + err := form.Validate() + + // parse errors + errs, ok := err.(validation.Errors) + if !ok && err != nil { + t.Errorf("(%d) Failed to parse errors %v", i, err) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +// @todo consider mocking a Oauth2 provider to test Submit diff --git a/forms/user_password_reset_confirm.go b/forms/user_password_reset_confirm.go new file mode 100644 index 00000000..e97ce29e --- /dev/null +++ b/forms/user_password_reset_confirm.go @@ -0,0 +1,78 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms/validators" + "github.com/pocketbase/pocketbase/models" +) + +// UserPasswordResetConfirm defines a user password reset confirmation form. +type UserPasswordResetConfirm struct { + app core.App + + Token string `form:"token" json:"token"` + Password string `form:"password" json:"password"` + PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` +} + +// NewUserPasswordResetConfirm creates new user password reset confirmation form. +func NewUserPasswordResetConfirm(app core.App) *UserPasswordResetConfirm { + return &UserPasswordResetConfirm{ + app: app, + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *UserPasswordResetConfirm) Validate() error { + minPasswordLength := form.app.Settings().EmailAuth.MinPasswordLength + + return validation.ValidateStruct(form, + validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), + validation.Field(&form.Password, validation.Required, validation.Length(minPasswordLength, 100)), + validation.Field(&form.PasswordConfirm, validation.Required, validation.By(validators.Compare(form.Password))), + ) +} + +func (form *UserPasswordResetConfirm) checkToken(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + user, err := form.app.Dao().FindUserByToken( + v, + form.app.Settings().UserPasswordResetToken.Secret, + ) + if err != nil || user == nil { + return validation.NewError("validation_invalid_token", "Invalid or expired token.") + } + + return nil +} + +// Submit validates and submits the form. +// On success returns the updated user model associated to `form.Token`. +func (form *UserPasswordResetConfirm) Submit() (*models.User, error) { + if err := form.Validate(); err != nil { + return nil, err + } + + user, err := form.app.Dao().FindUserByToken( + form.Token, + form.app.Settings().UserPasswordResetToken.Secret, + ) + if err != nil { + return nil, err + } + + if err := user.SetPassword(form.Password); err != nil { + return nil, err + } + + if err := form.app.Dao().SaveUser(user); err != nil { + return nil, err + } + + return user, nil +} diff --git a/forms/user_password_reset_confirm_test.go b/forms/user_password_reset_confirm_test.go new file mode 100644 index 00000000..0300e6ed --- /dev/null +++ b/forms/user_password_reset_confirm_test.go @@ -0,0 +1,165 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestUserPasswordResetConfirmValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty data + { + `{}`, + []string{"token", "password", "passwordConfirm"}, + }, + // empty fields + { + `{"token":"","password":"","passwordConfirm":""}`, + []string{"token", "password", "passwordConfirm"}, + }, + // invalid password length + { + `{"token":"invalid","password":"1234","passwordConfirm":"1234"}`, + []string{"token", "password"}, + }, + // mismatched passwords + { + `{"token":"invalid","password":"12345678","passwordConfirm":"87654321"}`, + []string{"token", "passwordConfirm"}, + }, + // invalid JWT token + { + `{"token":"invalid","password":"12345678","passwordConfirm":"12345678"}`, + []string{"token"}, + }, + // expired token + { + `{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNjQwOTkxNjYxfQ.cSUFKWLAKEvulWV4fqPD6RRtkZYoyat_Tb8lrA2xqtw", + "password":"12345678", + "passwordConfirm":"12345678" + }`, + []string{"token"}, + }, + // valid data + { + `{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxODkzNDUyNDYxfQ.YfpL4VOdsYh2gS30VIiPShgwwqPgt2CySD8TuuB1XD4", + "password":"12345678", + "passwordConfirm":"12345678" + }`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewUserPasswordResetConfirm(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Validate() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +func TestUserPasswordResetConfirmSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectError bool + }{ + // empty data (Validate call check) + { + `{}`, + true, + }, + // expired token + { + `{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNjQwOTkxNjYxfQ.cSUFKWLAKEvulWV4fqPD6RRtkZYoyat_Tb8lrA2xqtw", + "password":"12345678", + "passwordConfirm":"12345678" + }`, + true, + }, + // valid data + { + `{ + "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxODkzNDUyNDYxfQ.YfpL4VOdsYh2gS30VIiPShgwwqPgt2CySD8TuuB1XD4", + "password":"12345678", + "passwordConfirm":"12345678" + }`, + false, + }, + } + + for i, s := range scenarios { + form := forms.NewUserPasswordResetConfirm(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + user, err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + if s.expectError { + continue + } + + claims, _ := security.ParseUnverifiedJWT(form.Token) + tokenUserId, _ := claims["id"] + + if user.Id != tokenUserId { + t.Errorf("(%d) Expected user with id %s, got %v", i, tokenUserId, user) + } + + if !user.LastResetSentAt.IsZero() { + t.Errorf("(%d) Expected user.LastResetSentAt to be empty, got %v", i, user.LastResetSentAt) + } + + if !user.ValidatePassword(form.Password) { + t.Errorf("(%d) Expected the user password to have been updated to %q", i, form.Password) + } + } +} diff --git a/forms/user_password_reset_request.go b/forms/user_password_reset_request.go new file mode 100644 index 00000000..f879e1a6 --- /dev/null +++ b/forms/user_password_reset_request.go @@ -0,0 +1,70 @@ +package forms + +import ( + "errors" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tools/types" +) + +// UserPasswordResetRequest defines a user password reset request form. +type UserPasswordResetRequest struct { + app core.App + resendThreshold float64 + + Email string `form:"email" json:"email"` +} + +// NewUserPasswordResetRequest creates new user password reset request form. +func NewUserPasswordResetRequest(app core.App) *UserPasswordResetRequest { + return &UserPasswordResetRequest{ + app: app, + resendThreshold: 120, // 2 min + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +// +// This method doesn't checks whether user with `form.Email` exists (this is done on Submit). +func (form *UserPasswordResetRequest) Validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.Email, + validation.Required, + validation.Length(1, 255), + is.Email, + ), + ) +} + +// Submit validates and submits the form. +// On success sends a password reset email to the `form.Email` user. +func (form *UserPasswordResetRequest) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + user, err := form.app.Dao().FindUserByEmail(form.Email) + if err != nil { + return err + } + + now := time.Now().UTC() + lastResetSentAt := user.LastResetSentAt.Time() + if now.Sub(lastResetSentAt).Seconds() < form.resendThreshold { + return errors.New("You've already requested a password reset.") + } + + if err := mails.SendUserPasswordReset(form.app, user); err != nil { + return err + } + + // update last sent timestamp + user.LastResetSentAt = types.NowDateTime() + + return form.app.Dao().SaveUser(user) +} diff --git a/forms/user_password_reset_request_test.go b/forms/user_password_reset_request_test.go new file mode 100644 index 00000000..cc4224bd --- /dev/null +++ b/forms/user_password_reset_request_test.go @@ -0,0 +1,153 @@ +package forms_test + +import ( + "encoding/json" + "testing" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestUserPasswordResetRequestValidate(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty data + { + `{}`, + []string{"email"}, + }, + // empty fields + { + `{"email":""}`, + []string{"email"}, + }, + // invalid email format + { + `{"email":"invalid"}`, + []string{"email"}, + }, + // valid email + { + `{"email":"new@example.com"}`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewUserPasswordResetRequest(testApp) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Validate() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +func TestUserPasswordResetRequestSubmit(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + jsonData string + expectError bool + }{ + // empty field (Validate call check) + { + `{"email":""}`, + true, + }, + // invalid email field (Validate call check) + { + `{"email":"invalid"}`, + true, + }, + // nonexisting user + { + `{"email":"missing@example.com"}`, + true, + }, + // existing user + { + `{"email":"test@example.com"}`, + false, + }, + // existing user - reached send threshod + { + `{"email":"test@example.com"}`, + true, + }, + } + + now := types.NowDateTime() + time.Sleep(1 * time.Millisecond) + + for i, s := range scenarios { + testApp.TestMailer.TotalSend = 0 // reset + form := forms.NewUserPasswordResetRequest(testApp) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + expectedMails := 1 + if s.expectError { + expectedMails = 0 + } + if testApp.TestMailer.TotalSend != expectedMails { + t.Errorf("(%d) Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) + } + + if s.expectError { + continue + } + + // check whether LastResetSentAt was updated + user, err := testApp.Dao().FindUserByEmail(form.Email) + if err != nil { + t.Errorf("(%d) Expected user with email %q to exist, got nil", i, form.Email) + continue + } + + if user.LastResetSentAt.Time().Sub(now.Time()) < 0 { + t.Errorf("(%d) Expected LastResetSentAt to be after %v, got %v", i, now, user.LastResetSentAt) + } + } +} diff --git a/forms/user_upsert.go b/forms/user_upsert.go new file mode 100644 index 00000000..c78a3e10 --- /dev/null +++ b/forms/user_upsert.go @@ -0,0 +1,118 @@ +package forms + +import ( + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms/validators" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" +) + +// UserUpsert defines a user upsert (create/update) form. +type UserUpsert struct { + app core.App + user *models.User + isCreate bool + + Email string `form:"email" json:"email"` + Password string `form:"password" json:"password"` + PasswordConfirm string `form:"passwordConfirm" json:"passwordConfirm"` +} + +// NewUserUpsert creates new upsert form for the provided user model +// (pass an empty user model instance (`&models.User{}`) for create). +func NewUserUpsert(app core.App, user *models.User) *UserUpsert { + form := &UserUpsert{ + app: app, + user: user, + isCreate: !user.HasId(), + } + + // load defaults + form.Email = user.Email + + return form +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *UserUpsert) Validate() error { + config := form.app.Settings() + + return validation.ValidateStruct(form, + validation.Field( + &form.Email, + validation.Required, + validation.Length(1, 255), + is.Email, + validation.By(form.checkEmailDomain), + validation.By(form.checkUniqueEmail), + ), + validation.Field( + &form.Password, + validation.When(form.isCreate, validation.Required), + validation.Length(config.EmailAuth.MinPasswordLength, 100), + ), + validation.Field( + &form.PasswordConfirm, + validation.When(form.isCreate || form.Password != "", validation.Required), + validation.By(validators.Compare(form.Password)), + ), + ) +} + +func (form *UserUpsert) checkUniqueEmail(value any) error { + v, _ := value.(string) + + if v == "" || form.app.Dao().IsUserEmailUnique(v, form.user.Id) { + return nil + } + + return validation.NewError("validation_user_email_exists", "User email already exists.") +} + +func (form *UserUpsert) checkEmailDomain(value any) error { + val, _ := value.(string) + if val == "" { + return nil // nothing to check + } + + domain := val[strings.LastIndex(val, "@")+1:] + only := form.app.Settings().EmailAuth.OnlyDomains + except := form.app.Settings().EmailAuth.ExceptDomains + + // only domains check + if len(only) > 0 && !list.ExistInSlice(domain, only) { + return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed.") + } + + // except domains check + if len(except) > 0 && list.ExistInSlice(domain, except) { + return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed.") + } + + return nil +} + +// Submit validates the form and upserts the form user model. +func (form *UserUpsert) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + if form.Password != "" { + form.user.SetPassword(form.Password) + } + + if !form.isCreate && form.Email != form.user.Email { + form.user.Verified = false + form.user.LastVerificationSentAt = types.DateTime{} // reset + } + + form.user.Email = form.Email + + return form.app.Dao().SaveUser(form.user) +} diff --git a/forms/user_upsert_test.go b/forms/user_upsert_test.go new file mode 100644 index 00000000..4612c180 --- /dev/null +++ b/forms/user_upsert_test.go @@ -0,0 +1,242 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tests" +) + +func TestNewUserUpsert(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + user := &models.User{} + user.Email = "new@example.com" + + form := forms.NewUserUpsert(app, user) + + // check defaults loading + if form.Email != user.Email { + t.Fatalf("Expected email %q, got %q", user.Email, form.Email) + } +} + +func TestUserUpsertValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // mock app constraints + app.Settings().EmailAuth.MinPasswordLength = 5 + app.Settings().EmailAuth.ExceptDomains = []string{"test.com"} + app.Settings().EmailAuth.OnlyDomains = []string{"example.com", "test.com"} + + scenarios := []struct { + id string + jsonData string + expectedErrors []string + }{ + // empty data - create + { + "", + `{}`, + []string{"email", "password", "passwordConfirm"}, + }, + // empty data - update + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{}`, + []string{}, + }, + // invalid email address + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"invalid"}`, + []string{"email"}, + }, + // unique email constraint check (same email, aka. no changes) + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"test@example.com"}`, + []string{}, + }, + // unique email constraint check (existing email) + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"test2@something.com"}`, + []string{"email"}, + }, + // unique email constraint check (new email) + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"new@example.com"}`, + []string{}, + }, + // EmailAuth.OnlyDomains constraints check + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"test@something.com"}`, + []string{"email"}, + }, + // EmailAuth.ExceptDomains constraints check + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"test@test.com"}`, + []string{"email"}, + }, + // password length constraint check + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"password":"1234", "passwordConfirm": "1234"}`, + []string{"password"}, + }, + // passwords mismatch + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"password":"12345", "passwordConfirm": "54321"}`, + []string{"passwordConfirm"}, + }, + // valid data - all fields + { + "", + `{"email":"new@example.com","password":"12345","passwordConfirm":"12345"}`, + []string{}, + }, + } + + for i, s := range scenarios { + user := &models.User{} + if s.id != "" { + user, _ = app.Dao().FindUserById(s.id) + } + + form := forms.NewUserUpsert(app, user) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Validate() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +func TestUserUpsertSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + id string + jsonData string + expectError bool + }{ + // empty fields - create (Validate call check) + { + "", + `{}`, + true, + }, + // empty fields - update (Validate call check) + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{}`, + false, + }, + // updating with existing user email + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"test2@example.com"}`, + true, + }, + // updating with nonexisting user email + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"email":"update_new@example.com"}`, + false, + }, + // changing password + { + "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c", + `{"password":"123456789","passwordConfirm":"123456789"}`, + false, + }, + // creating user (existing email) + { + "", + `{"email":"test3@example.com","password":"123456789","passwordConfirm":"123456789"}`, + true, + }, + // creating user (new email) + { + "", + `{"email":"create_new@example.com","password":"123456789","passwordConfirm":"123456789"}`, + false, + }, + } + + for i, s := range scenarios { + user := &models.User{} + originalUser := &models.User{} + if s.id != "" { + user, _ = app.Dao().FindUserById(s.id) + originalUser, _ = app.Dao().FindUserById(s.id) + } + + form := forms.NewUserUpsert(app, user) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + if s.expectError { + continue + } + + if user.Email != form.Email { + t.Errorf("(%d) Expected email %q, got %q", i, form.Email, user.Email) + } + + // on email change Verified should reset + if user.Email != originalUser.Email && user.Verified { + t.Errorf("(%d) Expected Verified to be false, got true", i) + } + + if form.Password != "" && !user.ValidatePassword(form.Password) { + t.Errorf("(%d) Expected password to be updated to %q", i, form.Password) + } + if form.Password != "" && originalUser.TokenKey == user.TokenKey { + t.Errorf("(%d) Expected TokenKey to change, got %q", i, user.TokenKey) + } + } +} diff --git a/forms/user_verification_confirm.go b/forms/user_verification_confirm.go new file mode 100644 index 00000000..4bcd0935 --- /dev/null +++ b/forms/user_verification_confirm.go @@ -0,0 +1,73 @@ +package forms + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" +) + +// UserVerificationConfirm defines a user email confirmation form. +type UserVerificationConfirm struct { + app core.App + + Token string `form:"token" json:"token"` +} + +// NewUserVerificationConfirm creates a new user email confirmation form. +func NewUserVerificationConfirm(app core.App) *UserVerificationConfirm { + return &UserVerificationConfirm{ + app: app, + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +func (form *UserVerificationConfirm) Validate() error { + return validation.ValidateStruct(form, + validation.Field(&form.Token, validation.Required, validation.By(form.checkToken)), + ) +} + +func (form *UserVerificationConfirm) checkToken(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + user, err := form.app.Dao().FindUserByToken( + v, + form.app.Settings().UserVerificationToken.Secret, + ) + if err != nil || user == nil { + return validation.NewError("validation_invalid_token", "Invalid or expired token.") + } + + return nil +} + +// Submit validates and submits the form. +// On success returns the verified user model associated to `form.Token`. +func (form *UserVerificationConfirm) Submit() (*models.User, error) { + if err := form.Validate(); err != nil { + return nil, err + } + + user, err := form.app.Dao().FindUserByToken( + form.Token, + form.app.Settings().UserVerificationToken.Secret, + ) + if err != nil { + return nil, err + } + + if user.Verified { + return user, nil // already verified + } + + user.Verified = true + + if err := form.app.Dao().SaveUser(user); err != nil { + return nil, err + } + + return user, nil +} diff --git a/forms/user_verification_confirm_test.go b/forms/user_verification_confirm_test.go new file mode 100644 index 00000000..5291eba0 --- /dev/null +++ b/forms/user_verification_confirm_test.go @@ -0,0 +1,140 @@ +package forms_test + +import ( + "encoding/json" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestUserVerificationConfirmValidate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty data + { + `{}`, + []string{"token"}, + }, + // empty fields + { + `{"token":""}`, + []string{"token"}, + }, + // invalid JWT token + { + `{"token":"invalid"}`, + []string{"token"}, + }, + // expired token + { + `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNjQwOTkxNjYxfQ.6KBn19eFa9aFAZ6hvuhQtK7Ovxb6QlBQ97vJtulb_P8"}`, + []string{"token"}, + }, + // valid token + { + `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxOTA2MTA2NDIxfQ.yvH96FwtPHGvzhFSKl8Tsi1FnGytKpMrvb7K9F2_zQA"}`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewUserVerificationConfirm(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Validate() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +func TestUserVerificationConfirmSubmit(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + scenarios := []struct { + jsonData string + expectError bool + }{ + // empty data (Validate call check) + { + `{}`, + true, + }, + // expired token (Validate call check) + { + `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNjQwOTkxNjYxfQ.6KBn19eFa9aFAZ6hvuhQtK7Ovxb6QlBQ97vJtulb_P8"}`, + true, + }, + // valid token (already verified user) + { + `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRkMDE5N2NjLTJiNGEtM2Y4My1hMjZiLWQ3N2JjODQyM2QzYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxOTA2MTA2NDIxfQ.yvH96FwtPHGvzhFSKl8Tsi1FnGytKpMrvb7K9F2_zQA"}`, + false, + }, + // valid token (unverified user) + { + `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjdiYzg0ZDI3LTZiYTItYjQyYS0zODNmLTQxOTdjYzNkM2QwYyIsInR5cGUiOiJ1c2VyIiwiZW1haWwiOiJ0ZXN0MkBleGFtcGxlLmNvbSIsImV4cCI6MTkwNjEwNjQyMX0.KbSucLGasQqTkGxUgqaaCjKNOHJ3ZVkL1WTzSApc6oM"}`, + false, + }, + } + + for i, s := range scenarios { + form := forms.NewUserVerificationConfirm(app) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + user, err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + if s.expectError { + continue + } + + claims, _ := security.ParseUnverifiedJWT(form.Token) + tokenUserId, _ := claims["id"] + + if user.Id != tokenUserId { + t.Errorf("(%d) Expected user.Id %q, got %q", i, tokenUserId, user.Id) + } + + if !user.Verified { + t.Errorf("(%d) Expected user.Verified to be true, got false", i) + } + } +} diff --git a/forms/user_verification_request.go b/forms/user_verification_request.go new file mode 100644 index 00000000..54cb3938 --- /dev/null +++ b/forms/user_verification_request.go @@ -0,0 +1,74 @@ +package forms + +import ( + "errors" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tools/types" +) + +// UserVerificationRequest defines a user email verification request form. +type UserVerificationRequest struct { + app core.App + resendThreshold float64 + + Email string `form:"email" json:"email"` +} + +// NewUserVerificationRequest creates a new user email verification request form. +func NewUserVerificationRequest(app core.App) *UserVerificationRequest { + return &UserVerificationRequest{ + app: app, + resendThreshold: 120, // 2 min + } +} + +// Validate makes the form validatable by implementing [validation.Validatable] interface. +// +// // This method doesn't verify that user with `form.Email` exists (this is done on Submit). +func (form *UserVerificationRequest) Validate() error { + return validation.ValidateStruct(form, + validation.Field( + &form.Email, + validation.Required, + validation.Length(1, 255), + is.Email, + ), + ) +} + +// Submit validates and sends a verification request email +// to the `form.Email` user. +func (form *UserVerificationRequest) Submit() error { + if err := form.Validate(); err != nil { + return err + } + + user, err := form.app.Dao().FindUserByEmail(form.Email) + if err != nil { + return err + } + + if user.Verified { + return nil // already verified + } + + now := time.Now().UTC() + lastVerificationSentAt := user.LastVerificationSentAt.Time() + if (now.Sub(lastVerificationSentAt)).Seconds() < form.resendThreshold { + return errors.New("A verification email was already sent.") + } + + if err := mails.SendUserVerification(form.app, user); err != nil { + return err + } + + // update last sent timestamp + user.LastVerificationSentAt = types.NowDateTime() + + return form.app.Dao().SaveUser(user) +} diff --git a/forms/user_verification_request_test.go b/forms/user_verification_request_test.go new file mode 100644 index 00000000..9ece9188 --- /dev/null +++ b/forms/user_verification_request_test.go @@ -0,0 +1,171 @@ +package forms_test + +import ( + "encoding/json" + "testing" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestUserVerificationRequestValidate(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + jsonData string + expectedErrors []string + }{ + // empty data + { + `{}`, + []string{"email"}, + }, + // empty fields + { + `{"email":""}`, + []string{"email"}, + }, + // invalid email format + { + `{"email":"invalid"}`, + []string{"email"}, + }, + // valid email + { + `{"email":"new@example.com"}`, + []string{}, + }, + } + + for i, s := range scenarios { + form := forms.NewUserVerificationRequest(testApp) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + // parse errors + result := form.Validate() + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("(%d) Failed to parse errors %v", i, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("(%d) Expected error keys %v, got %v", i, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("(%d) Missing expected error key %q in %v", i, k, errs) + } + } + } +} + +func TestUserVerificationRequestSubmit(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + scenarios := []struct { + jsonData string + expectError bool + expectMail bool + }{ + // empty field (Validate call check) + { + `{"email":""}`, + true, + false, + }, + // invalid email field (Validate call check) + { + `{"email":"invalid"}`, + true, + false, + }, + // nonexisting user + { + `{"email":"missing@example.com"}`, + true, + false, + }, + // existing user (already verified) + { + `{"email":"test@example.com"}`, + false, + false, + }, + // existing user (already verified) - repeating request to test threshod skip + { + `{"email":"test@example.com"}`, + false, + false, + }, + // existing user (unverified) + { + `{"email":"test2@example.com"}`, + false, + true, + }, + // existing user (inverified) - reached send threshod + { + `{"email":"test2@example.com"}`, + true, + false, + }, + } + + now := types.NowDateTime() + time.Sleep(1 * time.Millisecond) + + for i, s := range scenarios { + testApp.TestMailer.TotalSend = 0 // reset + form := forms.NewUserVerificationRequest(testApp) + + // load data + loadErr := json.Unmarshal([]byte(s.jsonData), form) + if loadErr != nil { + t.Errorf("(%d) Failed to load form data: %v", i, loadErr) + continue + } + + err := form.Submit() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + expectedMails := 0 + if s.expectMail { + expectedMails = 1 + } + if testApp.TestMailer.TotalSend != expectedMails { + t.Errorf("(%d) Expected %d mail(s) to be sent, got %d", i, expectedMails, testApp.TestMailer.TotalSend) + } + + if s.expectError { + continue + } + + user, err := testApp.Dao().FindUserByEmail(form.Email) + if err != nil { + t.Errorf("(%d) Expected user with email %q to exist, got nil", i, form.Email) + continue + } + + // check whether LastVerificationSentAt was updated + if !user.Verified && user.LastVerificationSentAt.Time().Sub(now.Time()) < 0 { + t.Errorf("(%d) Expected LastVerificationSentAt to be after %v, got %v", i, now, user.LastVerificationSentAt) + } + } +} diff --git a/forms/validators/file.go b/forms/validators/file.go new file mode 100644 index 00000000..042b0fa3 --- /dev/null +++ b/forms/validators/file.go @@ -0,0 +1,63 @@ +package validators + +import ( + "encoding/binary" + "fmt" + "net/http" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/rest" +) + +// UploadedFileSize checks whether the validated `rest.UploadedFile` +// size is no more than the provided maxBytes. +// +// Example: +// validation.Field(&form.File, validation.By(validators.UploadedFileSize(1000))) +func UploadedFileSize(maxBytes int) validation.RuleFunc { + return func(value any) error { + v, _ := value.(*rest.UploadedFile) + if v == nil { + return nil // nothing to validate + } + + if binary.Size(v.Bytes()) > maxBytes { + return validation.NewError("validation_file_size_limit", fmt.Sprintf("Maximum allowed file size is %v bytes.", maxBytes)) + } + + return nil + } +} + +// UploadedFileMimeType checks whether the validated `rest.UploadedFile` +// mimetype is within the provided allowed mime types. +// +// Example: +// validMimeTypes := []string{"test/plain","image/jpeg"} +// validation.Field(&form.File, validation.By(validators.UploadedFileMimeType(validMimeTypes))) +func UploadedFileMimeType(validTypes []string) validation.RuleFunc { + return func(value any) error { + v, _ := value.(*rest.UploadedFile) + if v == nil { + return nil // nothing to validate + } + + if len(validTypes) == 0 { + return validation.NewError("validation_invalid_mime_type", "Unsupported file type.") + } + + filetype := http.DetectContentType(v.Bytes()) + + for _, t := range validTypes { + if t == filetype { + return nil // valid + } + } + + return validation.NewError("validation_invalid_mime_type", fmt.Sprintf( + "The following mime types are only allowed: %s.", + strings.Join(validTypes, ","), + )) + } +} diff --git a/forms/validators/file_test.go b/forms/validators/file_test.go new file mode 100644 index 00000000..2aa49298 --- /dev/null +++ b/forms/validators/file_test.go @@ -0,0 +1,92 @@ +package validators_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/pocketbase/pocketbase/forms/validators" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/rest" +) + +func TestUploadedFileSize(t *testing.T) { + data, mp, err := tests.MockMultipartData(nil, "test") + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodPost, "/", data) + req.Header.Add("Content-Type", mp.FormDataContentType()) + + files, err := rest.FindUploadedFiles(req, "test") + if err != nil { + t.Fatal(err) + } + + if len(files) != 1 { + t.Fatalf("Expected one test file, got %d", len(files)) + } + + scenarios := []struct { + maxBytes int + file *rest.UploadedFile + expectError bool + }{ + {0, nil, false}, + {4, nil, false}, + {3, files[0], true}, // all test files have "test" as content + {4, files[0], false}, + {5, files[0], false}, + } + + for i, s := range scenarios { + err := validators.UploadedFileSize(s.maxBytes)(s.file) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + } +} + +func TestUploadedFileMimeType(t *testing.T) { + data, mp, err := tests.MockMultipartData(nil, "test") + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodPost, "/", data) + req.Header.Add("Content-Type", mp.FormDataContentType()) + + files, err := rest.FindUploadedFiles(req, "test") + if err != nil { + t.Fatal(err) + } + + if len(files) != 1 { + t.Fatalf("Expected one test file, got %d", len(files)) + } + + scenarios := []struct { + types []string + file *rest.UploadedFile + expectError bool + }{ + {nil, nil, false}, + {[]string{"image/jpeg"}, nil, false}, + {[]string{}, files[0], true}, + {[]string{"image/jpeg"}, files[0], true}, + // test files are detected as "text/plain; charset=utf-8" content type + {[]string{"image/jpeg", "text/plain; charset=utf-8"}, files[0], false}, + } + + for i, s := range scenarios { + err := validators.UploadedFileMimeType(s.types)(s.file) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + } +} diff --git a/forms/validators/record_data.go b/forms/validators/record_data.go new file mode 100644 index 00000000..b215bb2b --- /dev/null +++ b/forms/validators/record_data.go @@ -0,0 +1,418 @@ +package validators + +import ( + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/pocketbase/dbx" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/types" +) + +var requiredErr = validation.NewError("validation_required", "Missing required value") + +// NewRecordDataValidator creates new [models.Record] data validator +// using the provided record constraints and schema. +// +// Example: +// validator := NewRecordDataValidator(app.Dao(), record, nil) +// err := validator.Validate(map[string]any{"test":123}) +func NewRecordDataValidator( + dao *daos.Dao, + record *models.Record, + uploadedFiles []*rest.UploadedFile, +) *RecordDataValidator { + return &RecordDataValidator{ + dao: dao, + record: record, + uploadedFiles: uploadedFiles, + } +} + +// RecordDataValidator defines a model.Record data validator +// using the provided record constraints and schema. +type RecordDataValidator struct { + dao *daos.Dao + record *models.Record + uploadedFiles []*rest.UploadedFile +} + +// Validate validates the provided `data` by checking it against +// the validator record constraints and schema. +func (validator *RecordDataValidator) Validate(data map[string]any) error { + keyedSchema := validator.record.Collection().Schema.AsMap() + if len(keyedSchema) == 0 { + return nil // no fields to check + } + + if len(data) == 0 { + return validation.NewError("validation_empty_data", "No data to validate") + } + + errs := validation.Errors{} + + // check for unknown fields + for key := range data { + if _, ok := keyedSchema[key]; !ok { + errs[key] = validation.NewError("validation_unknown_field", "Unknown field") + } + } + if len(errs) > 0 { + return errs + } + + for key, field := range keyedSchema { + // normalize value to emulate the same behavior + // when fetching or persisting the record model + value := field.PrepareValue(data[key]) + + // check required constraint + if field.Required && validation.Required.Validate(value) != nil { + errs[key] = requiredErr + continue + } + + // validate field value by its field type + if err := validator.checkFieldValue(field, value); err != nil { + errs[key] = err + continue + } + + // check unique constraint + if field.Unique && !validator.dao.IsRecordValueUnique( + validator.record.Collection(), + key, + value, + validator.record.GetId(), + ) { + errs[key] = validation.NewError("validation_not_unique", "Value must be unique") + continue + } + } + + if len(errs) == 0 { + return nil + } + + return errs +} + +func (validator *RecordDataValidator) checkFieldValue(field *schema.SchemaField, value any) error { + switch field.Type { + case schema.FieldTypeText: + return validator.checkTextValue(field, value) + case schema.FieldTypeNumber: + return validator.checkNumberValue(field, value) + case schema.FieldTypeBool: + return validator.checkBoolValue(field, value) + case schema.FieldTypeEmail: + return validator.checkEmailValue(field, value) + case schema.FieldTypeUrl: + return validator.checkUrlValue(field, value) + case schema.FieldTypeDate: + return validator.checkDateValue(field, value) + case schema.FieldTypeSelect: + return validator.checkSelectValue(field, value) + case schema.FieldTypeJson: + return validator.checkJsonValue(field, value) + case schema.FieldTypeFile: + return validator.checkFileValue(field, value) + case schema.FieldTypeRelation: + return validator.checkRelationValue(field, value) + case schema.FieldTypeUser: + return validator.checkUserValue(field, value) + } + + return nil +} + +func (validator *RecordDataValidator) checkTextValue(field *schema.SchemaField, value any) error { + val, _ := value.(string) + if val == "" { + return nil // nothing to check + } + + options, _ := field.Options.(*schema.TextOptions) + + if options.Min != nil && len(val) < *options.Min { + return validation.NewError("validation_min_text_constraint", fmt.Sprintf("Must be at least %d character(s)", *options.Min)) + } + + if options.Max != nil && len(val) > *options.Max { + return validation.NewError("validation_max_text_constraint", fmt.Sprintf("Must be less than %d character(s)", *options.Max)) + } + + if options.Pattern != "" { + match, _ := regexp.MatchString(options.Pattern, val) + if !match { + return validation.NewError("validation_invalid_format", "Invalid value format") + } + } + + return nil +} + +func (validator *RecordDataValidator) checkNumberValue(field *schema.SchemaField, value any) error { + if value == nil { + return nil // nothing to check + } + + val, _ := value.(float64) + options, _ := field.Options.(*schema.NumberOptions) + + if options.Min != nil && val < *options.Min { + return validation.NewError("validation_min_number_constraint", fmt.Sprintf("Must be larger than %f", *options.Min)) + } + + if options.Max != nil && val > *options.Max { + return validation.NewError("validation_max_number_constraint", fmt.Sprintf("Must be less than %f", *options.Max)) + } + + return nil +} + +func (validator *RecordDataValidator) checkBoolValue(field *schema.SchemaField, value any) error { + return nil +} + +func (validator *RecordDataValidator) checkEmailValue(field *schema.SchemaField, value any) error { + val, _ := value.(string) + if val == "" { + return nil // nothing to check + } + + if is.Email.Validate(val) != nil { + return validation.NewError("validation_invalid_email", "Must be a valid email") + } + + options, _ := field.Options.(*schema.EmailOptions) + domain := val[strings.LastIndex(val, "@")+1:] + + // only domains check + if len(options.OnlyDomains) > 0 && !list.ExistInSlice(domain, options.OnlyDomains) { + return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed") + } + + // except domains check + if len(options.ExceptDomains) > 0 && list.ExistInSlice(domain, options.ExceptDomains) { + return validation.NewError("validation_email_domain_not_allowed", "Email domain is not allowed") + } + + return nil +} + +func (validator *RecordDataValidator) checkUrlValue(field *schema.SchemaField, value any) error { + val, _ := value.(string) + if val == "" { + return nil // nothing to check + } + + if is.URL.Validate(val) != nil { + return validation.NewError("validation_invalid_url", "Must be a valid url") + } + + options, _ := field.Options.(*schema.UrlOptions) + + // extract host/domain + u, _ := url.Parse(val) + host := u.Host + + // only domains check + if len(options.OnlyDomains) > 0 && !list.ExistInSlice(host, options.OnlyDomains) { + return validation.NewError("validation_url_domain_not_allowed", "Url domain is not allowed") + } + + // except domains check + if len(options.ExceptDomains) > 0 && list.ExistInSlice(host, options.ExceptDomains) { + return validation.NewError("validation_url_domain_not_allowed", "Url domain is not allowed") + } + + return nil +} + +func (validator *RecordDataValidator) checkDateValue(field *schema.SchemaField, value any) error { + val, _ := value.(types.DateTime) + + if val.IsZero() { + if field.Required { + return requiredErr + } + return nil // nothing to check + } + + options, _ := field.Options.(*schema.DateOptions) + + if !options.Min.IsZero() { + if err := validation.Min(options.Min.Time()).Validate(val.Time()); err != nil { + return err + } + } + + if !options.Max.IsZero() { + if err := validation.Max(options.Max.Time()).Validate(val.Time()); err != nil { + return err + } + } + + return nil +} + +func (validator *RecordDataValidator) checkSelectValue(field *schema.SchemaField, value any) error { + normalizedVal := list.ToUniqueStringSlice(value) + if len(normalizedVal) == 0 { + return nil // nothing to check + } + + options, _ := field.Options.(*schema.SelectOptions) + + // check max selected items + if len(normalizedVal) > options.MaxSelect { + return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", options.MaxSelect)) + } + + // check against the allowed values + for _, val := range normalizedVal { + if !list.ExistInSlice(val, options.Values) { + return validation.NewError("validation_invalid_value", "Invalid value "+val) + } + } + + return nil +} + +func (validator *RecordDataValidator) checkJsonValue(field *schema.SchemaField, value any) error { + raw, _ := types.ParseJsonRaw(value) + if len(raw) == 0 { + return nil // nothing to check + } + + if is.JSON.Validate(value) != nil { + return validation.NewError("validation_invalid_json", "Must be a valid json value") + } + + return nil +} + +func (validator *RecordDataValidator) checkFileValue(field *schema.SchemaField, value any) error { + // normalize value access + var names []string + switch v := value.(type) { + case []string: + names = v + case string: + names = []string{v} + } + + options, _ := field.Options.(*schema.FileOptions) + + if len(names) > options.MaxSelect { + return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", options.MaxSelect)) + } + + // extract the uploaded files + files := []*rest.UploadedFile{} + if len(validator.uploadedFiles) > 0 { + for _, file := range validator.uploadedFiles { + if list.ExistInSlice(file.Name(), names) { + files = append(files, file) + } + } + } + + for _, file := range files { + // check size + if err := UploadedFileSize(options.MaxSize)(file); err != nil { + return err + } + + // check type + if len(options.MimeTypes) > 0 { + if err := UploadedFileMimeType(options.MimeTypes)(file); err != nil { + return err + } + } + } + + return nil +} + +func (validator *RecordDataValidator) checkRelationValue(field *schema.SchemaField, value any) error { + // normalize value access + var ids []string + switch v := value.(type) { + case []string: + ids = v + case string: + ids = []string{v} + } + + if len(ids) == 0 { + return nil // nothing to check + } + + options, _ := field.Options.(*schema.RelationOptions) + + if len(ids) > options.MaxSelect { + return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", options.MaxSelect)) + } + + // check if the related records exist + // --- + relCollection, err := validator.dao.FindCollectionByNameOrId(options.CollectionId) + if err != nil { + return validation.NewError("validation_missing_rel_collection", "Relation connection is missing or cannot be accessed") + } + + var total int + validator.dao.RecordQuery(relCollection). + Select("count(*)"). + AndWhere(dbx.In("id", list.ToInterfaceSlice(ids)...)). + Row(&total) + if total != len(ids) { + return validation.NewError("validation_missing_rel_records", "Failed to fetch all relation records with the provided ids") + } + // --- + + return nil +} + +func (validator *RecordDataValidator) checkUserValue(field *schema.SchemaField, value any) error { + // normalize value access + var ids []string + switch v := value.(type) { + case []string: + ids = v + case string: + ids = []string{v} + } + + if len(ids) == 0 { + return nil // nothing to check + } + + options, _ := field.Options.(*schema.UserOptions) + + if len(ids) > options.MaxSelect { + return validation.NewError("validation_too_many_values", fmt.Sprintf("Select no more than %d", options.MaxSelect)) + } + + // check if the related users exist + var total int + validator.dao.UserQuery(). + Select("count(*)"). + AndWhere(dbx.In("id", list.ToInterfaceSlice(ids)...)). + Row(&total) + if total != len(ids) { + return validation.NewError("validation_missing_users", "Failed to fetch all users with the provided ids") + } + + return nil +} diff --git a/forms/validators/record_data_test.go b/forms/validators/record_data_test.go new file mode 100644 index 00000000..daa34a4d --- /dev/null +++ b/forms/validators/record_data_test.go @@ -0,0 +1,1443 @@ +package validators_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/forms/validators" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/rest" + "github.com/pocketbase/pocketbase/tools/types" +) + +type testDataFieldScenario struct { + name string + data map[string]any + files []*rest.UploadedFile + expectedErrors []string +} + +func TestRecordDataValidatorEmptyAndUnknown(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, _ := app.Dao().FindCollectionByNameOrId("demo") + record := models.NewRecord(collection) + validator := validators.NewRecordDataValidator(app.Dao(), record, nil) + + emptyErr := validator.Validate(map[string]any{}) + if emptyErr == nil { + t.Fatal("Expected error for empty data, got nil") + } + + unknownErr := validator.Validate(map[string]any{"unknown": 123}) + if unknownErr == nil { + t.Fatal("Expected error for unknown data, got nil") + } +} + +func TestRecordDataValidatorValidateText(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + min := 3 + max := 10 + pattern := `^\w+$` + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeText, + Options: &schema.TextOptions{ + Min: &min, + Max: &max, + Pattern: pattern, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", "test") + dummy.SetDataValue("field2", "test") + dummy.SetDataValue("field3", "test") + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": "test", + "field2": "test", + "field3": "test", + }, + nil, + []string{"field3"}, + }, + { + "check min constraint", + map[string]any{ + "field1": "test", + "field2": "test", + "field3": strings.Repeat("a", min-1), + }, + nil, + []string{"field3"}, + }, + { + "check max constraint", + map[string]any{ + "field1": "test", + "field2": "test", + "field3": strings.Repeat("a", max+1), + }, + nil, + []string{"field3"}, + }, + { + "check pattern constraint", + map[string]any{ + "field1": nil, + "field2": "test", + "field3": "test!", + }, + nil, + []string{"field3"}, + }, + { + "valid data (only required)", + map[string]any{ + "field2": "test", + }, + nil, + []string{}, + }, + { + "valid data (all)", + map[string]any{ + "field1": "test", + "field2": 12345, // test value cast + "field3": "test2", + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateNumber(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + min := 1.0 + max := 150.0 + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeNumber, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeNumber, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeNumber, + Options: &schema.NumberOptions{ + Min: &min, + Max: &max, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", 123) + dummy.SetDataValue("field2", 123) + dummy.SetDataValue("field3", 123) + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check required constraint + casting", + map[string]any{ + "field1": "invalid", + "field2": "invalid", + "field3": "invalid", + }, + nil, + []string{"field2", "field3"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": 123, + "field2": 123, + "field3": 123, + }, + nil, + []string{"field3"}, + }, + { + "check min constraint", + map[string]any{ + "field1": 0.5, + "field2": 1, + "field3": min - 0.5, + }, + nil, + []string{"field3"}, + }, + { + "check max constraint", + map[string]any{ + "field1": nil, + "field2": max, + "field3": max + 0.5, + }, + nil, + []string{"field3"}, + }, + { + "valid data (only required)", + map[string]any{ + "field2": 1, + }, + nil, + []string{}, + }, + { + "valid data (all)", + map[string]any{ + "field1": nil, + "field2": 123, // test value cast + "field3": max, + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateBool(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeBool, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeBool, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeBool, + Options: &schema.BoolOptions{}, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", false) + dummy.SetDataValue("field2", true) + dummy.SetDataValue("field3", true) + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check required constraint + casting", + map[string]any{ + "field1": "invalid", + "field2": "invalid", + "field3": "invalid", + }, + nil, + []string{"field2"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": true, + "field2": true, + "field3": true, + }, + nil, + []string{"field3"}, + }, + { + "valid data (only required)", + map[string]any{ + "field2": 1, + }, + nil, + []string{}, + }, + { + "valid data (all)", + map[string]any{ + "field1": false, + "field2": true, + "field3": false, + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateEmail(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeEmail, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeEmail, + Options: &schema.EmailOptions{ + ExceptDomains: []string{"example.com"}, + }, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeEmail, + Options: &schema.EmailOptions{ + OnlyDomains: []string{"example.com"}, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", "test@demo.com") + dummy.SetDataValue("field2", "test@test.com") + dummy.SetDataValue("field3", "test@example.com") + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check email format validator", + map[string]any{ + "field1": "test", + "field2": "test.com", + "field3": 123, + }, + nil, + []string{"field1", "field2", "field3"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": "test@example.com", + "field2": "test@test.com", + "field3": "test@example.com", + }, + nil, + []string{"field3"}, + }, + { + "check ExceptDomains constraint", + map[string]any{ + "field1": "test@example.com", + "field2": "test@example.com", + "field3": "test2@example.com", + }, + nil, + []string{"field2"}, + }, + { + "check OnlyDomains constraint", + map[string]any{ + "field1": "test@test.com", + "field2": "test@test.com", + "field3": "test@test.com", + }, + nil, + []string{"field3"}, + }, + { + "valid data (only required)", + map[string]any{ + "field2": "test@test.com", + }, + nil, + []string{}, + }, + { + "valid data (all)", + map[string]any{ + "field1": "123@example.com", + "field2": "test@test.com", + "field3": "test2@example.com", + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateUrl(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeUrl, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeUrl, + Options: &schema.UrlOptions{ + ExceptDomains: []string{"example.com"}, + }, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeUrl, + Options: &schema.UrlOptions{ + OnlyDomains: []string{"example.com"}, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", "http://demo.com") + dummy.SetDataValue("field2", "http://test.com") + dummy.SetDataValue("field3", "http://example.com") + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check url format validator", + map[string]any{ + "field1": "/abc", + "field2": "test.com", // valid + "field3": "test@example.com", + }, + nil, + []string{"field1", "field3"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": "http://example.com", + "field2": "http://test.com", + "field3": "http://example.com", + }, + nil, + []string{"field3"}, + }, + { + "check ExceptDomains constraint", + map[string]any{ + "field1": "http://example.com", + "field2": "http://example.com", + "field3": "https://example.com", + }, + nil, + []string{"field2"}, + }, + { + "check OnlyDomains constraint", + map[string]any{ + "field1": "http://test.com/abc", + "field2": "http://test.com/abc", + "field3": "http://test.com/abc", + }, + nil, + []string{"field3"}, + }, + { + "check subdomains constraint", + map[string]any{ + "field1": "http://test.test.com", + "field2": "http://test.example.com", + "field3": "http://test.example.com", + }, + nil, + []string{"field3"}, + }, + { + "valid data (only required)", + map[string]any{ + "field2": "http://sub.test.com/abc", + }, + nil, + []string{}, + }, + { + "valid data (all)", + map[string]any{ + "field1": "http://example.com/123", + "field2": "http://test.com/", + "field3": "http://example.com/test2", + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateDate(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + min, _ := types.ParseDateTime("2022-01-01 01:01:01.123") + max, _ := types.ParseDateTime("2030-01-01 01:01:01") + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeDate, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeDate, + Options: &schema.DateOptions{ + Min: min, + }, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeDate, + Options: &schema.DateOptions{ + Max: max, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", "2022-01-01 01:01:01") + dummy.SetDataValue("field2", "2029-01-01 01:01:01.123") + dummy.SetDataValue("field3", "2029-01-01 01:01:01.123") + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check required constraint + cast", + map[string]any{ + "field1": "invalid", + "field2": "invalid", + "field3": "invalid", + }, + nil, + []string{"field2"}, + }, + { + "check required constraint + zero datetime", + map[string]any{ + "field1": "January 1, year 1, 00:00:00 UTC", + "field2": "0001-01-01 00:00:00", + "field3": "0001-01-01 00:00:00 +0000 UTC", + }, + nil, + []string{"field2"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": "2029-01-01 01:01:01.123", + "field2": "2029-01-01 01:01:01.123", + "field3": "2029-01-01 01:01:01.123", + }, + nil, + []string{"field3"}, + }, + { + "check min date constraint", + map[string]any{ + "field1": "2021-01-01 01:01:01", + "field2": "2021-01-01 01:01:01", + "field3": "2021-01-01 01:01:01", + }, + nil, + []string{"field2"}, + }, + { + "check max date constraint", + map[string]any{ + "field1": "2030-02-01 01:01:01", + "field2": "2030-02-01 01:01:01", + "field3": "2030-02-01 01:01:01", + }, + nil, + []string{"field3"}, + }, + { + "valid data (only required)", + map[string]any{ + "field2": "2029-01-01 01:01:01", + }, + nil, + []string{}, + }, + { + "valid data (all)", + map[string]any{ + "field1": "2029-01-01 01:01:01.000", + "field2": "2029-01-01 01:01:01", + "field3": "2029-01-01 01:01:01.456", + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateSelect(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + Values: []string{"1", "a", "b", "c"}, + MaxSelect: 1, + }, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + Values: []string{"a", "b", "c"}, + MaxSelect: 2, + }, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + Values: []string{"a", "b", "c"}, + MaxSelect: 99, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", "a") + dummy.SetDataValue("field2", []string{"a", "b"}) + dummy.SetDataValue("field3", []string{"a", "b", "c"}) + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check required constraint - empty values", + map[string]any{ + "field1": "", + "field2": "", + "field3": "", + }, + nil, + []string{"field2"}, + }, + { + "check required constraint - multiple select cast", + map[string]any{ + "field1": "a", + "field2": "a", + "field3": "a", + }, + nil, + []string{}, + }, + { + "check unique constraint", + map[string]any{ + "field1": "a", + "field2": "b", + "field3": []string{"a", "b", "c"}, + }, + nil, + []string{"field3"}, + }, + { + "check unique constraint - same elements but different order", + map[string]any{ + "field1": "a", + "field2": "b", + "field3": []string{"a", "c", "b"}, + }, + nil, + []string{}, + }, + { + "check Values constraint", + map[string]any{ + "field1": 1, + "field2": "d", + "field3": 123, + }, + nil, + []string{"field2", "field3"}, + }, + { + "check MaxSelect constraint", + map[string]any{ + "field1": []string{"a", "b"}, // this will be normalized to a single string value + "field2": []string{"a", "b", "c"}, + "field3": []string{"a", "b", "b", "b"}, // repeating values will be merged + }, + nil, + []string{"field2"}, + }, + { + "valid data - only required fields", + map[string]any{ + "field2": []string{"a", "b"}, + }, + nil, + []string{}, + }, + { + "valid data - all fields with normalizations", + map[string]any{ + "field1": "a", + "field2": []string{"a", "b", "b"}, // will be collapsed + "field3": "b", // will be normalzied to slice + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateJson(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeJson, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeJson, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeJson, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", `{"test":123}`) + dummy.SetDataValue("field2", `{"test":123}`) + dummy.SetDataValue("field3", `{"test":123}`) + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint - nil", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check required constraint - zero string", + map[string]any{ + "field1": "", + "field2": "", + "field3": "", + }, + nil, + []string{"field2"}, + }, + { + "check required constraint - zero number", + map[string]any{ + "field1": 0, + "field2": 0, + "field3": 0, + }, + nil, + []string{}, + }, + { + "check required constraint - zero slice", + map[string]any{ + "field1": []string{}, + "field2": []string{}, + "field3": []string{}, + }, + nil, + []string{}, + }, + { + "check required constraint - zero map", + map[string]any{ + "field1": map[string]string{}, + "field2": map[string]string{}, + "field3": map[string]string{}, + }, + nil, + []string{}, + }, + { + "check unique constraint", + map[string]any{ + "field1": `{"test":123}`, + "field2": `{"test":123}`, + "field3": map[string]any{"test": 123}, + }, + nil, + []string{"field3"}, + }, + { + "check json text validator", + map[string]any{ + "field1": `[1, 2, 3`, + "field2": `invalid`, + "field3": `null`, // valid + }, + nil, + []string{"field1", "field2"}, + }, + { + "valid data - only required fields", + map[string]any{ + "field2": `{"test":123}`, + }, + nil, + []string{}, + }, + { + "valid data - all fields with normalizations", + map[string]any{ + "field1": []string{"a", "b", "c"}, + "field2": 123, + "field3": `"test"`, + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateFile(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 1, + MaxSize: 3, + }, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 2, + MaxSize: 10, + MimeTypes: []string{"image/jpeg", "text/plain; charset=utf-8"}, + }, + }, + &schema.SchemaField{ + Name: "field3", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 3, + MaxSize: 10, + MimeTypes: []string{"image/jpeg"}, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // stub uploaded files + data, mp, err := tests.MockMultipartData(nil, "test", "test", "test", "test", "test") + if err != nil { + t.Fatal(err) + } + req := httptest.NewRequest(http.MethodPost, "/", data) + req.Header.Add("Content-Type", mp.FormDataContentType()) + testFiles, err := rest.FindUploadedFiles(req, "test") + if err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint - nil", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check MaxSelect constraint", + map[string]any{ + "field1": "test1", + "field2": []string{"test1", testFiles[0].Name(), testFiles[3].Name()}, + "field3": []string{"test1", "test2", "test3", "test4"}, + }, + []*rest.UploadedFile{testFiles[0], testFiles[1], testFiles[2]}, + []string{"field2", "field3"}, + }, + { + "check MaxSize constraint", + map[string]any{ + "field1": testFiles[0].Name(), + "field2": []string{"test1", testFiles[0].Name()}, + "field3": []string{"test1", "test2", "test3"}, + }, + []*rest.UploadedFile{testFiles[0], testFiles[1], testFiles[2]}, + []string{"field1"}, + }, + { + "check MimeTypes constraint", + map[string]any{ + "field1": "test1", + "field2": []string{"test1", testFiles[0].Name()}, + "field3": []string{testFiles[1].Name(), testFiles[2].Name()}, + }, + []*rest.UploadedFile{testFiles[0], testFiles[1], testFiles[2]}, + []string{"field3"}, + }, + { + "valid data - no new files (just file ids)", + map[string]any{ + "field1": "test1", + "field2": []string{"test1", "test2"}, + "field3": []string{"test1", "test2", "test3"}, + }, + nil, + []string{}, + }, + { + "valid data - just new files", + map[string]any{ + "field1": nil, + "field2": []string{testFiles[0].Name(), testFiles[1].Name()}, + "field3": nil, + }, + []*rest.UploadedFile{testFiles[0], testFiles[1], testFiles[2]}, + []string{}, + }, + { + "valid data - mixed existing and new files", + map[string]any{ + "field1": "test1", + "field2": []string{"test1", testFiles[0].Name()}, + "field3": "test1", // will be casted + }, + []*rest.UploadedFile{testFiles[0], testFiles[1], testFiles[2]}, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateRelation(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + demo, _ := app.Dao().FindCollectionByNameOrId("demo4") + + // demo4 rel ids + relId1 := "b8ba58f9-e2d7-42a0-b0e7-a11efd98236b" + relId2 := "df55c8ff-45ef-4c82-8aed-6e2183fe1125" + relId3 := "b84cd893-7119-43c9-8505-3c4e22da28a9" + relId4 := "054f9f24-0a0a-4e09-87b1-bc7ff2b336a2" + + // record rel ids from different collections + diffRelId1 := "63c2ab80-84ab-4057-a592-4604a731f78f" + diffRelId2 := "2c542824-9de1-42fe-8924-e57c86267760" + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{ + MaxSelect: 1, + CollectionId: demo.Id, + }, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{ + MaxSelect: 2, + CollectionId: demo.Id, + }, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{ + MaxSelect: 3, + CollectionId: demo.Id, + }, + }, + &schema.SchemaField{ + Name: "field4", + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{ + MaxSelect: 3, + CollectionId: "", // missing or nonexisting colleciton id + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", relId1) + dummy.SetDataValue("field2", []string{relId1, relId2}) + dummy.SetDataValue("field3", []string{relId1, relId2, relId3}) + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint - nil", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check required constraint - zero id", + map[string]any{ + "field1": "", + "field2": "", + "field3": "", + }, + nil, + []string{"field2"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": relId1, + "field2": relId2, + "field3": []string{relId1, relId2, relId3, relId3}, // repeating values are collapsed + }, + nil, + []string{"field3"}, + }, + { + "check nonexisting collection id", + map[string]any{ + "field2": relId1, + "field4": relId1, + }, + nil, + []string{"field4"}, + }, + { + "check MaxSelect constraint", + map[string]any{ + "field1": []string{relId1, relId2}, // will be normalized to relId1 only + "field2": []string{relId1, relId2, relId3}, + "field3": []string{relId1, relId2, relId3, relId4}, + }, + nil, + []string{"field2", "field3"}, + }, + { + "check with ids from different collections", + map[string]any{ + "field1": diffRelId1, + "field2": []string{relId2, diffRelId1}, + "field3": []string{diffRelId1, diffRelId2}, + }, + nil, + []string{"field1", "field2", "field3"}, + }, + { + "valid data - only required fields", + map[string]any{ + "field2": []string{relId1, relId2}, + }, + nil, + []string{}, + }, + { + "valid data - all fields with normalization", + map[string]any{ + "field1": []string{relId1, relId2}, + "field2": relId2, + "field3": []string{relId3, relId2, relId1}, // unique is not triggered because the order is different + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func TestRecordDataValidatorValidateUser(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + userId1 := "97cc3d3d-6ba2-383f-b42a-7bc84d27410c" + userId2 := "7bc84d27-6ba2-b42a-383f-4197cc3d3d0c" + userId3 := "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c" + missingUserId := "00000000-84ab-4057-a592-4604a731f78f" + + // create new test collection + collection := &models.Collection{} + collection.Name = "validate_test" + collection.Schema = schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{ + MaxSelect: 1, + }, + }, + &schema.SchemaField{ + Name: "field2", + Required: true, + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{ + MaxSelect: 2, + }, + }, + &schema.SchemaField{ + Name: "field3", + Unique: true, + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{ + MaxSelect: 3, + }, + }, + ) + if err := app.Dao().SaveCollection(collection); err != nil { + t.Fatal(err) + } + + // create dummy record (used for the unique check) + dummy := models.NewRecord(collection) + dummy.SetDataValue("field1", userId1) + dummy.SetDataValue("field2", []string{userId1, userId2}) + dummy.SetDataValue("field3", []string{userId1, userId2, userId3}) + if err := app.Dao().SaveRecord(dummy); err != nil { + t.Fatal(err) + } + + scenarios := []testDataFieldScenario{ + { + "check required constraint - nil", + map[string]any{ + "field1": nil, + "field2": nil, + "field3": nil, + }, + nil, + []string{"field2"}, + }, + { + "check required constraint - zero id", + map[string]any{ + "field1": "", + "field2": "", + "field3": "", + }, + nil, + []string{"field2"}, + }, + { + "check unique constraint", + map[string]any{ + "field1": nil, + "field2": userId1, + "field3": []string{userId1, userId2, userId3, userId3}, // repeating values are collapsed + }, + nil, + []string{"field3"}, + }, + { + "check MaxSelect constraint", + map[string]any{ + "field1": []string{userId1, userId2}, // maxSelect is 1 and will be normalized to userId1 only + "field2": []string{userId1, userId2, userId3}, + "field3": []string{userId1, userId3, userId2}, + }, + nil, + []string{"field2"}, + }, + { + "check with mixed existing and nonexisting user ids", + map[string]any{ + "field1": missingUserId, + "field2": []string{missingUserId, userId1}, + "field3": []string{userId1, missingUserId}, + }, + nil, + []string{"field1", "field2", "field3"}, + }, + { + "valid data - only required fields", + map[string]any{ + "field2": []string{userId1, userId2}, + }, + nil, + []string{}, + }, + { + "valid data - all fields with normalization", + map[string]any{ + "field1": []string{userId1, userId2}, + "field2": userId2, + "field3": []string{userId3, userId2, userId1}, // unique is not triggered because the order is different + }, + nil, + []string{}, + }, + } + + checkValidatorErrors(t, app.Dao(), models.NewRecord(collection), scenarios) +} + +func checkValidatorErrors(t *testing.T, dao *daos.Dao, record *models.Record, scenarios []testDataFieldScenario) { + for i, s := range scenarios { + validator := validators.NewRecordDataValidator(dao, record, s.files) + result := validator.Validate(s.data) + + prefix := fmt.Sprintf("%d", i) + if s.name != "" { + prefix = s.name + } + + // parse errors + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("[%s] Failed to parse errors %v", prefix, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("[%s] Expected error keys %v, got %v", prefix, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("[%s] Missing expected error key %q in %v", prefix, k, errs) + } + } + } +} diff --git a/forms/validators/string.go b/forms/validators/string.go new file mode 100644 index 00000000..10f5202a --- /dev/null +++ b/forms/validators/string.go @@ -0,0 +1,21 @@ +package validators + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" +) + +// Compare checks whether the validated value matches another string. +// +// Example: +// validation.Field(&form.PasswordConfirm, validation.By(validators.Compare(form.Password))) +func Compare(valueToCompare string) validation.RuleFunc { + return func(value any) error { + v, _ := value.(string) + + if v != valueToCompare { + return validation.NewError("validation_values_mismatch", "Values don't match.") + } + + return nil + } +} diff --git a/forms/validators/string_test.go b/forms/validators/string_test.go new file mode 100644 index 00000000..e9a0c6a0 --- /dev/null +++ b/forms/validators/string_test.go @@ -0,0 +1,30 @@ +package validators_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/forms/validators" +) + +func TestCompare(t *testing.T) { + scenarios := []struct { + valA string + valB string + expectError bool + }{ + {"", "", false}, + {"", "456", true}, + {"123", "", true}, + {"123", "456", true}, + {"123", "123", false}, + } + + for i, s := range scenarios { + err := validators.Compare(s.valA)(s.valB) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, s.expectError, hasErr, err) + } + } +} diff --git a/forms/validators/validators.go b/forms/validators/validators.go new file mode 100644 index 00000000..ec8c2177 --- /dev/null +++ b/forms/validators/validators.go @@ -0,0 +1,2 @@ +// Package validators implements custom shared PocketBase validators. +package validators diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..3d31fa80 --- /dev/null +++ b/go.mod @@ -0,0 +1,87 @@ +module github.com/pocketbase/pocketbase + +go 1.18 + +require ( + github.com/AlecAivazis/survey/v2 v2.3.5 + github.com/aws/aws-sdk-go v1.44.48 + github.com/disintegration/imaging v1.6.2 + github.com/domodwyer/mailyak/v3 v3.3.3 + github.com/fatih/color v1.13.0 + github.com/ganigeorgiev/fexpr v0.1.1 + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 + github.com/golang-jwt/jwt/v4 v4.4.2 + github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 + github.com/mattn/go-sqlite3 v1.14.14 + github.com/microcosm-cc/bluemonday v1.0.19 + github.com/pocketbase/dbx v1.6.0 + github.com/spf13/cast v1.5.0 + github.com/spf13/cobra v1.5.0 + gocloud.dev v0.25.0 + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d + golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 + modernc.org/sqlite v1.17.3 +) + +require ( + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/aws/aws-sdk-go-v2 v1.16.7 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.15.13 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.12.8 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.8 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.11.11 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.16.9 // indirect + github.com/aws/smithy-go v1.12.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/googleapis/gax-go/v2 v2.4.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + go.opencensus.io v0.23.0 // indirect + golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 // indirect + golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect + golang.org/x/tools v0.1.11 // indirect + golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + google.golang.org/api v0.86.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220706132729-d86698d07c53 // indirect + google.golang.org/grpc v1.47.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.36.0 // indirect + modernc.org/ccgo/v3 v3.16.6 // indirect + modernc.org/libc v1.16.14 // indirect + modernc.org/mathutil v1.4.1 // indirect + modernc.org/memory v1.1.1 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.2 // indirect + modernc.org/token v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..7f10de2d --- /dev/null +++ b/go.sum @@ -0,0 +1,1151 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.82.0/go.mod h1:vlKccHJGuFBFufnAnuB08dfEH9Y3H7dzDzRECFdC2TA= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= +cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= +cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= +cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= +cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= +cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= +contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= +contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= +contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= +github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= +github.com/Azure/azure-amqp-common-go/v3 v3.2.2/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI= +github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v59.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/Azure/azure-service-bus-go v0.11.5/go.mod h1:MI6ge2CuQWBVq+ly456MY7XqNLJip5LO1iSFodbNLbU= +github.com/Azure/azure-storage-blob-go v0.14.0 h1:1BCg74AmVdYwO3dlKwtFU1V0wU2PZdREkXvAmZJRUlM= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-amqp v0.16.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= +github.com/Azure/go-amqp v0.16.4/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.19/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= +github.com/Azure/go-autorest/autorest v0.11.22/go.mod h1:BAWYUWGPEtKPzjVkp0Q6an0MJcJDsoh5Z1BFAEFs4Xs= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.17/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.9/go.mod h1:hg3/1yw0Bq87O3KvvnJoAh34/0zbP7SFizX/qN5JvjU= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbPSRmHvSXXHOwGRyeXh1jVdquA2G8= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.48 h1:jLDC9RsNoYMLFlKpB8LdqUnoDdC2yvkS4QbuyPQJ8+M= +github.com/aws/aws-sdk-go v1.44.48/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= +github.com/aws/aws-sdk-go-v2 v1.16.7 h1:zfBwXus3u14OszRxGcqCDS4MfMCv10e8SMJ2r8Xm0Ns= +github.com/aws/aws-sdk-go-v2 v1.16.7/go.mod h1:6CpKuLXg2w7If3ABZCl/qZ6rEgwtjZTn4eAf4RcEyuw= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3 h1:S/ZBwevQkr7gv5YxONYpGQxlMFFYSRfz3RMcjsC9Qhk= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.3/go.mod h1:gNsR5CaXKmQSSzrmGxmwmct/r+ZBfbxorAuXYsj/M5Y= +github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= +github.com/aws/aws-sdk-go-v2/config v1.15.13 h1:CJH9zn/Enst7lDiGpoguVt0lZr5HcpNVlRJWbJ6qreo= +github.com/aws/aws-sdk-go-v2/config v1.15.13/go.mod h1:AcMu50uhV6wMBUlURnEXhr9b3fX6FLSTlEV89krTEGk= +github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= +github.com/aws/aws-sdk-go-v2/credentials v1.12.8 h1:niTa7zc7uyOP2ufri0jPESBt1h9yP3Zc0q+xzih3h8o= +github.com/aws/aws-sdk-go-v2/credentials v1.12.8/go.mod h1:P2Hd4Sy7mXRxPNcQMPBmqszSJoDXexX8XEDaT6lucO0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8 h1:VfBdn2AxwMbFyJN/lF/xuT3SakomJ86PZu3rCxb5K0s= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8/go.mod h1:oL1Q3KuCq1D4NykQnIvtRiBGLUXhcpY5pl6QZB2XEPU= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.19 h1:WfCYqsAADDRNCQQ5LGcrlqbR7SK3PYrP/UCh7qNGBQM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.19/go.mod h1:koLPv2oF6ksE3zBKLDP0GFmKfaCmYwVHqGIbaPrHIRg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14 h1:2C0pYHcUBmdzPj+EKNC4qj97oK6yjrUhc1KoSodglvk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14/go.mod h1:kdjrMwHwrC3+FsKhNcCMJ7tUVj/8uSD5CZXeQ4wV6fM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8 h1:2J+jdlBJWEmTyAwC82Ym68xCykIvnSnIN18b8xHGlcc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8/go.mod h1:ZIV8GYoC6WLBW5KGs+o4rsc65/ozd+eQ0L31XF5VDwk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15 h1:QquxR7NH3ULBsKC+NoTpilzbKKS+5AELfNREInbhvas= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15/go.mod h1:Tkrthp/0sNBShQQsamR7j/zY4p19tVTAs+nnqhH6R3c= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.5 h1:tEEHn+PGAxRVqMPEhtU8oCSW/1Ge3zP5nUgPrGQNUPs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.5/go.mod h1:aIwFF3dUk95ocCcA3zfk3nhz0oLkpzHFWuMp8l/4nNs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3 h1:4n4KCtv5SUoT5Er5XV41huuzrCqepxlW3SDI9qHQebc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.3/go.mod h1:gkb2qADY+OHaGLKNTYxMaQNacfeyQpZ4csDTQMeFmcw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.9 h1:gVv2vXOMqJeR4ZHHV32K7LElIJIIzyw/RU1b0lSfWTQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.9/go.mod h1:EF5RLnD9l0xvEWwMRcktIS/dI6lF8lU5eV3B13k6sWo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.8 h1:oKnAXxSF2FUvfgw8uzU/v9OTYorJJZ8eBmWhr9TWVVQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.8/go.mod h1:rDVhIMAX9N2r8nWxDUlbubvvaFMnfsm+3jAV7q+rpM4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.8 h1:TlN1UC39A0LUNoD51ubO5h32haznA+oVe15jO9O4Lj0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.8/go.mod h1:JlVwmWtT/1c5W+6oUsjXjAJ0iJZ+hlghdrDy/8JxGCU= +github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g= +github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1 h1:OKQIQ0QhEBmGr2LfT952meIZz3ujrPYnxH+dO/5ldnI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.27.1/go.mod h1:NffjpNsMUFXp6Ok/PahrktAncoekWrywvmIK83Q2raE= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o= +github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw= +github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM= +github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.11 h1:XOJWXNFXJyapJqQuCIPfftsOf0XZZioM0kK6OPRt9MY= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.11/go.mod h1:MO4qguFjs3wPGcCSpQ7kOFTwRvb+eu+fn+1vKleGHUk= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.9 h1:yOfILxyjmtr2ubRkRJldlHDFBhf5vw4CzhbwWIBmimQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.9/go.mod h1:O1IvkYxr+39hRf960Us6j0x1P8pDqhTX+oXM5kQNl/Y= +github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= +github.com/aws/smithy-go v1.12.0 h1:gXpeZel/jPoWQ7OEmLIgCUnhkFftqNfwWUwAHSlp1v0= +github.com/aws/smithy-go v1.12.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfzrpArPY/aFvc9yU= +github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/domodwyer/mailyak/v3 v3.3.3 h1:E9cjqDUiwY1QSE5G2CbWHM7EJV5FybKPHnGovc2iaA8= +github.com/domodwyer/mailyak/v3 v3.3.3/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/ganigeorgiev/fexpr v0.1.1 h1:La9kYEgTcIutvOnqNZ8pOUD0O0Q/Gn15sTVEX+IeBE8= +github.com/ganigeorgiev/fexpr v0.1.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= +github.com/google/go-replayers/httpreplay v1.1.1 h1:H91sIMlt1NZzN7R+/ASswyouLJfW0WLW7fhyUFvDEkY= +github.com/google/go-replayers/httpreplay v1.1.1/go.mod h1:gN9GeLIs7l6NUoVaSSnv2RiqK1NiwAmD0MrKeC9IIks= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE= +github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= +github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.11.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.10.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.15.0/go.mod h1:D/zyOyXiaM1TmVWnOM18p0xdDtdakRBa0RsVGI3U3bw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 h1:lFz33AOOXwTpqOiHvrN8nmTdkxSfuNLHLPjgQ1muPpU= +github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198/go.mod h1:uh3YlzsEJj7OG57rDWj6c3WEkOF1ZHGBQkDuUZw3rE8= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-ieproxy v0.0.3 h1:YkaHmK1CzE5C4O7A3hv3TCbfNDPSCf0RKZFX+VhBeYk= +github.com/mattn/go-ieproxy v0.0.3/go.mod h1:6ZpRmhBaYuBX1U2za+9rC9iCGLsSp2tftelZne7CPko= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c= +github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pocketbase/dbx v1.6.0 h1:iPQi99GpaMRne0KRVnd/kCfxayCP/f4QDb6hGxMRI3I= +github.com/pocketbase/dbx v1.6.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= +gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220617043117-41969df76e82 h1:KpZB5pUSBvrHltNEdK/tw0xlPeD13M6M6aGP32gKqiw= +golang.org/x/image v0.0.0-20220617043117-41969df76e82/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 h1:8NSylCMxLW4JvserAndSgFL7aPli6A68yf0bYFTcWCM= +golang.org/x/net v0.0.0-20220706163947-c90051bbdb60/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 h1:VnGaRqoLmqZH/3TMLJwYCEWkR4j1nuIU1U9TvbqsDUw= +golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM= +google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.68.0/go.mod h1:sOM8pTpwgflXRhz+oC8H2Dr+UcbMqkPPWNJo88Q7TH8= +google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.86.0 h1:ZAnyOHQFIuWso1BodVfSaRyffD74T9ERGFa3k1fNk/U= +google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220706132729-d86698d07c53 h1:liFd7OL799HvMNYG5xozhUoWDj944y+zXPDOhu4PyaM= +google.golang.org/genproto v0.0.0-20220706132729-d86698d07c53/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6 h1:3l18poV+iUemQ98O3X5OMr97LOqlzis+ytivU4NqGhA= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.14 h1:MUIjk9Xwlkrp0BqGhMfRkiq0EkZsqfNiP4eixL3YiPk= +modernc.org/libc v1.16.14/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1 h1:bDOL0DIDLQv7bWhP3gMvIrnoFw+Eo6F7a2QK9HPDiFU= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.17.3 h1:iE+coC5g17LtByDYDWKpR6m2Z9022YrSh3bumwOnIrI= +modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.2 h1:iFBDH6j1Z0bN/Q9udJnnFoFpENA4252qe/7/5woE5MI= +modernc.org/strutil v1.1.2/go.mod h1:OYajnUAcI/MX+XD/Wx7v1bbdvcQSvxgtb0gC+u3d3eg= +modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/mails/admin.go b/mails/admin.go new file mode 100644 index 00000000..252e069d --- /dev/null +++ b/mails/admin.go @@ -0,0 +1,76 @@ +package mails + +import ( + "fmt" + "net/mail" + "strings" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails/templates" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tokens" +) + +// SendAdminPasswordReset sends a password reset request email to the specified admin. +func SendAdminPasswordReset(app core.App, admin *models.Admin) error { + token, tokenErr := tokens.NewAdminResetPasswordToken(app, admin) + if tokenErr != nil { + return tokenErr + } + + actionUrl, urlErr := normalizeUrl(fmt.Sprintf( + "%s/#/confirm-password-reset/%s", + strings.TrimSuffix(app.Settings().Meta.AppUrl, "/"), + token, + )) + if urlErr != nil { + return urlErr + } + + params := struct { + AppName string + AppUrl string + Admin *models.Admin + Token string + ActionUrl string + }{ + AppName: app.Settings().Meta.AppName, + AppUrl: app.Settings().Meta.AppUrl, + Admin: admin, + Token: token, + ActionUrl: actionUrl, + } + + mailClient := app.NewMailClient() + + event := &core.MailerAdminEvent{ + MailClient: mailClient, + Admin: admin, + Meta: map[string]any{"token": token}, + } + + sendErr := app.OnMailerBeforeAdminResetPasswordSend().Trigger(event, func(e *core.MailerAdminEvent) error { + // resolve body template + body, renderErr := resolveTemplateContent(params, templates.Layout, templates.AdminPasswordResetBody) + if renderErr != nil { + return renderErr + } + + return e.MailClient.Send( + mail.Address{ + Name: app.Settings().Meta.SenderName, + Address: app.Settings().Meta.SenderAddress, + }, + mail.Address{Address: e.Admin.Email}, + "Reset admin password", + body, + nil, + ) + }) + + if sendErr == nil { + app.OnMailerAfterAdminResetPasswordSend().Trigger(event) + } + + return sendErr +} diff --git a/mails/admin_test.go b/mails/admin_test.go new file mode 100644 index 00000000..49ed865a --- /dev/null +++ b/mails/admin_test.go @@ -0,0 +1,37 @@ +package mails_test + +import ( + "strings" + "testing" + + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tests" +) + +func TestSendAdminPasswordReset(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + // ensure that action url normalization will be applied + testApp.Settings().Meta.AppUrl = "http://localhost:8090////" + + admin, _ := testApp.Dao().FindAdminByEmail("test@example.com") + + err := mails.SendAdminPasswordReset(testApp, admin) + if err != nil { + t.Fatal(err) + } + + if testApp.TestMailer.TotalSend != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + } + + expectedParts := []string{ + "http://localhost:8090/#/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", + } + for _, part := range expectedParts { + if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody) + } + } +} diff --git a/mails/base.go b/mails/base.go new file mode 100644 index 00000000..156389b3 --- /dev/null +++ b/mails/base.go @@ -0,0 +1,58 @@ +// Package mails implements various helper methods for sending user and admin +// emails like forgotten password, verification, etc. +package mails + +import ( + "bytes" + "net/url" + "path" + "strings" + "text/template" +) + +// normalizeUrl removes duplicated slashes from a url path. +func normalizeUrl(originalUrl string) (string, error) { + u, err := url.Parse(originalUrl) + if err != nil { + return "", err + } + + hasSlash := strings.HasSuffix(u.Path, "/") + + // clean up path by removing duplicated / + u.Path = path.Clean(u.Path) + u.RawPath = path.Clean(u.RawPath) + + // restore original trailing slash + if hasSlash && !strings.HasSuffix(u.Path, "/") { + u.Path += "/" + u.RawPath += "/" + } + + return u.String(), nil +} + +// resolveTemplateContent resolves inline html template strings. +func resolveTemplateContent(data any, content ...string) (string, error) { + if len(content) == 0 { + return "", nil + } + + t := template.New("inline_template") + + var parseErr error + for _, v := range content { + t, parseErr = t.Parse(v) + if parseErr != nil { + return "", parseErr + } + } + + var wr bytes.Buffer + + if executeErr := t.Execute(&wr, data); executeErr != nil { + return "", executeErr + } + + return wr.String(), nil +} diff --git a/mails/templates/admin_password_reset.go b/mails/templates/admin_password_reset.go new file mode 100644 index 00000000..e0d76058 --- /dev/null +++ b/mails/templates/admin_password_reset.go @@ -0,0 +1,25 @@ +package templates + +// Available variables: +// +// ``` +// Admin *models.Admin +// AppName string +// AppUrl string +// Token string +// ActionUrl string +// ``` +const AdminPasswordResetBody = ` +{{define "content"}} +

Hello,

+ +

Follow this link to reset your admin password for {{.AppName}}.

+ +

+ Reset password + {{.ActionUrl}} +

+ +

If you did not request to reset your password, please ignore this email and the link will expire on its own.

+{{end}} +` diff --git a/mails/templates/html_content.go b/mails/templates/html_content.go new file mode 100644 index 00000000..cb412751 --- /dev/null +++ b/mails/templates/html_content.go @@ -0,0 +1,8 @@ +package templates + +// Available variables: +// +// ``` +// HtmlContent template.HTML +// ``` +const HtmlBody = `{{define "content"}}{{.HtmlContent}}{{end}}` diff --git a/mails/templates/layout.go b/mails/templates/layout.go new file mode 100644 index 00000000..cbc65727 --- /dev/null +++ b/mails/templates/layout.go @@ -0,0 +1,117 @@ +package templates + +const Layout = ` + + + + + + + + +
+
+ {{template "content" .}} +
+
+ + +` diff --git a/mails/templates/user_confirm_email_change.go b/mails/templates/user_confirm_email_change.go new file mode 100644 index 00000000..98cbbdea --- /dev/null +++ b/mails/templates/user_confirm_email_change.go @@ -0,0 +1,26 @@ +package templates + +// Available variables: +// +// ``` +// User *models.User +// AppName string +// AppUrl string +// Token string +// ActionUrl string +// ``` +const UserConfirmEmailChangeBody = ` +{{define "content"}} +

Hello,

+

Click on the button below to confirm your new email address.

+

+ Confirm new email + {{.ActionUrl}} +

+

If you didn’t ask to change your email address, you can ignore this email.

+

+ Thanks,
+ {{.AppName}} team +

+{{end}} +` diff --git a/mails/templates/user_password_reset.go b/mails/templates/user_password_reset.go new file mode 100644 index 00000000..10258ad1 --- /dev/null +++ b/mails/templates/user_password_reset.go @@ -0,0 +1,26 @@ +package templates + +// Available variables: +// +// ``` +// User *models.User +// AppName string +// AppUrl string +// Token string +// ActionUrl string +// ``` +const UserPasswordResetBody = ` +{{define "content"}} +

Hello,

+

Click on the button below to reset your password.

+

+ Reset password + {{.ActionUrl}} +

+

If you didn’t ask to reset your password, you can ignore this email.

+

+ Thanks,
+ {{.AppName}} team +

+{{end}} +` diff --git a/mails/templates/user_verification.go b/mails/templates/user_verification.go new file mode 100644 index 00000000..b44d7928 --- /dev/null +++ b/mails/templates/user_verification.go @@ -0,0 +1,26 @@ +package templates + +// Available variables: +// +// ``` +// User *models.User +// AppName string +// AppUrl string +// Token string +// ActionUrl string +// ``` +const UserVerificationBody = ` +{{define "content"}} +

Hello,

+

Thank you for joining us at {{.AppName}}.

+

Click on the button below to verify your email address.

+

+ Verify + {{.ActionUrl}} +

+

+ Thanks,
+ {{.AppName}} team +

+{{end}} +` diff --git a/mails/user.go b/mails/user.go new file mode 100644 index 00000000..e9f61828 --- /dev/null +++ b/mails/user.go @@ -0,0 +1,192 @@ +package mails + +import ( + "net/mail" + "strings" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/mails/templates" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tokens" +) + +func prepareUserEmailBody( + app core.App, + user *models.User, + token string, + actionUrl string, + bodyTemplate string, +) (string, error) { + settings := app.Settings() + + // replace action url placeholder params (if any) + actionUrlParams := map[string]string{ + core.EmailPlaceholderAppUrl: settings.Meta.AppUrl, + core.EmailPlaceholderToken: token, + } + for k, v := range actionUrlParams { + actionUrl = strings.ReplaceAll(actionUrl, k, v) + } + var urlErr error + actionUrl, urlErr = normalizeUrl(actionUrl) + if urlErr != nil { + return "", urlErr + } + + params := struct { + AppName string + AppUrl string + User *models.User + Token string + ActionUrl string + }{ + AppName: settings.Meta.AppName, + AppUrl: settings.Meta.AppUrl, + User: user, + Token: token, + ActionUrl: actionUrl, + } + + return resolveTemplateContent(params, templates.Layout, bodyTemplate) +} + +// SendUserPasswordReset sends a password reset request email to the specified user. +func SendUserPasswordReset(app core.App, user *models.User) error { + token, tokenErr := tokens.NewUserResetPasswordToken(app, user) + if tokenErr != nil { + return tokenErr + } + + mailClient := app.NewMailClient() + + event := &core.MailerUserEvent{ + MailClient: mailClient, + User: user, + Meta: map[string]any{"token": token}, + } + + sendErr := app.OnMailerBeforeUserResetPasswordSend().Trigger(event, func(e *core.MailerUserEvent) error { + body, err := prepareUserEmailBody( + app, + user, + token, + app.Settings().Meta.UserResetPasswordUrl, + templates.UserPasswordResetBody, + ) + if err != nil { + return err + } + + return e.MailClient.Send( + mail.Address{ + Name: app.Settings().Meta.SenderName, + Address: app.Settings().Meta.SenderAddress, + }, + mail.Address{Address: e.User.Email}, + ("Reset your " + app.Settings().Meta.AppName + " password"), + body, + nil, + ) + }) + + if sendErr == nil { + app.OnMailerAfterUserResetPasswordSend().Trigger(event) + } + + return sendErr +} + +// SendUserVerification sends a verification request email to the specified user. +func SendUserVerification(app core.App, user *models.User) error { + token, tokenErr := tokens.NewUserVerifyToken(app, user) + if tokenErr != nil { + return tokenErr + } + + mailClient := app.NewMailClient() + + event := &core.MailerUserEvent{ + MailClient: mailClient, + User: user, + Meta: map[string]any{"token": token}, + } + + sendErr := app.OnMailerBeforeUserVerificationSend().Trigger(event, func(e *core.MailerUserEvent) error { + body, err := prepareUserEmailBody( + app, + user, + token, + app.Settings().Meta.UserVerificationUrl, + templates.UserVerificationBody, + ) + if err != nil { + return err + } + + return e.MailClient.Send( + mail.Address{ + Name: app.Settings().Meta.SenderName, + Address: app.Settings().Meta.SenderAddress, + }, + mail.Address{Address: e.User.Email}, + ("Verify your " + app.Settings().Meta.AppName + " email"), + body, + nil, + ) + }) + + if sendErr == nil { + app.OnMailerAfterUserVerificationSend().Trigger(event) + } + + return sendErr +} + +// SendUserChangeEmail sends a change email confirmation email to the specified user. +func SendUserChangeEmail(app core.App, user *models.User, newEmail string) error { + token, tokenErr := tokens.NewUserChangeEmailToken(app, user, newEmail) + if tokenErr != nil { + return tokenErr + } + + mailClient := app.NewMailClient() + + event := &core.MailerUserEvent{ + MailClient: mailClient, + User: user, + Meta: map[string]any{ + "token": token, + "newEmail": newEmail, + }, + } + + sendErr := app.OnMailerBeforeUserChangeEmailSend().Trigger(event, func(e *core.MailerUserEvent) error { + body, err := prepareUserEmailBody( + app, + user, + token, + app.Settings().Meta.UserConfirmEmailChangeUrl, + templates.UserConfirmEmailChangeBody, + ) + if err != nil { + return err + } + + return e.MailClient.Send( + mail.Address{ + Name: app.Settings().Meta.SenderName, + Address: app.Settings().Meta.SenderAddress, + }, + mail.Address{Address: newEmail}, + ("Confirm your " + app.Settings().Meta.AppName + " new email address"), + body, + nil, + ) + }) + + if sendErr == nil { + app.OnMailerAfterUserChangeEmailSend().Trigger(event) + } + + return sendErr +} diff --git a/mails/user_test.go b/mails/user_test.go new file mode 100644 index 00000000..4f3aa369 --- /dev/null +++ b/mails/user_test.go @@ -0,0 +1,87 @@ +package mails_test + +import ( + "strings" + "testing" + + "github.com/pocketbase/pocketbase/mails" + "github.com/pocketbase/pocketbase/tests" +) + +func TestSendUserPasswordReset(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + // ensure that action url normalization will be applied + testApp.Settings().Meta.AppUrl = "http://localhost:8090////" + + user, _ := testApp.Dao().FindUserByEmail("test@example.com") + + err := mails.SendUserPasswordReset(testApp, user) + if err != nil { + t.Fatal(err) + } + + if testApp.TestMailer.TotalSend != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + } + + expectedParts := []string{ + "http://localhost:8090/#/users/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", + } + for _, part := range expectedParts { + if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody) + } + } +} + +func TestSendUserVerification(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + user, _ := testApp.Dao().FindUserByEmail("test@example.com") + + err := mails.SendUserVerification(testApp, user) + if err != nil { + t.Fatal(err) + } + + if testApp.TestMailer.TotalSend != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + } + + expectedParts := []string{ + "http://localhost:8090/#/users/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", + } + for _, part := range expectedParts { + if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody) + } + } +} + +func TestSendUserChangeEmail(t *testing.T) { + testApp, _ := tests.NewTestApp() + defer testApp.Cleanup() + + user, _ := testApp.Dao().FindUserByEmail("test@example.com") + + err := mails.SendUserChangeEmail(testApp, user, "new_test@example.com") + if err != nil { + t.Fatal(err) + } + + if testApp.TestMailer.TotalSend != 1 { + t.Fatalf("Expected one email to be sent, got %d", testApp.TestMailer.TotalSend) + } + + expectedParts := []string{ + "http://localhost:8090/#/users/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", + } + for _, part := range expectedParts { + if !strings.Contains(testApp.TestMailer.LastHtmlBody, part) { + t.Fatalf("Couldn't find %s \nin\n %s", part, testApp.TestMailer.LastHtmlBody) + } + } +} diff --git a/migrations/1640988000_init.go b/migrations/1640988000_init.go new file mode 100644 index 00000000..0c0999f2 --- /dev/null +++ b/migrations/1640988000_init.go @@ -0,0 +1,141 @@ +// Package migrations contains the system PocketBase DB migrations. +package migrations + +import ( + "fmt" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/migrate" +) + +var AppMigrations migrate.MigrationsList + +// Register is a short alias for `AppMigrations.Register()` +// that is usually used in external/user defined migrations. +func Register( + up func(db dbx.Builder) error, + down func(db dbx.Builder) error, + optFilename ...string, +) { + AppMigrations.Register(up, down, optFilename...) +} + +func init() { + AppMigrations.Register(func(db dbx.Builder) error { + _, tablesErr := db.NewQuery(` + CREATE TABLE {{_admins}} ( + [[id]] TEXT PRIMARY KEY, + [[avatar]] INTEGER DEFAULT 0 NOT NULL, + [[email]] TEXT UNIQUE NOT NULL, + [[tokenKey]] TEXT UNIQUE NOT NULL, + [[passwordHash]] TEXT NOT NULL, + [[lastResetSentAt]] TEXT DEFAULT "" NOT NULL, + [[created]] TEXT DEFAULT "" NOT NULL, + [[updated]] TEXT DEFAULT "" NOT NULL + ); + + CREATE TABLE {{_users}} ( + [[id]] TEXT PRIMARY KEY, + [[verified]] BOOLEAN DEFAULT FALSE NOT NULL, + [[email]] TEXT UNIQUE NOT NULL, + [[tokenKey]] TEXT UNIQUE NOT NULL, + [[passwordHash]] TEXT NOT NULL, + [[lastResetSentAt]] TEXT DEFAULT "" NOT NULL, + [[lastVerificationSentAt]] TEXT DEFAULT "" NOT NULL, + [[created]] TEXT DEFAULT "" NOT NULL, + [[updated]] TEXT DEFAULT "" NOT NULL + ); + + CREATE TABLE {{_collections}} ( + [[id]] TEXT PRIMARY KEY, + [[system]] BOOLEAN DEFAULT FALSE NOT NULL, + [[name]] TEXT UNIQUE NOT NULL, + [[schema]] JSON DEFAULT "[]" NOT NULL, + [[listRule]] TEXT DEFAULT NULL, + [[viewRule]] TEXT DEFAULT NULL, + [[createRule]] TEXT DEFAULT NULL, + [[updateRule]] TEXT DEFAULT NULL, + [[deleteRule]] TEXT DEFAULT NULL, + [[created]] TEXT DEFAULT "" NOT NULL, + [[updated]] TEXT DEFAULT "" NOT NULL + ); + + CREATE TABLE {{_params}} ( + [[id]] TEXT PRIMARY KEY, + [[key]] TEXT UNIQUE NOT NULL, + [[value]] JSON DEFAULT NULL, + [[created]] TEXT DEFAULT "" NOT NULL, + [[updated]] TEXT DEFAULT "" NOT NULL + ); + `).Execute() + if tablesErr != nil { + return tablesErr + } + + // inserts the system profiles collection + // ----------------------------------------------------------- + profileOwnerRule := fmt.Sprintf("%s = @request.user.id", models.ProfileCollectionUserFieldName) + collection := &models.Collection{ + Name: models.ProfileCollectionName, + System: true, + CreateRule: &profileOwnerRule, + ListRule: &profileOwnerRule, + ViewRule: &profileOwnerRule, + UpdateRule: &profileOwnerRule, + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: models.ProfileCollectionUserFieldName, + Type: schema.FieldTypeUser, + Unique: true, + Required: true, + System: true, + Options: &schema.UserOptions{ + MaxSelect: 1, + CascadeDelete: true, + }, + }, + &schema.SchemaField{ + Name: "name", + Type: schema.FieldTypeText, + Options: &schema.TextOptions{}, + }, + &schema.SchemaField{ + Name: "avatar", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 1, + MaxSize: 5242880, + MimeTypes: []string{ + "image/jpg", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + }, + }, + }, + ), + } + + return daos.New(db).SaveCollection(collection) + }, func(db dbx.Builder) error { + tables := []string{ + "_params", + "_collections", + "_users", + "_admins", + models.ProfileCollectionName, + } + + for _, name := range tables { + if _, err := db.DropTable(name).Execute(); err != nil { + return err + } + } + + return nil + }) +} diff --git a/migrations/logs/1640988000_init.go b/migrations/logs/1640988000_init.go new file mode 100644 index 00000000..308f1398 --- /dev/null +++ b/migrations/logs/1640988000_init.go @@ -0,0 +1,38 @@ +package logs + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/migrate" +) + +var LogsMigrations migrate.MigrationsList + +func init() { + LogsMigrations.Register(func(db dbx.Builder) (err error) { + _, err = db.NewQuery(` + CREATE TABLE {{_requests}} ( + [[id]] TEXT PRIMARY KEY, + [[url]] TEXT DEFAULT "" NOT NULL, + [[method]] TEXT DEFAULT "get" NOT NULL, + [[status]] INTEGER DEFAULT 200 NOT NULL, + [[auth]] TEXT DEFAULT "guest" NOT NULL, + [[ip]] TEXT DEFAULT "127.0.0.1" NOT NULL, + [[referer]] TEXT DEFAULT "" NOT NULL, + [[userAgent]] TEXT DEFAULT "" NOT NULL, + [[meta]] JSON DEFAULT "{}" NOT NULL, + [[created]] TEXT DEFAULT "" NOT NULL, + [[updated]] TEXT DEFAULT "" NOT NULL + ); + + CREATE INDEX _request_status_idx on {{_requests}} ([[status]]); + CREATE INDEX _request_auth_idx on {{_requests}} ([[auth]]); + CREATE INDEX _request_ip_idx on {{_requests}} ([[ip]]); + CREATE INDEX _request_created_hour_idx on {{_requests}} (strftime('%Y-%m-%d %H:00:00', [[created]])); + `).Execute() + + return err + }, func(db dbx.Builder) error { + _, err := db.DropTable("_requests").Execute() + return err + }) +} diff --git a/models/admin.go b/models/admin.go new file mode 100644 index 00000000..ff0b1289 --- /dev/null +++ b/models/admin.go @@ -0,0 +1,13 @@ +package models + +var _ Model = (*Admin)(nil) + +type Admin struct { + BaseAccount + + Avatar int `db:"avatar" json:"avatar"` +} + +func (m *Admin) TableName() string { + return "_admins" +} diff --git a/models/admin_test.go b/models/admin_test.go new file mode 100644 index 00000000..d2fc911c --- /dev/null +++ b/models/admin_test.go @@ -0,0 +1,14 @@ +package models_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" +) + +func TestAdminTableName(t *testing.T) { + m := models.Admin{} + if m.TableName() != "_admins" { + t.Fatalf("Unexpected table name, got %q", m.TableName()) + } +} diff --git a/models/base.go b/models/base.go new file mode 100644 index 00000000..9c94e15a --- /dev/null +++ b/models/base.go @@ -0,0 +1,131 @@ +// Package models implements all PocketBase DB models. +package models + +import ( + "errors" + + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/types" + "golang.org/x/crypto/bcrypt" +) + +// ColumnValueMapper defines an interface for custom db model data serialization. +type ColumnValueMapper interface { + // ColumnValueMap returns the data to be used when persisting the model. + ColumnValueMap() map[string]any +} + +// Model defines an interface with common methods that all db models should have. +type Model interface { + TableName() string + HasId() bool + GetId() string + GetCreated() types.DateTime + GetUpdated() types.DateTime + RefreshId() + RefreshCreated() + RefreshUpdated() +} + +// ------------------------------------------------------------------- +// BaseModel +// ------------------------------------------------------------------- + +// BaseModel defines common fields and methods used by all other models. +type BaseModel struct { + Id string `db:"id" json:"id"` + Created types.DateTime `db:"created" json:"created"` + Updated types.DateTime `db:"updated" json:"updated"` +} + +// HasId returns whether the model has a nonzero primary key (aka. id). +func (m *BaseModel) HasId() bool { + return m.GetId() != "" +} + +// GetId returns the model's id. +func (m *BaseModel) GetId() string { + return m.Id +} + +// GetCreated returns the model's Created datetime. +func (m *BaseModel) GetCreated() types.DateTime { + return m.Created +} + +// GetCreated returns the model's Updated datetime. +func (m *BaseModel) GetUpdated() types.DateTime { + return m.Updated +} + +// RefreshId generates and sets a new model id. +// +// The generated id is a cryptographically random 15 characters length string +// (could change in the future). +func (m *BaseModel) RefreshId() { + m.Id = security.RandomString(15) +} + +// RefreshCreated updates the model's Created field with the current datetime. +func (m *BaseModel) RefreshCreated() { + m.Created = types.NowDateTime() +} + +// RefreshCreated updates the model's Created field with the current datetime. +func (m *BaseModel) RefreshUpdated() { + m.Updated = types.NowDateTime() +} + +// ------------------------------------------------------------------- +// BaseAccount +// ------------------------------------------------------------------- + +// BaseAccount defines common fields and methods used by auth models (aka. users and admins). +type BaseAccount struct { + BaseModel + + Email string `db:"email" json:"email"` + TokenKey string `db:"tokenKey" json:"-"` + PasswordHash string `db:"passwordHash" json:"-"` + LastResetSentAt types.DateTime `db:"lastResetSentAt" json:"lastResetSentAt"` +} + +// ValidatePassword validates a plain password against the model's password. +func (m *BaseAccount) ValidatePassword(password string) bool { + bytePassword := []byte(password) + bytePasswordHash := []byte(m.PasswordHash) + + // comparing the password with the hash + err := bcrypt.CompareHashAndPassword(bytePasswordHash, bytePassword) + + // nil means it is a match + return err == nil +} + +// SetPassword sets cryptographically secure string to `model.Password`. +// +// Additionally this method also resets the LastResetSentAt and the TokenKey fields. +func (m *BaseAccount) SetPassword(password string) error { + if password == "" { + return errors.New("The provided plain password is empty") + } + + // hash the password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 13) + if err != nil { + return err + } + + m.PasswordHash = string(hashedPassword) + m.LastResetSentAt = types.DateTime{} // reset + + // invalidate previously issued tokens + m.RefreshTokenKey() + + return nil +} + +// RefreshTokenKey generates and sets new random token key. +func (m *BaseAccount) RefreshTokenKey() { + m.TokenKey = security.RandomString(50) +} diff --git a/models/base_test.go b/models/base_test.go new file mode 100644 index 00000000..9af48de0 --- /dev/null +++ b/models/base_test.go @@ -0,0 +1,178 @@ +package models_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestBaseModelHasId(t *testing.T) { + scenarios := []struct { + model models.BaseModel + expected bool + }{ + { + models.BaseModel{}, + false, + }, + { + models.BaseModel{Id: ""}, + false, + }, + { + models.BaseModel{Id: "abc"}, + true, + }, + } + + for i, s := range scenarios { + result := s.model.HasId() + if result != s.expected { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestBaseModelGetId(t *testing.T) { + m0 := models.BaseModel{} + if m0.GetId() != "" { + t.Fatalf("Expected zero id value, got %v", m0.GetId()) + } + + id := "abc" + m1 := models.BaseModel{Id: id} + if m1.GetId() != id { + t.Fatalf("Expected id %v, got %v", id, m1.GetId()) + } +} + +func TestBaseModelRefreshId(t *testing.T) { + m := models.BaseModel{} + m.RefreshId() + + if m.GetId() == "" { + t.Fatalf("Expected nonempty id value, got %v", m.GetId()) + } +} + +func TestBaseModelCreated(t *testing.T) { + m := models.BaseModel{} + + if !m.GetCreated().IsZero() { + t.Fatalf("Expected zero datetime, got %v", m.GetCreated()) + } + + m.RefreshCreated() + + if m.GetCreated().IsZero() { + t.Fatalf("Expected non-zero datetime, got %v", m.GetCreated()) + } +} + +func TestBaseModelUpdated(t *testing.T) { + m := models.BaseModel{} + + if !m.GetUpdated().IsZero() { + t.Fatalf("Expected zero datetime, got %v", m.GetUpdated()) + } + + m.RefreshUpdated() + + if m.GetUpdated().IsZero() { + t.Fatalf("Expected non-zero datetime, got %v", m.GetUpdated()) + } +} + +// ------------------------------------------------------------------- +// BaseAccount tests +// ------------------------------------------------------------------- + +func TestBaseAccountValidatePassword(t *testing.T) { + scenarios := []struct { + account models.BaseAccount + password string + expected bool + }{ + { + // empty passwordHash + empty pass + models.BaseAccount{}, + "", + false, + }, + { + // empty passwordHash + nonempty pass + models.BaseAccount{}, + "123456", + false, + }, + { + // nonempty passwordHash + empty pass + models.BaseAccount{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."}, + "", + false, + }, + { + // nonempty passwordHash + wrong pass + models.BaseAccount{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."}, + "654321", + false, + }, + { + // nonempty passwordHash + correct pass + models.BaseAccount{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."}, + "123456", + true, + }, + } + + for i, s := range scenarios { + result := s.account.ValidatePassword(s.password) + if result != s.expected { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestBaseAccountSetPassword(t *testing.T) { + m := models.BaseAccount{ + // 123456 + PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv.", + LastResetSentAt: types.NowDateTime(), + TokenKey: "test", + } + + // empty pass + err1 := m.SetPassword("") + if err1 == nil { + t.Fatal("Expected empty password error") + } + + err2 := m.SetPassword("654321") + if err2 != nil { + t.Fatalf("Expected nil, got error %v", err2) + } + + if !m.ValidatePassword("654321") { + t.Fatalf("Password is invalid") + } + + if m.TokenKey == "test" { + t.Fatalf("Expected TokenKey to change, got %v", m.TokenKey) + } + + if !m.LastResetSentAt.IsZero() { + t.Fatalf("Expected LastResetSentAt to be zero datetime, got %v", m.LastResetSentAt) + } +} + +func TestBaseAccountRefreshTokenKey(t *testing.T) { + m := models.BaseAccount{TokenKey: "test"} + + m.RefreshTokenKey() + + // empty pass + if m.TokenKey == "" || m.TokenKey == "test" { + t.Fatalf("Expected TokenKey to change, got %q", m.TokenKey) + } +} diff --git a/models/collection.go b/models/collection.go new file mode 100644 index 00000000..8a008567 --- /dev/null +++ b/models/collection.go @@ -0,0 +1,27 @@ +package models + +import "github.com/pocketbase/pocketbase/models/schema" + +var _ Model = (*Collection)(nil) + +type Collection struct { + BaseModel + + Name string `db:"name" json:"name"` + System bool `db:"system" json:"system"` + Schema schema.Schema `db:"schema" json:"schema"` + ListRule *string `db:"listRule" json:"listRule"` + ViewRule *string `db:"viewRule" json:"viewRule"` + CreateRule *string `db:"createRule" json:"createRule"` + UpdateRule *string `db:"updateRule" json:"updateRule"` + DeleteRule *string `db:"deleteRule" json:"deleteRule"` +} + +func (m *Collection) TableName() string { + return "_collections" +} + +// BaseFilesPath returns the storage dir path used by the collection. +func (m *Collection) BaseFilesPath() string { + return m.Id +} diff --git a/models/collection_test.go b/models/collection_test.go new file mode 100644 index 00000000..16473d01 --- /dev/null +++ b/models/collection_test.go @@ -0,0 +1,25 @@ +package models_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" +) + +func TestCollectionTableName(t *testing.T) { + m := models.Collection{} + if m.TableName() != "_collections" { + t.Fatalf("Unexpected table name, got %q", m.TableName()) + } +} + +func TestCollectionBaseFilesPath(t *testing.T) { + m := models.Collection{} + + m.RefreshId() + + expected := m.Id + if m.BaseFilesPath() != expected { + t.Fatalf("Expected path %s, got %s", expected, m.BaseFilesPath()) + } +} diff --git a/models/param.go b/models/param.go new file mode 100644 index 00000000..cf5ef053 --- /dev/null +++ b/models/param.go @@ -0,0 +1,22 @@ +package models + +import ( + "github.com/pocketbase/pocketbase/tools/types" +) + +var _ Model = (*Param)(nil) + +const ( + ParamAppSettings = "settings" +) + +type Param struct { + BaseModel + + Key string `db:"key" json:"key"` + Value types.JsonRaw `db:"value" json:"value"` +} + +func (m *Param) TableName() string { + return "_params" +} diff --git a/models/param_test.go b/models/param_test.go new file mode 100644 index 00000000..42291551 --- /dev/null +++ b/models/param_test.go @@ -0,0 +1,14 @@ +package models_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" +) + +func TestParamTableName(t *testing.T) { + m := models.Param{} + if m.TableName() != "_params" { + t.Fatalf("Unexpected table name, got %q", m.TableName()) + } +} diff --git a/models/record.go b/models/record.go new file mode 100644 index 00000000..9d68eaed --- /dev/null +++ b/models/record.go @@ -0,0 +1,304 @@ +package models + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +var _ Model = (*Record)(nil) +var _ ColumnValueMapper = (*Record)(nil) + +type Record struct { + BaseModel + + collection *Collection + data map[string]any + expand map[string]any +} + +// NewRecord initializes a new empty Record model. +func NewRecord(collection *Collection) *Record { + return &Record{ + collection: collection, + data: map[string]any{}, + } +} + +// NewRecordFromNullStringMap initializes a single new Record model +// with data loaded from the provided NullStringMap. +func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record { + resultMap := map[string]any{} + + for _, field := range collection.Schema.Fields() { + var rawValue any + + nullString, ok := data[field.Name] + if !ok || !nullString.Valid { + rawValue = nil + } else { + rawValue = nullString.String + } + + resultMap[field.Name] = rawValue + } + + record := NewRecord(collection) + + // load base mode fields + resultMap[schema.ReservedFieldNameId] = data[schema.ReservedFieldNameId].String + resultMap[schema.ReservedFieldNameCreated] = data[schema.ReservedFieldNameCreated].String + resultMap[schema.ReservedFieldNameUpdated] = data[schema.ReservedFieldNameUpdated].String + + if err := record.Load(resultMap); err != nil { + log.Println("Failed to unmarshal record:", err) + } + + return record +} + +// NewRecordsFromNullStringMaps initializes a new Record model for +// each row in the provided NullStringMap slice. +func NewRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) []*Record { + result := []*Record{} + + for _, row := range rows { + result = append(result, NewRecordFromNullStringMap(collection, row)) + } + + return result +} + +// Returns the table name associated to the current Record model. +func (m *Record) TableName() string { + return m.collection.Name +} + +// Returns the Collection model associated to the current Record model. +func (m *Record) Collection() *Collection { + return m.collection +} + +// GetExpand returns a shallow copy of the optional `expand` data +// attached to the current Record model. +func (m *Record) GetExpand() map[string]any { + return shallowCopy(m.expand) +} + +// SetExpand assigns the provided data to `record.expand`. +func (m *Record) SetExpand(data map[string]any) { + m.expand = shallowCopy(data) +} + +// Data returns a shallow copy of the currently loaded record's data. +func (m *Record) Data() map[string]any { + return shallowCopy(m.data) +} + +// SetDataValue sets the provided key-value data pair for the current Record model. +// +// This method does nothing if the record doesn't have a `key` field. +func (m *Record) SetDataValue(key string, value any) { + if m.data == nil { + m.data = map[string]any{} + } + + field := m.Collection().Schema.GetFieldByName(key) + if field != nil { + m.data[key] = field.PrepareValue(value) + } +} + +// GetDataValue returns the current record's data value for `key`. +// +// Returns nil if data value with `key` is not found or set. +func (m *Record) GetDataValue(key string) any { + return m.data[key] +} + +// GetBoolDataValue returns the data value for `key` as a bool. +func (m *Record) GetBoolDataValue(key string) bool { + return cast.ToBool(m.GetDataValue(key)) +} + +// GetStringDataValue returns the data value for `key` as a string. +func (m *Record) GetStringDataValue(key string) string { + return cast.ToString(m.GetDataValue(key)) +} + +// GetIntDataValue returns the data value for `key` as an int. +func (m *Record) GetIntDataValue(key string) int { + return cast.ToInt(m.GetDataValue(key)) +} + +// GetFloatDataValue returns the data value for `key` as a float64. +func (m *Record) GetFloatDataValue(key string) float64 { + return cast.ToFloat64(m.GetDataValue(key)) +} + +// GetTimeDataValue returns the data value for `key` as a [time.Time] instance. +func (m *Record) GetTimeDataValue(key string) time.Time { + return cast.ToTime(m.GetDataValue(key)) +} + +// GetDateTimeDataValue returns the data value for `key` as a DateTime instance. +func (m *Record) GetDateTimeDataValue(key string) types.DateTime { + d, _ := types.ParseDateTime(m.GetDataValue(key)) + return d +} + +// GetStringSliceDataValue returns the data value for `key` as a slice of unique strings. +func (m *Record) GetStringSliceDataValue(key string) []string { + return list.ToUniqueStringSlice(m.GetDataValue(key)) +} + +// BaseFilesPath returns the storage dir path used by the record. +func (m *Record) BaseFilesPath() string { + return fmt.Sprintf("%s/%s", m.Collection().BaseFilesPath(), m.Id) +} + +// FindFileFieldByFile returns the first file type field for which +// any of the record's data contains the provided filename. +func (m *Record) FindFileFieldByFile(filename string) *schema.SchemaField { + for _, field := range m.Collection().Schema.Fields() { + if field.Type == schema.FieldTypeFile { + names := m.GetStringSliceDataValue(field.Name) + if list.ExistInSlice(filename, names) { + return field + } + } + } + return nil +} + +// Load bulk loads the provided data into the current Record model. +func (m *Record) Load(data map[string]any) error { + if data[schema.ReservedFieldNameId] != nil { + id, err := cast.ToStringE(data[schema.ReservedFieldNameId]) + if err != nil { + return err + } + m.Id = id + } + + if data[schema.ReservedFieldNameCreated] != nil { + m.Created, _ = types.ParseDateTime(data[schema.ReservedFieldNameCreated]) + } + + if data[schema.ReservedFieldNameUpdated] != nil { + m.Updated, _ = types.ParseDateTime(data[schema.ReservedFieldNameUpdated]) + } + + for k, v := range data { + m.SetDataValue(k, v) + } + + return nil +} + +// ColumnValueMap implements [ColumnValueMapper] interface. +func (m *Record) ColumnValueMap() map[string]any { + result := map[string]any{} + for key := range m.data { + result[key] = m.normalizeDataValueForDB(key) + } + + // set base model fields + result[schema.ReservedFieldNameId] = m.Id + result[schema.ReservedFieldNameCreated] = m.Created + result[schema.ReservedFieldNameUpdated] = m.Updated + + return result +} + +// PublicExport exports only the record fields that are safe to be public. +// +// This method also skips the "hidden" fields, aka. fields prefixed with `#`. +func (m *Record) PublicExport() map[string]any { + result := skipHiddenFields(m.data) + + // set base model fields + result[schema.ReservedFieldNameId] = m.Id + result[schema.ReservedFieldNameCreated] = m.Created + result[schema.ReservedFieldNameUpdated] = m.Updated + + // add helper collection fields + result["@collectionId"] = m.collection.Id + result["@collectionName"] = m.collection.Name + + // add expand (if set) + if m.expand != nil { + result["@expand"] = m.expand + } + + return result +} + +// MarshalJSON implements the [json.Marshaler] interface. +// +// Only the data exported by `PublicExport()` will be serialized. +func (m Record) MarshalJSON() ([]byte, error) { + return json.Marshal(m.PublicExport()) +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (m *Record) UnmarshalJSON(data []byte) error { + result := map[string]any{} + + if err := json.Unmarshal(data, &result); err != nil { + return err + } + + return m.Load(result) +} + +// normalizeDataValueForDB returns the `key` data value formatted for db storage. +func (m *Record) normalizeDataValueForDB(key string) any { + val := m.GetDataValue(key) + + switch ids := val.(type) { + case []string: + // encode strings slice + return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...) + case []any: + // encode interfaces slice + return append(types.JsonArray{}, ids...) + default: + // no changes + return val + } +} + +// shallowCopy shallow copy data into a new map. +func shallowCopy(data map[string]any) map[string]any { + result := map[string]any{} + + for k, v := range data { + result[k] = v + } + + return result +} + +// skipHiddenFields returns a new data map without the "#" prefixed fields. +func skipHiddenFields(data map[string]any) map[string]any { + result := map[string]any{} + + for key, val := range data { + // ignore "#" prefixed fields + if strings.HasPrefix(key, "#") { + continue + } + result[key] = val + } + + return result +} diff --git a/models/record_test.go b/models/record_test.go new file mode 100644 index 00000000..fc18e4b3 --- /dev/null +++ b/models/record_test.go @@ -0,0 +1,849 @@ +package models_test + +import ( + "database/sql" + "encoding/json" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestNewRecord(t *testing.T) { + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "test", + Type: schema.FieldTypeText, + }, + ), + } + + m := models.NewRecord(collection) + + if m.Collection().Id != collection.Id { + t.Fatalf("Expected collection with id %v, got %v", collection.Id, m.Collection().Id) + } + + if len(m.Data()) != 0 { + t.Fatalf("Expected empty data, got %v", m.Data()) + } +} + +func TestNewRecordFromNullStringMap(t *testing.T) { + collection := &models.Collection{ + Name: "test", + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field2", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field3", + Type: schema.FieldTypeBool, + }, + &schema.SchemaField{ + Name: "field4", + Type: schema.FieldTypeNumber, + }, + &schema.SchemaField{ + Name: "field5", + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + Values: []string{"test1", "test2"}, + MaxSelect: 1, + }, + }, + &schema.SchemaField{ + Name: "field6", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 2, + MaxSize: 1, + }, + }, + ), + } + + data := dbx.NullStringMap{ + "id": sql.NullString{ + String: "c23eb053-d07e-4fbe-86b3-b8ac31982e9a", + Valid: true, + }, + "created": sql.NullString{ + String: "2022-01-01 10:00:00.123", + Valid: true, + }, + "updated": sql.NullString{ + String: "2022-01-01 10:00:00.456", + Valid: true, + }, + "field1": sql.NullString{ + String: "test", + Valid: true, + }, + "field2": sql.NullString{ + String: "test", + Valid: false, // test invalid db serialization + }, + "field3": sql.NullString{ + String: "true", + Valid: true, + }, + "field4": sql.NullString{ + String: "123.123", + Valid: true, + }, + "field5": sql.NullString{ + String: `["test1","test2"]`, // will select only the first elem + Valid: true, + }, + "field6": sql.NullString{ + String: "test", // will be converted to slice + Valid: true, + }, + } + + m := models.NewRecordFromNullStringMap(collection, data) + encoded, err := m.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + expected := `{"@collectionId":"","@collectionName":"test","created":"2022-01-01 10:00:00.123","field1":"test","field2":null,"field3":true,"field4":123.123,"field5":"test1","field6":["test"],"id":"c23eb053-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"}` + + if string(encoded) != expected { + t.Fatalf("Expected %v, got \n%v", expected, string(encoded)) + } +} + +func TestNewRecordsFromNullStringMaps(t *testing.T) { + collection := &models.Collection{ + Name: "test", + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field2", + Type: schema.FieldTypeNumber, + }, + ), + } + + data := []dbx.NullStringMap{ + { + "id": sql.NullString{ + String: "11111111-d07e-4fbe-86b3-b8ac31982e9a", + Valid: true, + }, + "created": sql.NullString{ + String: "2022-01-01 10:00:00.123", + Valid: true, + }, + "updated": sql.NullString{ + String: "2022-01-01 10:00:00.456", + Valid: true, + }, + "field1": sql.NullString{ + String: "test1", + Valid: true, + }, + "field2": sql.NullString{ + String: "123", + Valid: false, // test invalid db serialization + }, + }, + { + "id": sql.NullString{ + String: "22222222-d07e-4fbe-86b3-b8ac31982e9a", + Valid: true, + }, + "field1": sql.NullString{ + String: "test2", + Valid: true, + }, + "field2": sql.NullString{ + String: "123", + Valid: true, + }, + }, + } + + result := models.NewRecordsFromNullStringMaps(collection, data) + encoded, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + + expected := `[{"@collectionId":"","@collectionName":"test","created":"2022-01-01 10:00:00.123","field1":"test1","field2":null,"id":"11111111-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"},{"@collectionId":"","@collectionName":"test","created":"","field1":"test2","field2":123,"id":"22222222-d07e-4fbe-86b3-b8ac31982e9a","updated":""}]` + + if string(encoded) != expected { + t.Fatalf("Expected %v, got \n%v", expected, string(encoded)) + } +} + +func TestRecordCollection(t *testing.T) { + collection := &models.Collection{} + collection.RefreshId() + + m := models.NewRecord(collection) + + if m.Collection().Id != collection.Id { + t.Fatalf("Expected collection with id %v, got %v", collection.Id, m.Collection().Id) + } +} + +func TestRecordTableName(t *testing.T) { + collection := &models.Collection{} + collection.Name = "test" + collection.RefreshId() + + m := models.NewRecord(collection) + + if m.TableName() != collection.Name { + t.Fatalf("Expected table %q, got %q", collection.Name, m.TableName()) + } +} + +func TestRecordExpand(t *testing.T) { + collection := &models.Collection{} + m := models.NewRecord(collection) + + data := map[string]any{"test": 123} + + m.SetExpand(data) + + // change the original data to check if it was shallow copied + data["test"] = 456 + + expand := m.GetExpand() + if v, ok := expand["test"]; !ok || v != 123 { + t.Fatalf("Expected expand.test to be %v, got %v", 123, v) + } +} + +func TestRecordLoadAndData(t *testing.T) { + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field", + Type: schema.FieldTypeText, + }, + ), + } + m := models.NewRecord(collection) + + data := map[string]any{ + "id": "11111111-d07e-4fbe-86b3-b8ac31982e9a", + "created": "2022-01-01 10:00:00.123", + "updated": "2022-01-01 10:00:00.456", + "field": "test", + "unknown": "test", + } + + m.Load(data) + + // change some of original data fields to check if they were shallow copied + data["id"] = "22222222-d07e-4fbe-86b3-b8ac31982e9a" + data["field"] = "new_test" + + expectedData := `{"field":"test"}` + encodedData, _ := json.Marshal(m.Data()) + if string(encodedData) != expectedData { + t.Fatalf("Expected data %v, got \n%v", expectedData, string(encodedData)) + } + + expectedModel := `{"@collectionId":"","@collectionName":"","created":"2022-01-01 10:00:00.123","field":"test","id":"11111111-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"}` + encodedModel, _ := json.Marshal(m) + if string(encodedModel) != expectedModel { + t.Fatalf("Expected model %v, got \n%v", expectedModel, string(encodedModel)) + } +} + +func TestRecordSetDataValue(t *testing.T) { + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field", + Type: schema.FieldTypeText, + }, + ), + } + m := models.NewRecord(collection) + + m.SetDataValue("unknown", 123) + m.SetDataValue("field", 123) // test whether PrepareValue will be called and casted to string + + data := m.Data() + if len(data) != 1 { + t.Fatalf("Expected only 1 data field to be set, got %v", data) + } + + if v, ok := data["field"]; !ok || v != "123" { + t.Fatalf("Expected field to be %v, got %v", "123", v) + } +} + +func TestRecordGetDataValue(t *testing.T) { + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeNumber, + }, + &schema.SchemaField{ + Name: "field2", + Type: schema.FieldTypeNumber, + }, + ), + } + m := models.NewRecord(collection) + + m.SetDataValue("field2", 123) + + // missing + v0 := m.GetDataValue("missing") + if v0 != nil { + t.Fatalf("Unexpected value for key 'missing'") + } + + // existing - not set + v1 := m.GetDataValue("field1") + if v1 != nil { + t.Fatalf("Unexpected value for key 'field1'") + } + + // existing - set + v2 := m.GetDataValue("field2") + if v2 != 123.0 { + t.Fatalf("Expected 123.0, got %v", v2) + } +} + +func TestRecordGetBoolDataValue(t *testing.T) { + scenarios := []struct { + value any + expected bool + }{ + {nil, false}, + {"", false}, + {0, false}, + {1, true}, + {[]string{"true"}, false}, + {time.Now(), false}, + {"test", false}, + {"false", false}, + {"true", true}, + {false, false}, + {true, true}, + } + + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{Name: "test"}, + ), + } + + for i, s := range scenarios { + m := models.NewRecord(collection) + m.SetDataValue("test", s.value) + + result := m.GetBoolDataValue("test") + if result != s.expected { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestRecordGetStringDataValue(t *testing.T) { + scenarios := []struct { + value any + expected string + }{ + {nil, ""}, + {"", ""}, + {0, "0"}, + {1.4, "1.4"}, + {[]string{"true"}, ""}, + {map[string]int{"test": 1}, ""}, + {[]byte("abc"), "abc"}, + {"test", "test"}, + {false, "false"}, + {true, "true"}, + } + + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{Name: "test"}, + ), + } + + for i, s := range scenarios { + m := models.NewRecord(collection) + m.SetDataValue("test", s.value) + + result := m.GetStringDataValue("test") + if result != s.expected { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestRecordGetIntDataValue(t *testing.T) { + scenarios := []struct { + value any + expected int + }{ + {nil, 0}, + {"", 0}, + {[]string{"true"}, 0}, + {map[string]int{"test": 1}, 0}, + {time.Now(), 0}, + {"test", 0}, + {123, 123}, + {2.4, 2}, + {"123", 123}, + {"123.5", 0}, + {false, 0}, + {true, 1}, + } + + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{Name: "test"}, + ), + } + + for i, s := range scenarios { + m := models.NewRecord(collection) + m.SetDataValue("test", s.value) + + result := m.GetIntDataValue("test") + if result != s.expected { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestRecordGetFloatDataValue(t *testing.T) { + scenarios := []struct { + value any + expected float64 + }{ + {nil, 0}, + {"", 0}, + {[]string{"true"}, 0}, + {map[string]int{"test": 1}, 0}, + {time.Now(), 0}, + {"test", 0}, + {123, 123}, + {2.4, 2.4}, + {"123", 123}, + {"123.5", 123.5}, + {false, 0}, + {true, 1}, + } + + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{Name: "test"}, + ), + } + + for i, s := range scenarios { + m := models.NewRecord(collection) + m.SetDataValue("test", s.value) + + result := m.GetFloatDataValue("test") + if result != s.expected { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestRecordGetTimeDataValue(t *testing.T) { + nowTime := time.Now() + testTime, _ := time.Parse(types.DefaultDateLayout, "2022-01-01 08:00:40.000") + + scenarios := []struct { + value any + expected time.Time + }{ + {nil, time.Time{}}, + {"", time.Time{}}, + {false, time.Time{}}, + {true, time.Time{}}, + {"test", time.Time{}}, + {[]string{"true"}, time.Time{}}, + {map[string]int{"test": 1}, time.Time{}}, + {1641024040, testTime}, + {"2022-01-01 08:00:40.000", testTime}, + {nowTime, nowTime}, + } + + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{Name: "test"}, + ), + } + + for i, s := range scenarios { + m := models.NewRecord(collection) + m.SetDataValue("test", s.value) + + result := m.GetTimeDataValue("test") + if !result.Equal(s.expected) { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestRecordGetDateTimeDataValue(t *testing.T) { + nowTime := time.Now() + testTime, _ := time.Parse(types.DefaultDateLayout, "2022-01-01 08:00:40.000") + + scenarios := []struct { + value any + expected time.Time + }{ + {nil, time.Time{}}, + {"", time.Time{}}, + {false, time.Time{}}, + {true, time.Time{}}, + {"test", time.Time{}}, + {[]string{"true"}, time.Time{}}, + {map[string]int{"test": 1}, time.Time{}}, + {1641024040, testTime}, + {"2022-01-01 08:00:40.000", testTime}, + {nowTime, nowTime}, + } + + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{Name: "test"}, + ), + } + + for i, s := range scenarios { + m := models.NewRecord(collection) + m.SetDataValue("test", s.value) + + result := m.GetDateTimeDataValue("test") + if !result.Time().Equal(s.expected) { + t.Errorf("(%d) Expected %v, got %v", i, s.expected, result) + } + } +} + +func TestRecordGetStringSliceDataValue(t *testing.T) { + nowTime := time.Now() + + scenarios := []struct { + value any + expected []string + }{ + {nil, []string{}}, + {"", []string{}}, + {false, []string{"false"}}, + {true, []string{"true"}}, + {nowTime, []string{}}, + {123, []string{"123"}}, + {"test", []string{"test"}}, + {map[string]int{"test": 1}, []string{}}, + {`["test1", "test2"]`, []string{"test1", "test2"}}, + {[]int{123, 123, 456}, []string{"123", "456"}}, + {[]string{"test", "test", "123"}, []string{"test", "123"}}, + } + + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{Name: "test"}, + ), + } + + for i, s := range scenarios { + m := models.NewRecord(collection) + m.SetDataValue("test", s.value) + + result := m.GetStringSliceDataValue("test") + + if len(result) != len(s.expected) { + t.Errorf("(%d) Expected %d elements, got %d: %v", i, len(s.expected), len(result), result) + continue + } + + for _, v := range result { + if !list.ExistInSlice(v, s.expected) { + t.Errorf("(%d) Cannot find %v in %v", i, v, s.expected) + } + } + } +} + +func TestRecordBaseFilesPath(t *testing.T) { + collection := &models.Collection{} + collection.RefreshId() + collection.Name = "test" + + m := models.NewRecord(collection) + m.RefreshId() + + expected := collection.BaseFilesPath() + "/" + m.Id + result := m.BaseFilesPath() + + if result != expected { + t.Fatalf("Expected %q, got %q", expected, result) + } +} + +func TestRecordFindFileFieldByFile(t *testing.T) { + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field2", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 1, + MaxSize: 1, + }, + }, + &schema.SchemaField{ + Name: "field3", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 2, + MaxSize: 1, + }, + }, + ), + } + + m := models.NewRecord(collection) + m.SetDataValue("field1", "test") + m.SetDataValue("field2", "test.png") + m.SetDataValue("field3", []string{"test1.png", "test2.png"}) + + scenarios := []struct { + filename string + expectField string + }{ + {"", ""}, + {"test", ""}, + {"test2", ""}, + {"test.png", "field2"}, + {"test2.png", "field3"}, + } + + for i, s := range scenarios { + result := m.FindFileFieldByFile(s.filename) + + var fieldName string + if result != nil { + fieldName = result.Name + } + + if s.expectField != fieldName { + t.Errorf("(%d) Expected field %v, got %v", i, s.expectField, result) + continue + } + } +} + +func TestRecordColumnValueMap(t *testing.T) { + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field2", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 1, + MaxSize: 1, + }, + }, + &schema.SchemaField{ + Name: "#field3", + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + MaxSelect: 2, + Values: []string{"test1", "test2", "test3"}, + }, + }, + &schema.SchemaField{ + Name: "field4", + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{ + MaxSelect: 2, + }, + }, + ), + } + + id1 := "11111111-1e32-4c94-ae06-90c25fcf6791" + id2 := "22222222-1e32-4c94-ae06-90c25fcf6791" + created, _ := types.ParseDateTime("2022-01-01 10:00:30.123") + + m := models.NewRecord(collection) + m.Id = id1 + m.Created = created + m.SetDataValue("field1", "test") + m.SetDataValue("field2", "test.png") + m.SetDataValue("#field3", []string{"test1", "test2"}) + m.SetDataValue("field4", []string{id1, id2, id1}) + + result := m.ColumnValueMap() + + encoded, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + + expected := `{"#field3":["test1","test2"],"created":"2022-01-01 10:00:30.123","field1":"test","field2":"test.png","field4":["11111111-1e32-4c94-ae06-90c25fcf6791","22222222-1e32-4c94-ae06-90c25fcf6791"],"id":"11111111-1e32-4c94-ae06-90c25fcf6791","updated":""}` + + if string(encoded) != expected { + t.Fatalf("Expected %v, got \n%v", expected, string(encoded)) + } +} + +func TestRecordPublicExport(t *testing.T) { + collection := &models.Collection{ + Name: "test", + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field2", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 1, + MaxSize: 1, + }, + }, + &schema.SchemaField{ + Name: "#field3", + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + MaxSelect: 2, + Values: []string{"test1", "test2", "test3"}, + }, + }, + ), + } + + created, _ := types.ParseDateTime("2022-01-01 10:00:30.123") + + m := models.NewRecord(collection) + m.Id = "210a896c-1e32-4c94-ae06-90c25fcf6791" + m.Created = created + m.SetDataValue("field1", "test") + m.SetDataValue("field2", "test.png") + m.SetDataValue("#field3", []string{"test1", "test2"}) + m.SetExpand(map[string]any{"test": 123}) + + result := m.PublicExport() + + encoded, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + + expected := `{"@collectionId":"","@collectionName":"test","@expand":{"test":123},"created":"2022-01-01 10:00:30.123","field1":"test","field2":"test.png","id":"210a896c-1e32-4c94-ae06-90c25fcf6791","updated":""}` + + if string(encoded) != expected { + t.Fatalf("Expected %v, got \n%v", expected, string(encoded)) + } +} + +func TestRecordMarshalJSON(t *testing.T) { + collection := &models.Collection{ + Name: "test", + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field1", + Type: schema.FieldTypeText, + }, + &schema.SchemaField{ + Name: "field2", + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{ + MaxSelect: 1, + MaxSize: 1, + }, + }, + &schema.SchemaField{ + Name: "#field3", + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + MaxSelect: 2, + Values: []string{"test1", "test2", "test3"}, + }, + }, + ), + } + + created, _ := types.ParseDateTime("2022-01-01 10:00:30.123") + + m := models.NewRecord(collection) + m.Id = "210a896c-1e32-4c94-ae06-90c25fcf6791" + m.Created = created + m.SetDataValue("field1", "test") + m.SetDataValue("field2", "test.png") + m.SetDataValue("#field3", []string{"test1", "test2"}) + m.SetExpand(map[string]any{"test": 123}) + + encoded, err := m.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + expected := `{"@collectionId":"","@collectionName":"test","@expand":{"test":123},"created":"2022-01-01 10:00:30.123","field1":"test","field2":"test.png","id":"210a896c-1e32-4c94-ae06-90c25fcf6791","updated":""}` + + if string(encoded) != expected { + t.Fatalf("Expected %v, got \n%v", expected, string(encoded)) + } +} + +func TestRecordUnmarshalJSON(t *testing.T) { + collection := &models.Collection{ + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "field", + Type: schema.FieldTypeText, + }, + ), + } + m := models.NewRecord(collection) + + m.UnmarshalJSON([]byte(`{ + "id": "11111111-d07e-4fbe-86b3-b8ac31982e9a", + "created": "2022-01-01 10:00:00.123", + "updated": "2022-01-01 10:00:00.456", + "field": "test", + "unknown": "test" + }`)) + + expected := `{"@collectionId":"","@collectionName":"","created":"2022-01-01 10:00:00.123","field":"test","id":"11111111-d07e-4fbe-86b3-b8ac31982e9a","updated":"2022-01-01 10:00:00.456"}` + encoded, _ := json.Marshal(m) + if string(encoded) != expected { + t.Fatalf("Expected model %v, got \n%v", expected, string(encoded)) + } +} diff --git a/models/request.go b/models/request.go new file mode 100644 index 00000000..5a3f91e2 --- /dev/null +++ b/models/request.go @@ -0,0 +1,29 @@ +package models + +import "github.com/pocketbase/pocketbase/tools/types" + +var _ Model = (*Request)(nil) + +// list with the supported values for `Request.Auth` +const ( + RequestAuthGuest = "guest" + RequestAuthUser = "user" + RequestAuthAdmin = "admin" +) + +type Request struct { + BaseModel + + Url string `db:"url" json:"url"` + Method string `db:"method" json:"method"` + Status int `db:"status" json:"status"` + Auth string `db:"auth" json:"auth"` + Ip string `db:"ip" json:"ip"` + Referer string `db:"referer" json:"referer"` + UserAgent string `db:"userAgent" json:"userAgent"` + Meta types.JsonMap `db:"meta" json:"meta"` +} + +func (m *Request) TableName() string { + return "_requests" +} diff --git a/models/request_test.go b/models/request_test.go new file mode 100644 index 00000000..0f1f99e5 --- /dev/null +++ b/models/request_test.go @@ -0,0 +1,14 @@ +package models_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models" +) + +func TestRequestTableName(t *testing.T) { + m := models.Request{} + if m.TableName() != "_requests" { + t.Fatalf("Unexpected table name, got %q", m.TableName()) + } +} diff --git a/models/schema/schema.go b/models/schema/schema.go new file mode 100644 index 00000000..cae30795 --- /dev/null +++ b/models/schema/schema.go @@ -0,0 +1,236 @@ +// Package schema implements custom Schema and SchemaField datatypes +// for handling the Collection schema definitions. +package schema + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "strconv" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/security" +) + +// NewSchema creates a new Schema instance with the provided fields. +func NewSchema(fields ...*SchemaField) Schema { + s := Schema{} + + for _, f := range fields { + s.AddField(f) + } + + return s +} + +// Schema defines a dynamic db schema as a slice of `SchemaField`s. +type Schema struct { + fields []*SchemaField +} + +// Fields returns the registered schema fields. +func (s *Schema) Fields() []*SchemaField { + return s.fields +} + +// InitFieldsOptions calls `InitOptions()` for all schema fields. +func (s *Schema) InitFieldsOptions() error { + for _, field := range s.Fields() { + if err := field.InitOptions(); err != nil { + return err + } + } + return nil +} + +// Clone creates a deep clone of the current schema. +func (s *Schema) Clone() (*Schema, error) { + copyRaw, err := json.Marshal(s) + if err != nil { + return nil, err + } + + result := &Schema{} + if err := json.Unmarshal(copyRaw, result); err != nil { + return nil, err + } + + return result, nil +} + +// AsMap returns a map with all registered schema field. +// The returned map is indexed with each field name. +func (s *Schema) AsMap() map[string]*SchemaField { + result := map[string]*SchemaField{} + + for _, field := range s.fields { + result[field.Name] = field + } + + return result +} + +// GetFieldById returns a single field by its id. +func (s *Schema) GetFieldById(id string) *SchemaField { + for _, field := range s.fields { + if field.Id == id { + return field + } + } + return nil +} + +// GetFieldByName returns a single field by its name. +func (s *Schema) GetFieldByName(name string) *SchemaField { + for _, field := range s.fields { + if field.Name == name { + return field + } + } + return nil +} + +// RemoveField removes a single schema field by its id. +// +// This method does nothing if field with `id` doesn't exist. +func (s *Schema) RemoveField(id string) { + for i, field := range s.fields { + if field.Id == id { + s.fields = append(s.fields[:i], s.fields[i+1:]...) + return + } + } +} + +// AddField registers the provided newField to the current schema. +// +// If field with `newField.Id` already exist, the existing field is +// replaced with the new one. +// +// Otherwise the new field is appended to the other schema fields. +func (s *Schema) AddField(newField *SchemaField) { + if newField.Id == "" { + // set default id + newField.Id = strings.ToLower(security.RandomString(8)) + } + + for i, field := range s.fields { + // replace existing + if field.Id == newField.Id { + s.fields[i] = newField + return + } + } + + // add new field + s.fields = append(s.fields, newField) +} + +// Validate makes Schema validatable by implementing [validation.Validatable] interface. +// +// Internally calls each individual field's validator and additionally +// checks for invalid renamed fields and field name duplications. +func (s Schema) Validate() error { + return validation.Validate(&s.fields, validation.Required, validation.By(func(value any) error { + fields := s.fields // use directly the schema value to avoid unnecesary interface casting + + if len(fields) == 0 { + return validation.NewError("validation_invalid_schema", "Invalid schema format.") + } + + ids := []string{} + names := []string{} + for i, field := range fields { + if list.ExistInSlice(field.Id, ids) { + return validation.Errors{ + strconv.Itoa(i): validation.Errors{ + "id": validation.NewError( + "validation_duplicated_field_id", + "Duplicated or invalid schema field id", + ), + }, + } + } + + // field names are used as db columns and should be case insensitive + nameLower := strings.ToLower(field.Name) + + if list.ExistInSlice(nameLower, names) { + return validation.Errors{ + strconv.Itoa(i): validation.Errors{ + "name": validation.NewError( + "validation_duplicated_field_name", + "Duplicated or invalid schema field name", + ), + }, + } + } + + ids = append(ids, field.Id) + names = append(names, nameLower) + } + + return nil + })) +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (s Schema) MarshalJSON() ([]byte, error) { + if s.fields == nil { + s.fields = []*SchemaField{} + } + return json.Marshal(s.fields) +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +// +// On success, all schema field options are auto initialized. +func (s *Schema) UnmarshalJSON(data []byte) error { + fields := []*SchemaField{} + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + + s.fields = []*SchemaField{} + + for _, f := range fields { + s.AddField(f) + } + + return s.InitFieldsOptions() +} + +// Value implements the [driver.Valuer] interface. +func (s Schema) Value() (driver.Value, error) { + if len(s.fields) == 0 { + return nil, nil + } + + data, err := json.Marshal(s.fields) + + return string(data), err +} + +// Scan implements [sql.Scanner] interface to scan the provided value +// into the current Schema instance. +func (s *Schema) Scan(value any) error { + var data []byte + switch v := value.(type) { + case nil: + // no cast needed + case []byte: + data = v + case string: + data = []byte(v) + default: + return fmt.Errorf("Failed to unmarshal Schema value %q.", value) + } + + if len(data) == 0 { + data = []byte("[]") + } + + return s.UnmarshalJSON(data) +} diff --git a/models/schema/schema_field.go b/models/schema/schema_field.go new file mode 100644 index 00000000..d92f9573 --- /dev/null +++ b/models/schema/schema_field.go @@ -0,0 +1,503 @@ +package schema + +import ( + "encoding/json" + "errors" + "regexp" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" + "github.com/spf13/cast" +) + +var schemaFieldNameRegex = regexp.MustCompile(`^\#?\w+$`) + +// reserved internal field names +const ( + ReservedFieldNameId = "id" + ReservedFieldNameCreated = "created" + ReservedFieldNameUpdated = "updated" +) + +// ReservedFieldNames returns slice with reserved/system field names. +func ReservedFieldNames() []string { + return []string{ + ReservedFieldNameId, + ReservedFieldNameCreated, + ReservedFieldNameUpdated, + } +} + +// All valid field types +const ( + FieldTypeText string = "text" + FieldTypeNumber string = "number" + FieldTypeBool string = "bool" + FieldTypeEmail string = "email" + FieldTypeUrl string = "url" + FieldTypeDate string = "date" + FieldTypeSelect string = "select" + FieldTypeJson string = "json" + FieldTypeFile string = "file" + FieldTypeRelation string = "relation" + FieldTypeUser string = "user" +) + +// FieldTypes returns slice with all supported field types. +func FieldTypes() []string { + return []string{ + FieldTypeText, + FieldTypeNumber, + FieldTypeBool, + FieldTypeEmail, + FieldTypeUrl, + FieldTypeDate, + FieldTypeSelect, + FieldTypeJson, + FieldTypeFile, + FieldTypeRelation, + FieldTypeUser, + } +} + +// ArraybleFieldTypes returns slice with all array value supported field types. +func ArraybleFieldTypes() []string { + return []string{ + FieldTypeSelect, + FieldTypeFile, + FieldTypeRelation, + FieldTypeUser, + } +} + +// SchemaField defines a single schema field structure. +type SchemaField struct { + System bool `form:"system" json:"system"` + Id string `form:"id" json:"id"` + Name string `form:"name" json:"name"` + Type string `form:"type" json:"type"` + Required bool `form:"required" json:"required"` + Unique bool `form:"unique" json:"unique"` + Options any `form:"options" json:"options"` +} + +// ColDefinition returns the field db column type definition as string. +func (f *SchemaField) ColDefinition() string { + switch f.Type { + case FieldTypeNumber: + return "REAL DEFAULT 0" + case FieldTypeBool: + return "Boolean DEFAULT FALSE" + case FieldTypeJson: + return "JSON DEFAULT NULL" + default: + return "TEXT DEFAULT ''" + } +} + +// String serializes and returns the current field as string. +func (f SchemaField) String() string { + data, _ := f.MarshalJSON() + return string(data) +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (f SchemaField) MarshalJSON() ([]byte, error) { + type alias SchemaField // alias to prevent recursion + + f.InitOptions() + + return json.Marshal(alias(f)) +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +// +// The schema field options are auto initialized on success. +func (f *SchemaField) UnmarshalJSON(data []byte) error { + type alias *SchemaField // alias to prevent recursion + + a := alias(f) + + if err := json.Unmarshal(data, a); err != nil { + return err + } + + return f.InitOptions() +} + +// Validate makes `SchemaField` validatable by implementing [validation.Validatable] interface. +func (f SchemaField) Validate() error { + // init field options (if not already) + f.InitOptions() + + // add commonly used filter literals to the exlude names list + excludeNames := ReservedFieldNames() + excludeNames = append(excludeNames, "null", "true", "false") + + return validation.ValidateStruct(&f, + validation.Field(&f.Options, validation.Required, validation.By(f.checkOptions)), + validation.Field(&f.Id, validation.Required, validation.Length(5, 255)), + validation.Field( + &f.Name, + validation.Required, + validation.Length(1, 255), + validation.Match(schemaFieldNameRegex), + validation.NotIn(list.ToInterfaceSlice(excludeNames)...), + ), + validation.Field(&f.Type, validation.Required, validation.In(list.ToInterfaceSlice(FieldTypes())...)), + // currently file fields cannot be unique because a proper + // hash/content check could cause performance issues + validation.Field(&f.Unique, validation.When(f.Type == FieldTypeFile, validation.Empty)), + ) +} + +func (f *SchemaField) checkOptions(value any) error { + v, ok := value.(FieldOptions) + if !ok { + return validation.NewError("validation_invalid_options", "Failed to initialize field options") + } + + return v.Validate() +} + +// InitOptions initializes the current field options based on its type. +// +// Returns error on unknown field type. +func (f *SchemaField) InitOptions() error { + if _, ok := f.Options.(FieldOptions); ok { + return nil // already inited + } + + serialized, err := json.Marshal(f.Options) + if err != nil { + return err + } + + var options any + switch f.Type { + case FieldTypeText: + options = &TextOptions{} + case FieldTypeNumber: + options = &NumberOptions{} + case FieldTypeBool: + options = &BoolOptions{} + case FieldTypeEmail: + options = &EmailOptions{} + case FieldTypeUrl: + options = &UrlOptions{} + case FieldTypeDate: + options = &DateOptions{} + case FieldTypeSelect: + options = &SelectOptions{} + case FieldTypeJson: + options = &JsonOptions{} + case FieldTypeFile: + options = &FileOptions{} + case FieldTypeRelation: + options = &RelationOptions{} + case FieldTypeUser: + options = &UserOptions{} + default: + return errors.New("Missing or unknown field field type.") + } + + if err := json.Unmarshal(serialized, options); err != nil { + return err + } + + f.Options = options + + return nil +} + +// PrepareValue returns normalized and properly formatted field value. +func (f *SchemaField) PrepareValue(value any) any { + // init field options (if not already) + f.InitOptions() + + switch f.Type { + case FieldTypeText, FieldTypeEmail, FieldTypeUrl: // string + if value == nil { + return nil + } + return cast.ToString(value) + case FieldTypeJson: // string + if value == nil { + return nil + } + val, _ := types.ParseJsonRaw(value) + return val + case FieldTypeNumber: // nil, int or float + if value == nil { + return nil + } + return cast.ToFloat64(value) + case FieldTypeBool: // bool + return cast.ToBool(value) + case FieldTypeDate: // string, DateTime or time.Time + if value == nil { + return nil + } + val, _ := types.ParseDateTime(value) + return val + case FieldTypeSelect: // nil, string or slice of strings + val := list.ToUniqueStringSlice(value) + + options, _ := f.Options.(*SelectOptions) + if options.MaxSelect <= 1 { + if len(val) > 0 { + return val[0] + } + return nil + } + + return val + case FieldTypeFile: // nil, string or slice of strings + val := list.ToUniqueStringSlice(value) + + options, _ := f.Options.(*FileOptions) + if options.MaxSelect <= 1 { + if len(val) > 0 { + return val[0] + } + return nil + } + + return val + case FieldTypeRelation: // nil, string or slice of strings + ids := list.ToUniqueStringSlice(value) + + options, _ := f.Options.(*RelationOptions) + if options.MaxSelect <= 1 { + if len(ids) > 0 { + return ids[0] + } + return nil + } + + return ids + case FieldTypeUser: // nil, string or slice of strings + ids := list.ToUniqueStringSlice(value) + + options, _ := f.Options.(*UserOptions) + if options.MaxSelect <= 1 { + if len(ids) > 0 { + return ids[0] + } + return nil + } + + return ids + default: + return value // unmodified + } +} + +// ------------------------------------------------------------------- + +// FieldOptions interfaces that defines common methods that every field options struct has. +type FieldOptions interface { + Validate() error +} + +type TextOptions struct { + Min *int `form:"min" json:"min"` + Max *int `form:"max" json:"max"` + Pattern string `form:"pattern" json:"pattern"` +} + +func (o TextOptions) Validate() error { + minVal := 0 + if o.Min != nil { + minVal = *o.Min + } + + return validation.ValidateStruct(&o, + validation.Field(&o.Min, validation.Min(0)), + validation.Field(&o.Max, validation.Min(minVal)), + validation.Field(&o.Pattern, validation.By(o.checkRegex)), + ) +} + +func (o *TextOptions) checkRegex(value any) error { + v, _ := value.(string) + if v == "" { + return nil // nothing to check + } + + if _, err := regexp.Compile(v); err != nil { + return validation.NewError("validation_invalid_regex", err.Error()) + } + + return nil +} + +// ------------------------------------------------------------------- + +type NumberOptions struct { + Min *float64 `form:"min" json:"min"` + Max *float64 `form:"max" json:"max"` +} + +func (o NumberOptions) Validate() error { + var maxRules []validation.Rule + if o.Min != nil && o.Max != nil { + maxRules = append(maxRules, validation.Min(*o.Min)) + } + + return validation.ValidateStruct(&o, + validation.Field(&o.Max, maxRules...), + ) +} + +// ------------------------------------------------------------------- + +type BoolOptions struct { +} + +func (o BoolOptions) Validate() error { + return nil +} + +// ------------------------------------------------------------------- + +type EmailOptions struct { + ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` + OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` +} + +func (o EmailOptions) Validate() error { + return validation.ValidateStruct(&o, + validation.Field( + &o.ExceptDomains, + validation.When(len(o.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + validation.Field( + &o.OnlyDomains, + validation.When(len(o.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + ) +} + +// ------------------------------------------------------------------- + +type UrlOptions struct { + ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"` + OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"` +} + +func (o UrlOptions) Validate() error { + return validation.ValidateStruct(&o, + validation.Field( + &o.ExceptDomains, + validation.When(len(o.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + validation.Field( + &o.OnlyDomains, + validation.When(len(o.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)), + ), + ) +} + +// ------------------------------------------------------------------- + +type DateOptions struct { + Min types.DateTime `form:"min" json:"min"` + Max types.DateTime `form:"max" json:"max"` +} + +func (o DateOptions) Validate() error { + return validation.ValidateStruct(&o, + validation.Field(&o.Max, validation.By(o.checkRange(o.Min, o.Max))), + ) +} + +func (o *DateOptions) checkRange(min types.DateTime, max types.DateTime) validation.RuleFunc { + return func(value any) error { + v, _ := value.(types.DateTime) + + if v.IsZero() || min.IsZero() || max.IsZero() { + return nil // nothing to check + } + + return validation.Date(types.DefaultDateLayout). + Min(min.Time()). + Max(max.Time()). + Validate(v.String()) + } +} + +// ------------------------------------------------------------------- + +type SelectOptions struct { + MaxSelect int `form:"maxSelect" json:"maxSelect"` + Values []string `form:"values" json:"values"` +} + +func (o SelectOptions) Validate() error { + return validation.ValidateStruct(&o, + validation.Field(&o.Values, validation.Required), + validation.Field( + &o.MaxSelect, + validation.Required, + validation.Min(1), + validation.Max(len(o.Values)), + ), + ) +} + +// ------------------------------------------------------------------- + +type JsonOptions struct { +} + +func (o JsonOptions) Validate() error { + return nil +} + +// ------------------------------------------------------------------- + +type FileOptions struct { + MaxSelect int `form:"maxSelect" json:"maxSelect"` + MaxSize int `form:"maxSize" json:"maxSize"` // in bytes + MimeTypes []string `form:"mimeTypes" json:"mimeTypes"` + Thumbs []string `form:"thumbs" json:"thumbs"` +} + +func (o FileOptions) Validate() error { + return validation.ValidateStruct(&o, + validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)), + validation.Field(&o.MaxSize, validation.Required, validation.Min(1)), + validation.Field(&o.Thumbs, validation.Each(validation.Match(regexp.MustCompile(`^[1-9]\d*x[1-9]\d*$`)))), + ) +} + +// ------------------------------------------------------------------- + +type RelationOptions struct { + MaxSelect int `form:"maxSelect" json:"maxSelect"` + CollectionId string `form:"collectionId" json:"collectionId"` + CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"` +} + +func (o RelationOptions) Validate() error { + return validation.ValidateStruct(&o, + validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)), + validation.Field(&o.CollectionId, validation.Required), + ) +} + +// ------------------------------------------------------------------- + +type UserOptions struct { + MaxSelect int `form:"maxSelect" json:"maxSelect"` + CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"` +} + +func (o UserOptions) Validate() error { + return validation.ValidateStruct(&o, + validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)), + ) +} diff --git a/models/schema/schema_field_test.go b/models/schema/schema_field_test.go new file mode 100644 index 00000000..c381aa88 --- /dev/null +++ b/models/schema/schema_field_test.go @@ -0,0 +1,1344 @@ +package schema_test + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestReservedFieldNames(t *testing.T) { + result := schema.ReservedFieldNames() + + if len(result) != 3 { + t.Fatalf("Expected %d names, got %d (%v)", 3, len(result), result) + } +} + +func TestFieldTypes(t *testing.T) { + result := schema.FieldTypes() + + if len(result) != 11 { + t.Fatalf("Expected %d types, got %d (%v)", 3, len(result), result) + } +} + +func TestArraybleFieldTypes(t *testing.T) { + result := schema.ArraybleFieldTypes() + + if len(result) != 4 { + t.Fatalf("Expected %d types, got %d (%v)", 3, len(result), result) + } +} + +func TestSchemaFieldColDefinition(t *testing.T) { + scenarios := []struct { + field schema.SchemaField + expected string + }{ + { + schema.SchemaField{Type: schema.FieldTypeText, Name: "test"}, + "TEXT DEFAULT ''", + }, + { + schema.SchemaField{Type: schema.FieldTypeNumber, Name: "test"}, + "REAL DEFAULT 0", + }, + { + schema.SchemaField{Type: schema.FieldTypeBool, Name: "test"}, + "Boolean DEFAULT FALSE", + }, + { + schema.SchemaField{Type: schema.FieldTypeEmail, Name: "test"}, + "TEXT DEFAULT ''", + }, + { + schema.SchemaField{Type: schema.FieldTypeUrl, Name: "test"}, + "TEXT DEFAULT ''", + }, + { + schema.SchemaField{Type: schema.FieldTypeDate, Name: "test"}, + "TEXT DEFAULT ''", + }, + { + schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test"}, + "TEXT DEFAULT ''", + }, + { + schema.SchemaField{Type: schema.FieldTypeJson, Name: "test"}, + "JSON DEFAULT NULL", + }, + { + schema.SchemaField{Type: schema.FieldTypeFile, Name: "test"}, + "TEXT DEFAULT ''", + }, + { + schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test"}, + "TEXT DEFAULT ''", + }, + { + schema.SchemaField{Type: schema.FieldTypeUser, Name: "test"}, + "TEXT DEFAULT ''", + }, + } + + for i, s := range scenarios { + def := s.field.ColDefinition() + if def != s.expected { + t.Errorf("(%d) Expected definition %q, got %q", i, s.expected, def) + } + } +} + +func TestSchemaFieldString(t *testing.T) { + f := schema.SchemaField{ + Id: "abc", + Name: "test", + Type: schema.FieldTypeText, + Required: true, + Unique: false, + System: true, + Options: &schema.TextOptions{ + Pattern: "test", + }, + } + + result := f.String() + expected := `{"system":true,"id":"abc","name":"test","type":"text","required":true,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}` + + if result != expected { + t.Errorf("Expected \n%v, got \n%v", expected, result) + } +} + +func TestSchemaFieldMarshalJSON(t *testing.T) { + scenarios := []struct { + field schema.SchemaField + expected string + }{ + // empty + { + schema.SchemaField{}, + `{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`, + }, + // without defined options + { + schema.SchemaField{ + Id: "abc", + Name: "test", + Type: schema.FieldTypeText, + Required: true, + Unique: false, + System: true, + }, + `{"system":true,"id":"abc","name":"test","type":"text","required":true,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`, + }, + // with defined options + { + schema.SchemaField{ + Name: "test", + Type: schema.FieldTypeText, + Required: true, + Unique: false, + System: true, + Options: &schema.TextOptions{ + Pattern: "test", + }, + }, + `{"system":true,"id":"","name":"test","type":"text","required":true,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`, + }, + } + + for i, s := range scenarios { + result, err := s.field.MarshalJSON() + if err != nil { + t.Fatalf("(%d) %v", i, err) + } + + if string(result) != s.expected { + t.Errorf("(%d), Expected \n%v, got \n%v", i, s.expected, string(result)) + } + } +} + +func TestSchemaFieldUnmarshalJSON(t *testing.T) { + scenarios := []struct { + data []byte + expectError bool + expectJson string + }{ + { + nil, + true, + `{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`, + }, + { + []byte{}, + true, + `{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`, + }, + { + []byte(`{"system": true}`), + true, + `{"system":true,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`, + }, + { + []byte(`{"invalid"`), + true, + `{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`, + }, + { + []byte(`{"type":"text","system":true}`), + false, + `{"system":true,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`, + }, + { + []byte(`{"type":"text","options":{"pattern":"test"}}`), + false, + `{"system":false,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`, + }, + } + + for i, s := range scenarios { + f := schema.SchemaField{} + err := f.UnmarshalJSON(s.data) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + if f.String() != s.expectJson { + t.Errorf("(%d), Expected json \n%v, got \n%v", i, s.expectJson, f.String()) + } + } +} + +func TestSchemaFieldValidate(t *testing.T) { + scenarios := []struct { + name string + field schema.SchemaField + expectedErrors []string + }{ + { + "empty field", + schema.SchemaField{}, + []string{"id", "options", "name", "type"}, + }, + { + "missing id", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "", + Name: "test", + }, + []string{"id"}, + }, + { + "invalid id length check", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234", + Name: "test", + }, + []string{"id"}, + }, + { + "valid id length check", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "12345", + Name: "test", + }, + []string{}, + }, + { + "invalid name format", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: "test!@#", + }, + []string{"name"}, + }, + { + "reserved name (null)", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: "null", + }, + []string{"name"}, + }, + { + "reserved name (true)", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: "null", + }, + []string{"name"}, + }, + { + "reserved name (false)", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: "false", + }, + []string{"name"}, + }, + { + "reserved name (id)", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: schema.ReservedFieldNameId, + }, + []string{"name"}, + }, + { + "reserved name (created)", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: schema.ReservedFieldNameCreated, + }, + []string{"name"}, + }, + { + "reserved name (updated)", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: schema.ReservedFieldNameUpdated, + }, + []string{"name"}, + }, + { + "valid name", + schema.SchemaField{ + Type: schema.FieldTypeText, + Id: "1234567890", + Name: "test", + }, + []string{}, + }, + { + "unique check for type file", + schema.SchemaField{ + Type: schema.FieldTypeFile, + Id: "1234567890", + Name: "test", + Unique: true, + Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}, + }, + []string{"unique"}, + }, + { + "trigger options validator (auto init)", + schema.SchemaField{ + Type: schema.FieldTypeFile, + Id: "1234567890", + Name: "test", + }, + []string{"options"}, + }, + { + "trigger options validator (invalid option field value)", + schema.SchemaField{ + Type: schema.FieldTypeFile, + Id: "1234567890", + Name: "test", + Options: &schema.FileOptions{MaxSelect: 0, MaxSize: 0}, + }, + []string{"options"}, + }, + { + "trigger options validator (valid option field value)", + schema.SchemaField{ + Type: schema.FieldTypeFile, + Id: "1234567890", + Name: "test", + Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}, + }, + []string{}, + }, + } + + for _, s := range scenarios { + result := s.field.Validate() + + // parse errors + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("[%s] Failed to parse errors %v", s.name, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs) + } + } + } +} + +func TestSchemaFieldInitOptions(t *testing.T) { + scenarios := []struct { + field schema.SchemaField + expectError bool + expectJson string + }{ + { + schema.SchemaField{}, + true, + `{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`, + }, + { + schema.SchemaField{Type: "unknown"}, + true, + `{"system":false,"id":"","name":"","type":"unknown","required":false,"unique":false,"options":null}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeText}, + false, + `{"system":false,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeNumber}, + false, + `{"system":false,"id":"","name":"","type":"number","required":false,"unique":false,"options":{"min":null,"max":null}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeBool}, + false, + `{"system":false,"id":"","name":"","type":"bool","required":false,"unique":false,"options":{}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeEmail}, + false, + `{"system":false,"id":"","name":"","type":"email","required":false,"unique":false,"options":{"exceptDomains":null,"onlyDomains":null}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeUrl}, + false, + `{"system":false,"id":"","name":"","type":"url","required":false,"unique":false,"options":{"exceptDomains":null,"onlyDomains":null}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeDate}, + false, + `{"system":false,"id":"","name":"","type":"date","required":false,"unique":false,"options":{"min":"","max":""}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeSelect}, + false, + `{"system":false,"id":"","name":"","type":"select","required":false,"unique":false,"options":{"maxSelect":0,"values":null}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeJson}, + false, + `{"system":false,"id":"","name":"","type":"json","required":false,"unique":false,"options":{}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeFile}, + false, + `{"system":false,"id":"","name":"","type":"file","required":false,"unique":false,"options":{"maxSelect":0,"maxSize":0,"mimeTypes":null,"thumbs":null}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeRelation}, + false, + `{"system":false,"id":"","name":"","type":"relation","required":false,"unique":false,"options":{"maxSelect":0,"collectionId":"","cascadeDelete":false}}`, + }, + { + schema.SchemaField{Type: schema.FieldTypeUser}, + false, + `{"system":false,"id":"","name":"","type":"user","required":false,"unique":false,"options":{"maxSelect":0,"cascadeDelete":false}}`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeText, + Options: &schema.TextOptions{Pattern: "test"}, + }, + false, + `{"system":false,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`, + }, + } + + for i, s := range scenarios { + err := s.field.InitOptions() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err) + } + + if s.field.String() != s.expectJson { + t.Errorf("(%d), Expected %v, got %v", i, s.expectJson, s.field.String()) + } + } +} + +func TestSchemaFieldPrepareValue(t *testing.T) { + scenarios := []struct { + field schema.SchemaField + value any + expectJson string + }{ + {schema.SchemaField{Type: "unkown"}, "test", `"test"`}, + {schema.SchemaField{Type: "unkown"}, 123, "123"}, + {schema.SchemaField{Type: "unkown"}, []int{1, 2, 1}, "[1,2,1]"}, + + // text + {schema.SchemaField{Type: schema.FieldTypeText}, nil, `null`}, + {schema.SchemaField{Type: schema.FieldTypeText}, []int{1, 2}, `""`}, + {schema.SchemaField{Type: schema.FieldTypeText}, "test", `"test"`}, + {schema.SchemaField{Type: schema.FieldTypeText}, 123, `"123"`}, + + // email + {schema.SchemaField{Type: schema.FieldTypeEmail}, nil, `null`}, + {schema.SchemaField{Type: schema.FieldTypeEmail}, []int{1, 2}, `""`}, + {schema.SchemaField{Type: schema.FieldTypeEmail}, "test", `"test"`}, + {schema.SchemaField{Type: schema.FieldTypeEmail}, 123, `"123"`}, + + // url + {schema.SchemaField{Type: schema.FieldTypeUrl}, nil, `null`}, + {schema.SchemaField{Type: schema.FieldTypeUrl}, []int{1, 2}, `""`}, + {schema.SchemaField{Type: schema.FieldTypeUrl}, "test", `"test"`}, + {schema.SchemaField{Type: schema.FieldTypeUrl}, 123, `"123"`}, + + // json + {schema.SchemaField{Type: schema.FieldTypeJson}, nil, "null"}, + {schema.SchemaField{Type: schema.FieldTypeJson}, 123, "123"}, + {schema.SchemaField{Type: schema.FieldTypeJson}, `"test"`, `"test"`}, + {schema.SchemaField{Type: schema.FieldTypeJson}, map[string]int{"test": 123}, `{"test":123}`}, + {schema.SchemaField{Type: schema.FieldTypeJson}, []int{1, 2, 1}, `[1,2,1]`}, + + // number + {schema.SchemaField{Type: schema.FieldTypeNumber}, nil, "null"}, + {schema.SchemaField{Type: schema.FieldTypeNumber}, 1, "1"}, + {schema.SchemaField{Type: schema.FieldTypeNumber}, 1.5, "1.5"}, + {schema.SchemaField{Type: schema.FieldTypeNumber}, "1.5", "1.5"}, + + // bool + {schema.SchemaField{Type: schema.FieldTypeBool}, nil, "false"}, + {schema.SchemaField{Type: schema.FieldTypeBool}, 1, "true"}, + {schema.SchemaField{Type: schema.FieldTypeBool}, 0, "false"}, + {schema.SchemaField{Type: schema.FieldTypeBool}, "", "false"}, + {schema.SchemaField{Type: schema.FieldTypeBool}, "false", "false"}, + {schema.SchemaField{Type: schema.FieldTypeBool}, "true", "true"}, + {schema.SchemaField{Type: schema.FieldTypeBool}, false, "false"}, + {schema.SchemaField{Type: schema.FieldTypeBool}, true, "true"}, + + // date + {schema.SchemaField{Type: schema.FieldTypeDate}, nil, "null"}, + {schema.SchemaField{Type: schema.FieldTypeDate}, "", `""`}, + {schema.SchemaField{Type: schema.FieldTypeDate}, 1641024040, `"2022-01-01 08:00:40.000"`}, + {schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123", `"2022-01-01 11:27:10.123"`}, + {schema.SchemaField{Type: schema.FieldTypeDate}, types.DateTime{}, `""`}, + {schema.SchemaField{Type: schema.FieldTypeDate}, time.Time{}, `""`}, + + // select (single) + {schema.SchemaField{Type: schema.FieldTypeSelect}, nil, `null`}, + {schema.SchemaField{Type: schema.FieldTypeSelect}, "", `null`}, + {schema.SchemaField{Type: schema.FieldTypeSelect}, 123, `"123"`}, + {schema.SchemaField{Type: schema.FieldTypeSelect}, "test", `"test"`}, + {schema.SchemaField{Type: schema.FieldTypeSelect}, []string{"test1", "test2"}, `"test1"`}, + { + // no values validation/filtering + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{ + Values: []string{"test1", "test2"}, + }, + }, + "test", + `"test"`, + }, + // select (multiple) + { + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{MaxSelect: 2}, + }, + nil, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{MaxSelect: 2}, + }, + "", + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{MaxSelect: 2}, + }, + []string{}, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{MaxSelect: 2}, + }, + 123, + `["123"]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{MaxSelect: 2}, + }, + "test", + `["test"]`, + }, + { + // no values validation + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{MaxSelect: 2}, + }, + []string{"test1", "test2", "test3"}, + `["test1","test2","test3"]`, + }, + { + // duplicated values + schema.SchemaField{ + Type: schema.FieldTypeSelect, + Options: &schema.SelectOptions{MaxSelect: 2}, + }, + []string{"test1", "test2", "test1"}, + `["test1","test2"]`, + }, + + // file (single) + {schema.SchemaField{Type: schema.FieldTypeFile}, nil, `null`}, + {schema.SchemaField{Type: schema.FieldTypeFile}, "", `null`}, + {schema.SchemaField{Type: schema.FieldTypeFile}, 123, `"123"`}, + {schema.SchemaField{Type: schema.FieldTypeFile}, "test", `"test"`}, + {schema.SchemaField{Type: schema.FieldTypeFile}, []string{"test1", "test2"}, `"test1"`}, + // file (multiple) + { + schema.SchemaField{ + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{MaxSelect: 2}, + }, + nil, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{MaxSelect: 2}, + }, + "", + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{MaxSelect: 2}, + }, + []string{}, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{MaxSelect: 2}, + }, + 123, + `["123"]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{MaxSelect: 2}, + }, + "test", + `["test"]`, + }, + { + // no values validation + schema.SchemaField{ + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{MaxSelect: 2}, + }, + []string{"test1", "test2", "test3"}, + `["test1","test2","test3"]`, + }, + { + // duplicated values + schema.SchemaField{ + Type: schema.FieldTypeFile, + Options: &schema.FileOptions{MaxSelect: 2}, + }, + []string{"test1", "test2", "test1"}, + `["test1","test2"]`, + }, + + // relation (single) + {schema.SchemaField{Type: schema.FieldTypeRelation}, nil, `null`}, + {schema.SchemaField{Type: schema.FieldTypeRelation}, "", `null`}, + {schema.SchemaField{Type: schema.FieldTypeRelation}, 123, `"123"`}, + {schema.SchemaField{Type: schema.FieldTypeRelation}, "abc", `"abc"`}, + {schema.SchemaField{Type: schema.FieldTypeRelation}, "1ba88b4f-e9da-42f0-9764-9a55c953e724", `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`}, + { + schema.SchemaField{Type: schema.FieldTypeRelation}, + []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"}, + `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`, + }, + // relation (multiple) + { + schema.SchemaField{ + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{MaxSelect: 2}, + }, + nil, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{MaxSelect: 2}, + }, + "", + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{MaxSelect: 2}, + }, + []string{}, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{MaxSelect: 2}, + }, + 123, + `["123"]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{MaxSelect: 2}, + }, + []string{"", "abc"}, + `["abc"]`, + }, + { + // no values validation + schema.SchemaField{ + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{MaxSelect: 2}, + }, + []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"}, + `["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`, + }, + { + // duplicated values + schema.SchemaField{ + Type: schema.FieldTypeRelation, + Options: &schema.RelationOptions{MaxSelect: 2}, + }, + []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724", "1ba88b4f-e9da-42f0-9764-9a55c953e724"}, + `["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`, + }, + + // user (single) + {schema.SchemaField{Type: schema.FieldTypeUser}, nil, `null`}, + {schema.SchemaField{Type: schema.FieldTypeUser}, "", `null`}, + {schema.SchemaField{Type: schema.FieldTypeUser}, 123, `"123"`}, + {schema.SchemaField{Type: schema.FieldTypeUser}, "1ba88b4f-e9da-42f0-9764-9a55c953e724", `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`}, + { + schema.SchemaField{Type: schema.FieldTypeUser}, + []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"}, + `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`, + }, + // user (multiple) + { + schema.SchemaField{ + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{MaxSelect: 2}, + }, + nil, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{MaxSelect: 2}, + }, + "", + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{MaxSelect: 2}, + }, + []string{}, + `[]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{MaxSelect: 2}, + }, + 123, + `["123"]`, + }, + { + schema.SchemaField{ + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{MaxSelect: 2}, + }, + []string{"", "abc"}, + `["abc"]`, + }, + { + // no values validation + schema.SchemaField{ + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{MaxSelect: 2}, + }, + []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"}, + `["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`, + }, + { + // duplicated values + schema.SchemaField{ + Type: schema.FieldTypeUser, + Options: &schema.UserOptions{MaxSelect: 2}, + }, + []string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724", "1ba88b4f-e9da-42f0-9764-9a55c953e724"}, + `["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`, + }, + } + + for i, s := range scenarios { + result := s.field.PrepareValue(s.value) + + encoded, err := json.Marshal(result) + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + + if string(encoded) != s.expectJson { + t.Errorf("(%d), Expected %v, got %v", i, s.expectJson, string(encoded)) + } + } +} + +// ------------------------------------------------------------------- + +type fieldOptionsScenario struct { + name string + options schema.FieldOptions + expectedErrors []string +} + +func checkFieldOptionsScenarios(t *testing.T, scenarios []fieldOptionsScenario) { + for i, s := range scenarios { + result := s.options.Validate() + + prefix := fmt.Sprintf("%d", i) + if s.name != "" { + prefix = s.name + } + + // parse errors + errs, ok := result.(validation.Errors) + if !ok && result != nil { + t.Errorf("[%s] Failed to parse errors %v", prefix, result) + continue + } + + // check errors + if len(errs) > len(s.expectedErrors) { + t.Errorf("[%s] Expected error keys %v, got %v", prefix, s.expectedErrors, errs) + } + for _, k := range s.expectedErrors { + if _, ok := errs[k]; !ok { + t.Errorf("[%s] Missing expected error key %q in %v", prefix, k, errs) + } + } + } +} + +func TestTextOptionsValidate(t *testing.T) { + minus := -1 + number0 := 0 + number1 := 10 + number2 := 20 + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.TextOptions{}, + []string{}, + }, + { + "min - failure", + schema.TextOptions{ + Min: &minus, + }, + []string{"min"}, + }, + { + "min - success", + schema.TextOptions{ + Min: &number0, + }, + []string{}, + }, + { + "max - failure without min", + schema.TextOptions{ + Max: &minus, + }, + []string{"max"}, + }, + { + "max - failure with min", + schema.TextOptions{ + Min: &number2, + Max: &number1, + }, + []string{"max"}, + }, + { + "max - success", + schema.TextOptions{ + Min: &number1, + Max: &number2, + }, + []string{}, + }, + { + "pattern - failure", + schema.TextOptions{Pattern: "(test"}, + []string{"pattern"}, + }, + { + "pattern - success", + schema.TextOptions{Pattern: `^\#?\w+$`}, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestNumberOptionsValidate(t *testing.T) { + number1 := 10.0 + number2 := 20.0 + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.NumberOptions{}, + []string{}, + }, + { + "max - without min", + schema.NumberOptions{ + Max: &number1, + }, + []string{}, + }, + { + "max - failure with min", + schema.NumberOptions{ + Min: &number2, + Max: &number1, + }, + []string{"max"}, + }, + { + "max - success with min", + schema.NumberOptions{ + Min: &number1, + Max: &number2, + }, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestBoolOptionsValidate(t *testing.T) { + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.BoolOptions{}, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestEmailOptionsValidate(t *testing.T) { + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.EmailOptions{}, + []string{}, + }, + { + "ExceptDomains failure", + schema.EmailOptions{ + ExceptDomains: []string{"invalid"}, + }, + []string{"exceptDomains"}, + }, + { + "ExceptDomains success", + schema.EmailOptions{ + ExceptDomains: []string{"example.com", "sub.example.com"}, + }, + []string{}, + }, + { + "OnlyDomains check", + schema.EmailOptions{ + OnlyDomains: []string{"invalid"}, + }, + []string{"onlyDomains"}, + }, + { + "OnlyDomains success", + schema.EmailOptions{ + OnlyDomains: []string{"example.com", "sub.example.com"}, + }, + []string{}, + }, + { + "OnlyDomains + ExceptDomains at the same time", + schema.EmailOptions{ + ExceptDomains: []string{"test1.com"}, + OnlyDomains: []string{"test2.com"}, + }, + []string{"exceptDomains", "onlyDomains"}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestUrlOptionsValidate(t *testing.T) { + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.UrlOptions{}, + []string{}, + }, + { + "ExceptDomains failure", + schema.UrlOptions{ + ExceptDomains: []string{"invalid"}, + }, + []string{"exceptDomains"}, + }, + { + "ExceptDomains success", + schema.UrlOptions{ + ExceptDomains: []string{"example.com", "sub.example.com"}, + }, + []string{}, + }, + { + "OnlyDomains check", + schema.UrlOptions{ + OnlyDomains: []string{"invalid"}, + }, + []string{"onlyDomains"}, + }, + { + "OnlyDomains success", + schema.UrlOptions{ + OnlyDomains: []string{"example.com", "sub.example.com"}, + }, + []string{}, + }, + { + "OnlyDomains + ExceptDomains at the same time", + schema.UrlOptions{ + ExceptDomains: []string{"test1.com"}, + OnlyDomains: []string{"test2.com"}, + }, + []string{"exceptDomains", "onlyDomains"}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestDateOptionsValidate(t *testing.T) { + date1 := types.NowDateTime() + date2, _ := types.ParseDateTime(date1.Time().AddDate(1, 0, 0)) + + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.DateOptions{}, + []string{}, + }, + { + "min only", + schema.DateOptions{ + Min: date1, + }, + []string{}, + }, + { + "max only", + schema.DateOptions{ + Min: date1, + }, + []string{}, + }, + { + "zero min + max", + schema.DateOptions{ + Min: types.DateTime{}, + Max: date1, + }, + []string{}, + }, + { + "min + zero max", + schema.DateOptions{ + Min: date1, + Max: types.DateTime{}, + }, + []string{}, + }, + { + "min > max", + schema.DateOptions{ + Min: date2, + Max: date1, + }, + []string{"max"}, + }, + { + "min == max", + schema.DateOptions{ + Min: date1, + Max: date1, + }, + []string{"max"}, + }, + { + "min < max", + schema.DateOptions{ + Min: date1, + Max: date2, + }, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestSelectOptionsValidate(t *testing.T) { + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.SelectOptions{}, + []string{"values", "maxSelect"}, + }, + { + "MaxSelect <= 0", + schema.SelectOptions{ + Values: []string{"test1", "test2"}, + MaxSelect: 0, + }, + []string{"maxSelect"}, + }, + { + "MaxSelect > Values", + schema.SelectOptions{ + Values: []string{"test1", "test2"}, + MaxSelect: 3, + }, + []string{"maxSelect"}, + }, + { + "MaxSelect <= Values", + schema.SelectOptions{ + Values: []string{"test1", "test2"}, + MaxSelect: 2, + }, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestJsonOptionsValidate(t *testing.T) { + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.JsonOptions{}, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestFileOptionsValidate(t *testing.T) { + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.FileOptions{}, + []string{"maxSelect", "maxSize"}, + }, + { + "MaxSelect <= 0 && maxSize <= 0", + schema.FileOptions{ + MaxSize: 0, + MaxSelect: 0, + }, + []string{"maxSelect", "maxSize"}, + }, + { + "MaxSelect > 0 && maxSize > 0", + schema.FileOptions{ + MaxSize: 2, + MaxSelect: 1, + }, + []string{}, + }, + { + "invalid thumbs format", + schema.FileOptions{ + MaxSize: 1, + MaxSelect: 2, + Thumbs: []string{"100", "200x100"}, + }, + []string{"thumbs"}, + }, + { + "invalid thumbs format - zero width", + schema.FileOptions{ + MaxSize: 1, + MaxSelect: 2, + Thumbs: []string{"0x100"}, + }, + []string{"thumbs"}, + }, + { + "invalid thumbs format - zero height", + schema.FileOptions{ + MaxSize: 1, + MaxSelect: 2, + Thumbs: []string{"100x0"}, + }, + []string{"thumbs"}, + }, + { + "invalid thumbs format - zero with and height", + schema.FileOptions{ + MaxSize: 1, + MaxSelect: 2, + Thumbs: []string{"0x0"}, + }, + []string{"thumbs"}, + }, + { + "valid thumbs format", + schema.FileOptions{ + MaxSize: 1, + MaxSelect: 2, + Thumbs: []string{"100x100", "200x100", "1x1"}, + }, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestRelationOptionsValidate(t *testing.T) { + + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.RelationOptions{}, + []string{"maxSelect", "collectionId"}, + }, + { + "empty CollectionId", + schema.RelationOptions{ + CollectionId: "", + MaxSelect: 1, + }, + []string{"collectionId"}, + }, + { + "MaxSelect <= 0", + schema.RelationOptions{ + CollectionId: "abc", + MaxSelect: 0, + }, + []string{"maxSelect"}, + }, + { + "MaxSelect > 0 && non-empty CollectionId", + schema.RelationOptions{ + CollectionId: "abc", + MaxSelect: 1, + }, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} + +func TestUserOptionsValidate(t *testing.T) { + scenarios := []fieldOptionsScenario{ + { + "empty", + schema.UserOptions{}, + []string{"maxSelect"}, + }, + { + "MaxSelect <= 0", + schema.UserOptions{ + MaxSelect: 0, + }, + []string{"maxSelect"}, + }, + { + "MaxSelect > 0", + schema.UserOptions{ + MaxSelect: 1, + }, + []string{}, + }, + } + + checkFieldOptionsScenarios(t, scenarios) +} diff --git a/models/schema/schema_test.go b/models/schema/schema_test.go new file mode 100644 index 00000000..4e2fe8c2 --- /dev/null +++ b/models/schema/schema_test.go @@ -0,0 +1,414 @@ +package schema_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/models/schema" +) + +func TestNewSchemaAndFields(t *testing.T) { + testSchema := schema.NewSchema( + &schema.SchemaField{Id: "id1", Name: "test1"}, + &schema.SchemaField{Name: "test2"}, + &schema.SchemaField{Id: "id1", Name: "test1_new"}, // should replace the original id1 field + ) + + fields := testSchema.Fields() + + if len(fields) != 2 { + t.Fatalf("Expected 2 fields, got %d (%v)", len(fields), fields) + } + + for _, f := range fields { + if f.Id == "" { + t.Fatalf("Expected field id to be set, found empty id for field %v", f) + } + } + + if fields[0].Name != "test1_new" { + t.Fatalf("Expected field with name test1_new, got %s", fields[0].Name) + } + + if fields[1].Name != "test2" { + t.Fatalf("Expected field with name test2, got %s", fields[1].Name) + } +} + +func TestSchemaInitFieldsOptions(t *testing.T) { + f0 := &schema.SchemaField{Name: "test1", Type: "unknown"} + schema0 := schema.NewSchema(f0) + + err0 := schema0.InitFieldsOptions() + if err0 == nil { + t.Fatalf("Expected unknown field schema to fail, got nil") + } + + // --- + + f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail} + schema1 := schema.NewSchema(f1, f2) + + err1 := schema1.InitFieldsOptions() + if err1 != nil { + t.Fatal(err1) + } + + if _, ok := f1.Options.(*schema.TextOptions); !ok { + t.Fatalf("Failed to init f1 options") + } + + if _, ok := f2.Options.(*schema.EmailOptions); !ok { + t.Fatalf("Failed to init f2 options") + } +} + +func TestSchemaClone(t *testing.T) { + f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail} + s1 := schema.NewSchema(f1, f2) + + s2, err := s1.Clone() + if err != nil { + t.Fatal(err) + } + + s1Encoded, _ := s1.MarshalJSON() + s2Encoded, _ := s2.MarshalJSON() + + if string(s1Encoded) != string(s2Encoded) { + t.Fatalf("Expected the cloned schema to be equal, got %v VS\n %v", s1, s2) + } + + // change in one schema shouldn't result to change in the other + // (aka. check if it is a deep clone) + s1.Fields()[0].Name = "test1_update" + if s2.Fields()[0].Name != "test1" { + t.Fatalf("Expected s2 field name to not change, got %q", s2.Fields()[0].Name) + } +} + +func TestSchemaAsMap(t *testing.T) { + f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail} + testSchema := schema.NewSchema(f1, f2) + + result := testSchema.AsMap() + + if len(result) != 2 { + t.Fatalf("Expected 2 map elements, got %d (%v)", len(result), result) + } + + expectedIndexes := []string{f1.Name, f2.Name} + + for _, index := range expectedIndexes { + if _, ok := result[index]; !ok { + t.Fatalf("Missing index %q", index) + } + } +} + +func TestSchemaGetFieldByName(t *testing.T) { + f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeText} + testSchema := schema.NewSchema(f1, f2) + + // missing field + result1 := testSchema.GetFieldByName("missing") + if result1 != nil { + t.Fatalf("Found unexpected field %v", result1) + } + + // existing field + result2 := testSchema.GetFieldByName("test1") + if result2 == nil || result2.Name != "test1" { + t.Fatalf("Cannot find field with Name 'test1', got %v ", result2) + } +} + +func TestSchemaGetFieldById(t *testing.T) { + f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText} + testSchema := schema.NewSchema(f1, f2) + + // missing field id + result1 := testSchema.GetFieldById("test1") + if result1 != nil { + t.Fatalf("Found unexpected field %v", result1) + } + + // existing field id + result2 := testSchema.GetFieldById("id2") + if result2 == nil || result2.Id != "id2" { + t.Fatalf("Cannot find field with id 'id2', got %v ", result2) + } +} + +func TestSchemaRemoveField(t *testing.T) { + f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText} + f3 := &schema.SchemaField{Id: "id3", Name: "test3", Type: schema.FieldTypeText} + testSchema := schema.NewSchema(f1, f2, f3) + + testSchema.RemoveField("id2") + testSchema.RemoveField("test3") // should do nothing + + expected := []string{"test1", "test3"} + + if len(testSchema.Fields()) != len(expected) { + t.Fatalf("Expected %d, got %d (%v)", len(expected), len(testSchema.Fields()), testSchema) + } + + for _, name := range expected { + if f := testSchema.GetFieldByName(name); f == nil { + t.Fatalf("Missing field %q", name) + } + } +} + +func TestSchemaAddField(t *testing.T) { + f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{Id: "f2Id", Name: "test2", Type: schema.FieldTypeText} + f3 := &schema.SchemaField{Id: "f3Id", Name: "test3", Type: schema.FieldTypeText} + testSchema := schema.NewSchema(f1, f2, f3) + + f2New := &schema.SchemaField{Id: "f2Id", Name: "test2_new", Type: schema.FieldTypeEmail} + f4 := &schema.SchemaField{Name: "test4", Type: schema.FieldTypeUrl} + + testSchema.AddField(f2New) + testSchema.AddField(f4) + + if len(testSchema.Fields()) != 4 { + t.Fatalf("Expected %d, got %d (%v)", 4, len(testSchema.Fields()), testSchema) + } + + // check if each field has id + for _, f := range testSchema.Fields() { + if f.Id == "" { + t.Fatalf("Expected field id to be set, found empty id for field %v", f) + } + } + + // check if f2 field was replaced + if f := testSchema.GetFieldById("f2Id"); f == nil || f.Type != schema.FieldTypeEmail { + t.Fatalf("Expected f2 field to be replaced, found %v", f) + } + + // check if f4 was added + if f := testSchema.GetFieldByName("test4"); f == nil || f.Name != "test4" { + t.Fatalf("Expected f4 field to be added, found %v", f) + } +} + +func TestSchemaValidate(t *testing.T) { + // emulate duplicated field ids + duplicatedIdsSchema := schema.NewSchema( + &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText}, + &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText}, + ) + duplicatedIdsSchema.Fields()[1].Id = "id1" // manually set existing id + + scenarios := []struct { + schema schema.Schema + expectError bool + }{ + // no fields + { + schema.NewSchema(), + true, + }, + // duplicated field ids + { + duplicatedIdsSchema, + true, + }, + // duplicated field names (case insensitive) + { + schema.NewSchema( + &schema.SchemaField{Name: "test", Type: schema.FieldTypeText}, + &schema.SchemaField{Name: "TeSt", Type: schema.FieldTypeText}, + ), + true, + }, + // failure - base individual fields validation + { + schema.NewSchema( + &schema.SchemaField{Name: "", Type: schema.FieldTypeText}, + ), + true, + }, + // success - base individual fields validation + { + schema.NewSchema( + &schema.SchemaField{Name: "test", Type: schema.FieldTypeText}, + ), + false, + }, + // failure - individual field options validation + { + schema.NewSchema( + &schema.SchemaField{Name: "test", Type: schema.FieldTypeFile}, + ), + true, + }, + // success - individual field options validation + { + schema.NewSchema( + &schema.SchemaField{Name: "test", Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}}, + ), + false, + }, + } + + for i, s := range scenarios { + err := s.schema.Validate() + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + } +} + +func TestSchemaMarshalJSON(t *testing.T) { + f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText} + f2 := &schema.SchemaField{ + Id: "f2id", + Name: "test2", + Type: schema.FieldTypeText, + Options: &schema.TextOptions{Pattern: "test"}, + } + testSchema := schema.NewSchema(f1, f2) + + result, err := testSchema.MarshalJSON() + if err != nil { + t.Fatal(err) + } + + expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"id":"f2id","name":"test2","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]` + + if string(result) != expected { + t.Fatalf("Expected %s, got %s", expected, string(result)) + } +} + +func TestSchemaUnmarshalJSON(t *testing.T) { + encoded := `[{"system":false,"id":"fid1", "name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"name":"test2","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]` + testSchema := schema.Schema{} + testSchema.AddField(&schema.SchemaField{Name: "tempField", Type: schema.FieldTypeUrl}) + err := testSchema.UnmarshalJSON([]byte(encoded)) + if err != nil { + t.Fatal(err) + } + + fields := testSchema.Fields() + if len(fields) != 2 { + t.Fatalf("Expected 2 fields, found %v", fields) + } + + f1 := testSchema.GetFieldByName("test1") + if f1 == nil { + t.Fatal("Expected to find field 'test1', got nil") + } + if f1.Id != "fid1" { + t.Fatalf("Expected fid1 id, got %s", f1.Id) + } + _, ok := f1.Options.(*schema.TextOptions) + if !ok { + t.Fatal("'test1' field options are not inited.") + } + + f2 := testSchema.GetFieldByName("test2") + if f2 == nil { + t.Fatal("Expected to find field 'test2', got nil") + } + if f2.Id == "" { + t.Fatal("Expected f2 id to be set, got empty string") + } + o2, ok := f2.Options.(*schema.TextOptions) + if !ok { + t.Fatal("'test2' field options are not inited.") + } + if o2.Pattern != "test" { + t.Fatalf("Expected pattern to be %q, got %q", "test", o2.Pattern) + } +} + +func TestSchemaValue(t *testing.T) { + // empty schema + s1 := schema.Schema{} + v1, err := s1.Value() + if err != nil { + t.Fatal(err) + } + if v1 != nil { + t.Fatalf("Expected nil, got %v", v1) + } + + // schema with fields + f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText} + s2 := schema.NewSchema(f1) + + v2, err := s2.Value() + if err != nil { + t.Fatal(err) + } + expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]` + + if v2 != expected { + t.Fatalf("Expected %v, got %v", expected, v2) + } +} + +func TestSchemaScan(t *testing.T) { + scenarios := []struct { + data any + expectError bool + expectJson string + }{ + {nil, false, "[]"}, + {"", false, "[]"}, + {[]byte{}, false, "[]"}, + {"[]", false, "[]"}, + {"invalid", true, "[]"}, + {123, true, "[]"}, + // no field type + {`[{}]`, true, `[]`}, + // unknown field type + { + `[{"system":false,"id":"123","name":"test1","type":"unknown","required":false,"unique":false}]`, + true, + `[]`, + }, + // without options + { + `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false}]`, + false, + `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`, + }, + // with options + { + `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`, + false, + `[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`, + }, + } + + for i, s := range scenarios { + testSchema := schema.Schema{} + + err := testSchema.Scan(s.data) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + json, _ := testSchema.MarshalJSON() + if string(json) != s.expectJson { + t.Errorf("(%d) Expected json %v, got %v", i, s.expectJson, string(json)) + } + } +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 00000000..45793b51 --- /dev/null +++ b/models/user.go @@ -0,0 +1,47 @@ +package models + +import ( + "encoding/json" + + "github.com/pocketbase/pocketbase/tools/types" +) + +var _ Model = (*User)(nil) + +const ( + // The name of the system user profiles collection. + ProfileCollectionName = "profiles" + + // The name of the user field from the system user profiles collection. + ProfileCollectionUserFieldName = "userId" +) + +type User struct { + BaseAccount + + Verified bool `db:"verified" json:"verified"` + LastVerificationSentAt types.DateTime `db:"lastVerificationSentAt" json:"lastVerificationSentAt"` + + // profile rel + Profile *Record `db:"-" json:"profile"` +} + +func (m *User) TableName() string { + return "_users" +} + +// AsMap returns the current user data as a plain map +// (including the profile relation, if loaded). +func (m *User) AsMap() (map[string]any, error) { + userBytes, err := json.Marshal(m) + if err != nil { + return nil, err + } + + result := map[string]any{} + if err := json.Unmarshal(userBytes, &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/models/user_test.go b/models/user_test.go new file mode 100644 index 00000000..19550213 --- /dev/null +++ b/models/user_test.go @@ -0,0 +1,43 @@ +package models_test + +import ( + "encoding/json" + "testing" + + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestUserTableName(t *testing.T) { + m := models.User{} + if m.TableName() != "_users" { + t.Fatalf("Unexpected table name, got %q", m.TableName()) + } +} + +func TestUserAsMap(t *testing.T) { + date, _ := types.ParseDateTime("2022-01-01 01:12:23.456") + + m := models.User{} + m.Id = "210a896c-1e32-4c94-ae06-90c25fcf6791" + m.Email = "test@example.com" + m.PasswordHash = "test" + m.LastResetSentAt = date + m.Updated = date + m.RefreshTokenKey() + + result, err := m.AsMap() + if err != nil { + t.Fatal(err) + } + + encoded, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + + expected := `{"created":"","email":"test@example.com","id":"210a896c-1e32-4c94-ae06-90c25fcf6791","lastResetSentAt":"2022-01-01 01:12:23.456","lastVerificationSentAt":"","profile":null,"updated":"2022-01-01 01:12:23.456","verified":false}` + if string(encoded) != expected { + t.Errorf("Expected %s, got %s", expected, string(encoded)) + } +} diff --git a/pocketbase.go b/pocketbase.go new file mode 100644 index 00000000..02e36156 --- /dev/null +++ b/pocketbase.go @@ -0,0 +1,205 @@ +package pocketbase + +import ( + "log" + "os" + "os/signal" + "path/filepath" + "strings" + "sync" + "syscall" + + "github.com/pocketbase/pocketbase/cmd" + "github.com/pocketbase/pocketbase/core" + "github.com/spf13/cobra" +) + +var _ core.App = (*PocketBase)(nil) + +// Version of PocketBase +const Version = "0.1.0" + +// appWrapper serves as a private core.App instance wrapper. +type appWrapper struct { + core.App +} + +// PocketBase defines a PocketBase app launcher. +// +// It implements [core.App] via embedding and all of interface methods +// could be accessed directly through the instance (eg. PocketBase.DataDir()). +type PocketBase struct { + *appWrapper + + // RootCmd is the main cli command + RootCmd *cobra.Command + + // console flags + debugFlag bool + dataDirFlag string + encryptionEnv string + + // console flag fallback values + defaultDebug bool + defaultDataDir string + defaultEncryptionEnv string + + // serve start banner + showStartBanner bool +} + +// New creates a new PocketBase instance. +// +// Note that the application will not be initialized/bootstrapped yet, +// aka. DB connections, migrations, app settings, etc. will not be accessable. +// Everything will be initialized when Start() is executed. +// If you want initialize the application before calling Start(), +// then you'll have to manually call Bootstrap(). +func New() *PocketBase { + // try to find the base executable directory and how it was run + var withGoRun bool + var baseDir string + if strings.HasPrefix(os.Args[0], os.TempDir()) { + // probably ran with go run... + withGoRun = true + baseDir, _ = os.Getwd() + } else { + // probably ran with go build... + withGoRun = false + baseDir = filepath.Dir(os.Args[0]) + } + + defaultDir := filepath.Join(baseDir, "pb_data") + + pb := &PocketBase{ + RootCmd: &cobra.Command{ + Use: "pocketbase", + Short: "PocketBase CLI", + FParseErrWhitelist: cobra.FParseErrWhitelist{ + UnknownFlags: true, + }, + }, + defaultDebug: withGoRun, + defaultDataDir: defaultDir, + defaultEncryptionEnv: "", + showStartBanner: true, + } + + // no need to provide the default cobra completion command + pb.RootCmd.CompletionOptions.DisableDefaultCmd = true + + // parse base flags + // (errors are ignored, since the full flags parsing happens on Execute()) + pb.eagerParseFlags() + + pb.appWrapper = &appWrapper{core.NewBaseApp( + pb.dataDirFlag, + pb.encryptionEnv, + pb.debugFlag, + )} + + return pb +} + +// DefaultDebug sets the default --debug flag value. +func (pb *PocketBase) DefaultDebug(val bool) *PocketBase { + pb.defaultDebug = val + return pb +} + +// DefaultDataDir sets the default --dir flag value. +func (pb *PocketBase) DefaultDataDir(val string) *PocketBase { + pb.defaultDataDir = val + return pb +} + +// DefaultEncryptionEnv sets the default --encryptionEnv flag value. +func (pb *PocketBase) DefaultEncryptionEnv(val string) *PocketBase { + pb.defaultEncryptionEnv = val + return pb +} + +// ShowStartBanner shows/hides the web server start banner. +func (pb *PocketBase) ShowStartBanner(val bool) *PocketBase { + pb.showStartBanner = val + return pb +} + +// Start starts the application, aka. registers the default system +// commands (serve, migrate, version) and executes pb.RootCmd. +func (pb *PocketBase) Start() error { + // register system commands + pb.RootCmd.AddCommand(cmd.NewServeCommand(pb, pb.showStartBanner)) + pb.RootCmd.AddCommand(cmd.NewVersionCommand(pb, Version)) + pb.RootCmd.AddCommand(cmd.NewMigrateCommand(pb)) + + return pb.Execute() +} + +// Execute initializes the application (if not already) and executes +// the pb.RootCmd with graceful shutdown support. +// +// This method differs from pb.Start() by not registering the default +// system commands! +func (pb *PocketBase) Execute() error { + if err := pb.Bootstrap(); err != nil { + return err + } + + var wg sync.WaitGroup + + wg.Add(1) + + // wait for interrupt signal to gracefully shutdown the application + go func() { + defer wg.Done() + quit := make(chan os.Signal, 1) // we need to reserve to buffer size 1, so the notifier are not blocked + signal.Notify(quit, os.Interrupt, syscall.SIGTERM) + <-quit + }() + + // execute the root command + go func() { + defer wg.Done() + if err := pb.RootCmd.Execute(); err != nil { + log.Println(err) + } + }() + + wg.Wait() + + // cleanup + return pb.onTerminate() +} + +// onTerminate tries to release the app resources on app termination. +func (pb *PocketBase) onTerminate() error { + return pb.ResetBootstrapState() +} + +// eagerParseFlags parses the global app flags before calling pb.RootCmd.Execute(). +// so we can have all PocketBase flags ready for use on initialization. +func (pb *PocketBase) eagerParseFlags() error { + pb.RootCmd.PersistentFlags().StringVar( + &pb.dataDirFlag, + "dir", + pb.defaultDataDir, + "the PocketBase data directory", + ) + + pb.RootCmd.PersistentFlags().StringVar( + &pb.encryptionEnv, + "encryptionEnv", + pb.defaultEncryptionEnv, + "the env variable whose value of 32 chars will be used \nas encryption key for the app settings (default none)", + ) + + pb.RootCmd.PersistentFlags().BoolVar( + &pb.debugFlag, + "debug", + pb.defaultDebug, + "enable debug mode, aka. showing more detailed logs", + ) + + return pb.RootCmd.ParseFlags(os.Args[1:]) +} diff --git a/pocketbase_test.go b/pocketbase_test.go new file mode 100644 index 00000000..68a006fa --- /dev/null +++ b/pocketbase_test.go @@ -0,0 +1,96 @@ +package pocketbase + +import ( + "os" + "testing" +) + +func TestNew(t *testing.T) { + testDir := "./pb_test_data_dir" + defer os.RemoveAll(testDir) + + // reset os.Args + os.Args = os.Args[0:1] + os.Args = append( + os.Args, + "--dir="+testDir, + "--encryptionEnv=test_encryption_env", + "--debug=true", + ) + + app := New() + + if app == nil { + t.Fatal("Expected initialized PocketBase instance, got nil") + } + + if app.RootCmd == nil { + t.Fatal("Expected RootCmd to be initialized, got nil") + } + + if app.appWrapper == nil { + t.Fatal("Expected appWrapper to be initialized, got nil") + } + + if app.DataDir() != testDir { + t.Fatalf("Expected app.DataDir() %q, got %q", testDir, app.DataDir()) + } + + if app.EncryptionEnv() != "test_encryption_env" { + t.Fatalf("Expected app.DataDir() test_encryption_env, got %q", app.EncryptionEnv()) + } + + if app.IsDebug() != true { + t.Fatal("Expected app.IsDebug() true, got false") + } +} + +func TestDefaultDebug(t *testing.T) { + app := New() + + app.DefaultDebug(true) + if app.defaultDebug != true { + t.Fatalf("Expected defaultDebug %v, got %v", true, app.defaultDebug) + } + + app.DefaultDebug(false) + if app.defaultDebug != false { + t.Fatalf("Expected defaultDebug %v, got %v", false, app.defaultDebug) + } +} + +func TestDefaultDataDir(t *testing.T) { + app := New() + + expected := "test_default" + + app.DefaultDataDir(expected) + if app.defaultDataDir != expected { + t.Fatalf("Expected defaultDataDir %v, got %v", expected, app.defaultDataDir) + } +} + +func TestDefaultEncryptionEnv(t *testing.T) { + app := New() + + expected := "test_env" + + app.DefaultEncryptionEnv(expected) + if app.defaultEncryptionEnv != expected { + t.Fatalf("Expected defaultEncryptionEnv %v, got %v", expected, app.defaultEncryptionEnv) + } +} + +func TestShowStartBanner(t *testing.T) { + app := New() + + app.ShowStartBanner(true) + if app.showStartBanner != true { + t.Fatalf("Expected showStartBanner %v, got %v", true, app.showStartBanner) + } + + app.ShowStartBanner(false) + if app.showStartBanner != false { + t.Fatalf("Expected showStartBanner %v, got %v", false, app.showStartBanner) + } +} diff --git a/resolvers/record_field_resolver.go b/resolvers/record_field_resolver.go new file mode 100644 index 00000000..76291236 --- /dev/null +++ b/resolvers/record_field_resolver.go @@ -0,0 +1,282 @@ +package resolvers + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/inflector" + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/search" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/spf13/cast" +) + +// ensure that `search.FieldResolver` interface is implemented +var _ search.FieldResolver = (*RecordFieldResolver)(nil) + +type join struct { + table string + on dbx.Expression +} + +// RecordFieldResolver defines a custom search resolver struct for +// managing Record model search fields. +// +// Usually used together with `search.Provider`. Example: +// resolver := resolvers.NewRecordFieldResolver(app.Dao(), myCollection, map[string]any{"test": 123}) +// provider := search.NewProvider(resolver) +// ... +type RecordFieldResolver struct { + dao *daos.Dao + baseCollection *models.Collection + allowedFields []string + requestData map[string]any + joins map[string]join + loadedCollections []*models.Collection +} + +// NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`. +func NewRecordFieldResolver( + dao *daos.Dao, + baseCollection *models.Collection, + requestData map[string]any, +) *RecordFieldResolver { + return &RecordFieldResolver{ + dao: dao, + baseCollection: baseCollection, + requestData: requestData, + joins: make(map[string]join), + loadedCollections: []*models.Collection{baseCollection}, + allowedFields: []string{ + `^\w+[\w\.]*$`, + `^\@request\.method$`, + `^\@request\.user\.\w+[\w\.]*$`, + `^\@request\.data\.\w+[\w\.]*$`, + `^\@request\.query\.\w+[\w\.]*$`, + `^\@collection\.\w+\.\w+[\w\.]*$`, + }, + } +} + +// UpdateQuery implements `search.FieldResolver` interface. +// +// Conditionally updates the provided search query based on the +// resolved fields (eg. dynamically joining relations). +func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error { + if len(r.joins) > 0 { + query.Distinct(true) + + for _, join := range r.joins { + query.LeftJoin(join.table, join.on) + } + } + + return nil +} + +// Resolve implements `search.FieldResolver` interface. +// +// Example of resolvable field formats: +// id +// project.screen.status +// @request.status +// @collection.product.name +func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, placeholderParams dbx.Params, err error) { + if len(r.allowedFields) > 0 && !list.ExistInSliceWithRegex(fieldName, r.allowedFields) { + return "", nil, fmt.Errorf("Failed to resolve field %q", fieldName) + } + + props := strings.Split(fieldName, ".") + + // check for @request field + if props[0] == "@request" { + if len(props) == 1 { + return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName) + } + + return r.resolveRequestField(props[1:]...) + } + + currentCollectionName := r.baseCollection.Name + currentTableAlias := currentCollectionName + + // check for @collection field (aka. non-relational join) + // must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]" + if props[0] == "@collection" { + if len(props) < 3 { + return "", nil, fmt.Errorf("Invalid @collection field path in %q.", fieldName) + } + + currentCollectionName = props[1] + currentTableAlias = "c_" + currentCollectionName + + collection, err := r.loadCollection(currentCollectionName) + if err != nil { + return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName) + } + + r.addJoin(collection.Name, currentTableAlias, "", "", "") + + props = props[2:] // leave only the collection fields + } + + baseModelFields := schema.ReservedFieldNames() + + totalProps := len(props) + + for i, prop := range props { + collection, err := r.loadCollection(currentCollectionName) + if err != nil { + return "", nil, fmt.Errorf("Failed to resolve field %q.", prop) + } + + // base model prop (always available but not part of the collection schema) + if list.ExistInSlice(prop, baseModelFields) { + return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil + } + + field := collection.Schema.GetFieldByName(prop) + if field == nil { + return "", nil, fmt.Errorf("Unrecognized field %q.", prop) + } + + // last prop + if i == totalProps-1 { + return fmt.Sprintf("[[%s.%s]]", inflector.Columnify(currentTableAlias), inflector.Columnify(prop)), nil, nil + } + + // check if it is a relation field + if field.Type != schema.FieldTypeRelation { + return "", nil, fmt.Errorf("Field %q is not a valid relation.", prop) + } + + // auto join the relation + // --- + field.InitOptions() + options, ok := field.Options.(*schema.RelationOptions) + if !ok { + return "", nil, fmt.Errorf("Failed to initialize field %q options.", prop) + } + + relCollection, relErr := r.loadCollection(options.CollectionId) + if relErr != nil { + return "", nil, fmt.Errorf("Failed to find field %q collection.", prop) + } + newCollectionName := relCollection.Name + newTableAlias := (currentTableAlias + "_" + field.Name) + + r.addJoin( + newCollectionName, + newTableAlias, + "id", + currentTableAlias, + field.Name, + ) + + currentCollectionName = newCollectionName + currentTableAlias = newTableAlias + } + + return "", nil, fmt.Errorf("Failed to resolve field %q.", fieldName) +} + +func (r *RecordFieldResolver) resolveRequestField(path ...string) (resultName string, placeholderParams dbx.Params, err error) { + // ignore error because requestData is dynamic and some of the + // lookup keys may not be defined for the request + resultVal, _ := extractNestedMapVal(r.requestData, path...) + + switch v := resultVal.(type) { + case nil: + return "NULL", nil, nil + case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + // no further processing is needed... + default: + // non-plain value + // try casting to string (in case for exampe fmt.Stringer is implemented) + val, castErr := cast.ToStringE(v) + + // if that doesn't work, try encoding it + if castErr != nil { + encoded, jsonErr := json.Marshal(v) + if jsonErr == nil { + val = string(encoded) + } + } + + resultVal = val + } + + placeholder := "f" + security.RandomString(7) + name := fmt.Sprintf("{:%s}", placeholder) + params := dbx.Params{placeholder: resultVal} + + return name, params, nil +} + +func extractNestedMapVal(m map[string]any, keys ...string) (result any, err error) { + var ok bool + + if len(keys) == 0 { + return nil, fmt.Errorf("At least one key should be provided.") + } + + if result, ok = m[keys[0]]; !ok { + return nil, fmt.Errorf("Invalid key path - missing key %q.", keys[0]) + } + + // end key reached + if len(keys) == 1 { + return result, nil + } + + if m, ok = result.(map[string]any); !ok { + return nil, fmt.Errorf("Expected map structure, got %#v.", result) + } + + return extractNestedMapVal(m, keys[1:]...) +} + +func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models.Collection, error) { + // return already loaded + for _, collection := range r.loadedCollections { + if collection.Name == collectionNameOrId || collection.Id == collectionNameOrId { + return collection, nil + } + } + + // load collection + collection, err := r.dao.FindCollectionByNameOrId(collectionNameOrId) + if err != nil { + return nil, err + } + r.loadedCollections = append(r.loadedCollections, collection) + + return collection, nil +} + +func (r *RecordFieldResolver) addJoin(tableName, tableAlias, fieldName, ref, refFieldName string) { + table := fmt.Sprintf( + "%s %s", + inflector.Columnify(tableName), + inflector.Columnify(tableAlias), + ) + + var on dbx.Expression + if ref != "" { + on = dbx.NewExp(fmt.Sprintf( + // 'LIKE' expr is used to handle the case when the reference field supports multiple values (aka. is json array) + "[[%s.%s]] LIKE ('%%' || [[%s.%s]] || '%%')", + inflector.Columnify(ref), + inflector.Columnify(refFieldName), + inflector.Columnify(tableAlias), + inflector.Columnify(fieldName), + )) + } + + r.joins[tableAlias] = join{table, on} +} diff --git a/resolvers/record_field_resolver_test.go b/resolvers/record_field_resolver_test.go new file mode 100644 index 00000000..f6e86071 --- /dev/null +++ b/resolvers/record_field_resolver_test.go @@ -0,0 +1,245 @@ +package resolvers_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/resolvers" + "github.com/pocketbase/pocketbase/tests" +) + +func TestRecordFieldResolverUpdateQuery(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.Dao().FindCollectionByNameOrId("demo4") + if err != nil { + t.Fatal(err) + } + + scenarios := []struct { + fieldName string + expectQueryParts []string // we are matching parts of the query + // since joins are added with map iteration and the order is not guaranteed + }{ + // missing field + {"", []string{ + "SELECT `demo4`.* FROM `demo4`", + }}, + // non relation field + {"title", []string{ + "SELECT `demo4`.* FROM `demo4`", + }}, + // incomplete rel + {"onerel", []string{ + "SELECT `demo4`.* FROM `demo4`", + }}, + // single rel + {"onerel.title", []string{ + "SELECT DISTINCT `demo4`.* FROM `demo4`", + " LEFT JOIN `demo4` `demo4_onerel` ON [[demo4.onerel]] LIKE ('%' || [[demo4_onerel.id]] || '%')", + }}, + // nested incomplete rels + {"manyrels.onerel", []string{ + "SELECT DISTINCT `demo4`.* FROM `demo4`", + " LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')", + }}, + // nested complete rels + {"manyrels.onerel.title", []string{ + "SELECT DISTINCT `demo4`.* FROM `demo4`", + " LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')", + " LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')", + }}, + // // repeated nested rels + {"manyrels.onerel.manyrels.onerel.title", []string{ + "SELECT DISTINCT `demo4`.* FROM `demo4`", + " LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4.manyrels]] LIKE ('%' || [[demo4_manyrels.id]] || '%')", + " LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel.id]] || '%')", + " LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel.manyrels]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels.id]] || '%')", + " LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels.onerel]] LIKE ('%' || [[demo4_manyrels_onerel_manyrels_onerel.id]] || '%')", + }}, + } + + for i, s := range scenarios { + query := app.Dao().RecordQuery(collection) + + r := resolvers.NewRecordFieldResolver(app.Dao(), collection, nil) + r.Resolve(s.fieldName) + + if err := r.UpdateQuery(query); err != nil { + t.Errorf("(%d) UpdateQuery failed with error %v", i, err) + continue + } + + rawQuery := query.Build().SQL() + + partsLength := 0 + for _, part := range s.expectQueryParts { + partsLength += len(part) + if !strings.Contains(rawQuery, part) { + t.Errorf("(%d) Part %v is missing from query \n%v", i, part, rawQuery) + } + } + + if partsLength != len(rawQuery) { + t.Errorf("(%d) Expected %d characters, got %d in \n%v", i, partsLength, len(rawQuery), rawQuery) + } + } +} + +func TestRecordFieldResolverResolveSchemaFields(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.Dao().FindCollectionByNameOrId("demo4") + if err != nil { + t.Fatal(err) + } + + r := resolvers.NewRecordFieldResolver(app.Dao(), collection, nil) + + scenarios := []struct { + fieldName string + expectError bool + expectName string + }{ + {"", true, ""}, + {" ", true, ""}, + {"unknown", true, ""}, + {"invalid format", true, ""}, + {"id", false, "[[demo4.id]]"}, + {"created", false, "[[demo4.created]]"}, + {"updated", false, "[[demo4.updated]]"}, + {"title", false, "[[demo4.title]]"}, + {"title.test", true, ""}, + {"manyrels", false, "[[demo4.manyrels]]"}, + {"manyrels.", true, ""}, + {"manyrels.unknown", true, ""}, + {"manyrels.title", false, "[[demo4_manyrels.title]]"}, + {"manyrels.onerel.manyrels.onefile", false, "[[demo4_manyrels_onerel_manyrels.onefile]]"}, + {"@collect", true, ""}, + {"collection.demo4.title", true, ""}, + {"@collection", true, ""}, + {"@collection.unknown", true, ""}, + {"@collection.demo", true, ""}, + {"@collection.demo.", true, ""}, + {"@collection.demo.title", false, "[[c_demo.title]]"}, + {"@collection.demo4.title", false, "[[c_demo4.title]]"}, + {"@collection.demo4.id", false, "[[c_demo4.id]]"}, + {"@collection.demo4.created", false, "[[c_demo4.created]]"}, + {"@collection.demo4.updated", false, "[[c_demo4.updated]]"}, + {"@collection.demo4.manyrels.missing", true, ""}, + {"@collection.demo4.manyrels.onerel.manyrels.onerel.onefile", false, "[[c_demo4_manyrels_onerel_manyrels_onerel.onefile]]"}, + } + + for i, s := range scenarios { + name, params, err := r.Resolve(s.fieldName) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if name != s.expectName { + t.Errorf("(%d) Expected name %q, got %q", i, s.expectName, name) + } + + // params should be empty for non @request fields + if len(params) != 0 { + t.Errorf("(%d) Expected 0 params, got %v", i, params) + } + } +} + +func TestRecordFieldResolverResolveRequestDataFields(t *testing.T) { + app, _ := tests.NewTestApp() + defer app.Cleanup() + + collection, err := app.Dao().FindCollectionByNameOrId("demo4") + if err != nil { + t.Fatal(err) + } + + requestData := map[string]any{ + "method": "get", + "query": map[string]any{ + "a": 123, + }, + "data": map[string]any{ + "b": 456, + "c": map[string]int{"sub": 1}, + }, + "user": nil, + } + + r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData) + + scenarios := []struct { + fieldName string + expectError bool + expectParamValue string // encoded json + }{ + {"@request", true, ""}, + {"@request.invalid format", true, ""}, + {"@request.invalid_format2!", true, ""}, + {"@request.missing", true, ""}, + {"@request.method", false, `"get"`}, + {"@request.query", true, ``}, + {"@request.query.a", false, `123`}, + {"@request.query.a.missing", false, ``}, + {"@request.data", true, ``}, + {"@request.data.b", false, `456`}, + {"@request.data.b.missing", false, ``}, + {"@request.data.c", false, `"{\"sub\":1}"`}, + {"@request.user", true, ""}, + {"@request.user.id", false, ""}, + } + + for i, s := range scenarios { + name, params, err := r.Resolve(s.fieldName) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if hasErr { + continue + } + + // missing key + // --- + if len(params) == 0 { + if name != "NULL" { + t.Errorf("(%d) Expected 0 placeholder parameters, got %v", i, params) + } + continue + } + + // existing key + // --- + if len(params) != 1 { + t.Errorf("(%d) Expected 1 placeholder parameter, got %v", i, params) + continue + } + + var paramName string + var paramValue any + for k, v := range params { + paramName = k + paramValue = v + } + + if name != ("{:" + paramName + "}") { + t.Errorf("(%d) Expected parameter name %q, got %q", i, paramName, name) + } + + encodedParamValue, _ := json.Marshal(paramValue) + if string(encodedParamValue) != s.expectParamValue { + t.Errorf("(%d) Expected params %v, got %v", i, s.expectParamValue, string(encodedParamValue)) + } + } +} diff --git a/resolvers/resolvers.go b/resolvers/resolvers.go new file mode 100644 index 00000000..8c045a89 --- /dev/null +++ b/resolvers/resolvers.go @@ -0,0 +1,2 @@ +// Package resolvers contains custom search.FieldResolver implementations. +package resolvers diff --git a/tests/api.go b/tests/api.go new file mode 100644 index 00000000..5674f2ec --- /dev/null +++ b/tests/api.go @@ -0,0 +1,121 @@ +package tests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" +) + +// ApiScenario defines a single api request test case/scenario. +type ApiScenario struct { + Name string + Method string + Url string + Body io.Reader + RequestHeaders map[string]string + // expectations + ExpectedStatus int + ExpectedContent []string + ExpectedEvents map[string]int + // test events + BeforeFunc func(t *testing.T, app *TestApp, e *echo.Echo) + AfterFunc func(t *testing.T, app *TestApp, e *echo.Echo) +} + +// Test executes the test case/scenario. +func (scenario *ApiScenario) Test(t *testing.T) { + testApp, _ := NewTestApp() + defer testApp.Cleanup() + + e, err := apis.InitApi(testApp) + if err != nil { + t.Fatal(err) + } + + if scenario.BeforeFunc != nil { + scenario.BeforeFunc(t, testApp, e) + } + + recorder := httptest.NewRecorder() + req := httptest.NewRequest(scenario.Method, scenario.Url, scenario.Body) + + // add middeware to timeout long running requests (eg. keep-alive routes) + e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + ctx, cancelFunc := context.WithTimeout(c.Request().Context(), 100*time.Millisecond) + defer cancelFunc() + c.SetRequest(c.Request().Clone(ctx)) + return next(c) + } + }) + + // set default header + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + // set scenario headers + for k, v := range scenario.RequestHeaders { + req.Header.Set(k, v) + } + + // execute request + e.ServeHTTP(recorder, req) + + res := recorder.Result() + + var prefix = scenario.Name + if prefix == "" { + prefix = fmt.Sprintf("%s:%s", scenario.Method, scenario.Url) + } + + if res.StatusCode != scenario.ExpectedStatus { + t.Errorf("[%s] Expected status code %d, got %d", prefix, scenario.ExpectedStatus, res.StatusCode) + } + + if len(scenario.ExpectedContent) == 0 { + if len(recorder.Body.Bytes()) != 0 { + t.Errorf("[%s] Expected empty body, got %v", prefix, recorder.Body.String()) + } + } else { + // normalize json response format + buffer := new(bytes.Buffer) + err := json.Compact(buffer, recorder.Body.Bytes()) + var normalizedBody string + if err != nil { + // not a json... + normalizedBody = recorder.Body.String() + } else { + normalizedBody = buffer.String() + } + + for _, item := range scenario.ExpectedContent { + if !strings.Contains(normalizedBody, item) { + t.Errorf("[%s] Cannot find %v in response body %v", prefix, item, normalizedBody) + break + } + } + } + + if len(testApp.EventCalls) > len(scenario.ExpectedEvents) { + t.Errorf("[%s] Expected events %v, got %v", prefix, scenario.ExpectedEvents, testApp.EventCalls) + } + + for event, expectedCalls := range scenario.ExpectedEvents { + actualCalls, _ := testApp.EventCalls[event] + if actualCalls != expectedCalls { + t.Errorf("[%s] Expected event %s to be called %d, got %d", prefix, event, expectedCalls, actualCalls) + } + } + + if scenario.AfterFunc != nil { + scenario.AfterFunc(t, testApp, e) + } +} diff --git a/tests/app.go b/tests/app.go new file mode 100644 index 00000000..4c64c3a9 --- /dev/null +++ b/tests/app.go @@ -0,0 +1,447 @@ +// Pacakge tests provides common helpers and mocks used in PocketBase application tests. +package tests + +import ( + "io" + "os" + "path" + "path/filepath" + "runtime" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/mailer" +) + +// TestApp is a wrapper app instance used for testing. +type TestApp struct { + *core.BaseApp + + // EventCalls defines a map to inspect which app events + // (and how many times) were triggered. + EventCalls map[string]int + TestMailer *TestMailer +} + +// Cleanup resets the test application state and removes the test +// app's dataDir from the filesystem. +// +// After this call, the app instance shouldn't be used anymore. +func (t *TestApp) Cleanup() { + t.ResetEventCalls() + t.ResetBootstrapState() + + if t.DataDir() != "" { + os.RemoveAll(t.DataDir()) + } +} + +func (t *TestApp) NewMailClient() mailer.Mailer { + t.TestMailer.Reset() + return t.TestMailer +} + +// ResetEventCalls resets the EventCalls counter. +func (t *TestApp) ResetEventCalls() { + t.EventCalls = make(map[string]int) +} + +// NewTestApp creates and initializes a full application instance for testing. +// +// It is the caller's responsibility to call `app.Cleanup()` +// when the app is no longer needed. +func NewTestApp() (*TestApp, error) { + tempDir, err := NewTempDataDir() + if err != nil { + return nil, err + } + + app := core.NewBaseApp(tempDir, "pb_test_env", false) + + // load data dir and db connections + if err := app.Bootstrap(); err != nil { + return nil, err + } + + // force disable request logs because the logs db call execute in a separate + // go routine and it is possible to panic due to earlier api test completion. + app.Settings().Logs.MaxDays = 0 + + t := &TestApp{ + BaseApp: app, + EventCalls: make(map[string]int), + TestMailer: &TestMailer{}, + } + + // no need to count since this is executed always + // t.OnBeforeServe().Add(func(e *core.ServeEvent) error { + // t.EventCalls["OnBeforeServe"]++ + // return nil + // }) + + t.OnModelBeforeCreate().Add(func(e *core.ModelEvent) error { + t.EventCalls["OnModelBeforeCreate"]++ + return nil + }) + + t.OnModelAfterCreate().Add(func(e *core.ModelEvent) error { + t.EventCalls["OnModelAfterCreate"]++ + return nil + }) + + t.OnModelBeforeUpdate().Add(func(e *core.ModelEvent) error { + t.EventCalls["OnModelBeforeUpdate"]++ + return nil + }) + + t.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error { + t.EventCalls["OnModelAfterUpdate"]++ + return nil + }) + + t.OnModelBeforeDelete().Add(func(e *core.ModelEvent) error { + t.EventCalls["OnModelBeforeDelete"]++ + return nil + }) + + t.OnModelAfterDelete().Add(func(e *core.ModelEvent) error { + t.EventCalls["OnModelAfterDelete"]++ + return nil + }) + + t.OnRecordsListRequest().Add(func(e *core.RecordsListEvent) error { + t.EventCalls["OnRecordsListRequest"]++ + return nil + }) + + t.OnRecordViewRequest().Add(func(e *core.RecordViewEvent) error { + t.EventCalls["OnRecordViewRequest"]++ + return nil + }) + + t.OnRecordBeforeCreateRequest().Add(func(e *core.RecordCreateEvent) error { + t.EventCalls["OnRecordBeforeCreateRequest"]++ + return nil + }) + + t.OnRecordAfterCreateRequest().Add(func(e *core.RecordCreateEvent) error { + t.EventCalls["OnRecordAfterCreateRequest"]++ + return nil + }) + + t.OnRecordBeforeUpdateRequest().Add(func(e *core.RecordUpdateEvent) error { + t.EventCalls["OnRecordBeforeUpdateRequest"]++ + return nil + }) + + t.OnRecordAfterUpdateRequest().Add(func(e *core.RecordUpdateEvent) error { + t.EventCalls["OnRecordAfterUpdateRequest"]++ + return nil + }) + + t.OnRecordBeforeDeleteRequest().Add(func(e *core.RecordDeleteEvent) error { + t.EventCalls["OnRecordBeforeDeleteRequest"]++ + return nil + }) + + t.OnRecordAfterDeleteRequest().Add(func(e *core.RecordDeleteEvent) error { + t.EventCalls["OnRecordAfterDeleteRequest"]++ + return nil + }) + + t.OnUsersListRequest().Add(func(e *core.UsersListEvent) error { + t.EventCalls["OnUsersListRequest"]++ + return nil + }) + + t.OnUserViewRequest().Add(func(e *core.UserViewEvent) error { + t.EventCalls["OnUserViewRequest"]++ + return nil + }) + + t.OnUserBeforeCreateRequest().Add(func(e *core.UserCreateEvent) error { + t.EventCalls["OnUserBeforeCreateRequest"]++ + return nil + }) + + t.OnUserAfterCreateRequest().Add(func(e *core.UserCreateEvent) error { + t.EventCalls["OnUserAfterCreateRequest"]++ + return nil + }) + + t.OnUserBeforeUpdateRequest().Add(func(e *core.UserUpdateEvent) error { + t.EventCalls["OnUserBeforeUpdateRequest"]++ + return nil + }) + + t.OnUserAfterUpdateRequest().Add(func(e *core.UserUpdateEvent) error { + t.EventCalls["OnUserAfterUpdateRequest"]++ + return nil + }) + + t.OnUserBeforeDeleteRequest().Add(func(e *core.UserDeleteEvent) error { + t.EventCalls["OnUserBeforeDeleteRequest"]++ + return nil + }) + + t.OnUserAfterDeleteRequest().Add(func(e *core.UserDeleteEvent) error { + t.EventCalls["OnUserAfterDeleteRequest"]++ + return nil + }) + + t.OnUserBeforeOauth2Register().Add(func(e *core.UserOauth2RegisterEvent) error { + t.EventCalls["OnUserBeforeOauth2Register"]++ + return nil + }) + + t.OnUserAfterOauth2Register().Add(func(e *core.UserOauth2RegisterEvent) error { + t.EventCalls["OnUserAfterOauth2Register"]++ + return nil + }) + + t.OnUserAuthRequest().Add(func(e *core.UserAuthEvent) error { + t.EventCalls["OnUserAuthRequest"]++ + return nil + }) + + t.OnMailerBeforeAdminResetPasswordSend().Add(func(e *core.MailerAdminEvent) error { + t.EventCalls["OnMailerBeforeAdminResetPasswordSend"]++ + return nil + }) + + t.OnMailerAfterAdminResetPasswordSend().Add(func(e *core.MailerAdminEvent) error { + t.EventCalls["OnMailerAfterAdminResetPasswordSend"]++ + return nil + }) + + t.OnMailerBeforeUserResetPasswordSend().Add(func(e *core.MailerUserEvent) error { + t.EventCalls["OnMailerBeforeUserResetPasswordSend"]++ + return nil + }) + + t.OnMailerAfterUserResetPasswordSend().Add(func(e *core.MailerUserEvent) error { + t.EventCalls["OnMailerAfterUserResetPasswordSend"]++ + return nil + }) + + t.OnMailerBeforeUserVerificationSend().Add(func(e *core.MailerUserEvent) error { + t.EventCalls["OnMailerBeforeUserVerificationSend"]++ + return nil + }) + + t.OnMailerAfterUserVerificationSend().Add(func(e *core.MailerUserEvent) error { + t.EventCalls["OnMailerAfterUserVerificationSend"]++ + return nil + }) + + t.OnMailerBeforeUserChangeEmailSend().Add(func(e *core.MailerUserEvent) error { + t.EventCalls["OnMailerBeforeUserChangeEmailSend"]++ + return nil + }) + + t.OnMailerAfterUserChangeEmailSend().Add(func(e *core.MailerUserEvent) error { + t.EventCalls["OnMailerAfterUserChangeEmailSend"]++ + return nil + }) + + t.OnRealtimeConnectRequest().Add(func(e *core.RealtimeConnectEvent) error { + t.EventCalls["OnRealtimeConnectRequest"]++ + return nil + }) + + t.OnRealtimeBeforeSubscribeRequest().Add(func(e *core.RealtimeSubscribeEvent) error { + t.EventCalls["OnRealtimeBeforeSubscribeRequest"]++ + return nil + }) + + t.OnRealtimeAfterSubscribeRequest().Add(func(e *core.RealtimeSubscribeEvent) error { + t.EventCalls["OnRealtimeAfterSubscribeRequest"]++ + return nil + }) + + t.OnSettingsListRequest().Add(func(e *core.SettingsListEvent) error { + t.EventCalls["OnSettingsListRequest"]++ + return nil + }) + + t.OnSettingsBeforeUpdateRequest().Add(func(e *core.SettingsUpdateEvent) error { + t.EventCalls["OnSettingsBeforeUpdateRequest"]++ + return nil + }) + + t.OnSettingsAfterUpdateRequest().Add(func(e *core.SettingsUpdateEvent) error { + t.EventCalls["OnSettingsAfterUpdateRequest"]++ + return nil + }) + + t.OnCollectionsListRequest().Add(func(e *core.CollectionsListEvent) error { + t.EventCalls["OnCollectionsListRequest"]++ + return nil + }) + + t.OnCollectionViewRequest().Add(func(e *core.CollectionViewEvent) error { + t.EventCalls["OnCollectionViewRequest"]++ + return nil + }) + + t.OnCollectionBeforeCreateRequest().Add(func(e *core.CollectionCreateEvent) error { + t.EventCalls["OnCollectionBeforeCreateRequest"]++ + return nil + }) + + t.OnCollectionAfterCreateRequest().Add(func(e *core.CollectionCreateEvent) error { + t.EventCalls["OnCollectionAfterCreateRequest"]++ + return nil + }) + + t.OnCollectionBeforeUpdateRequest().Add(func(e *core.CollectionUpdateEvent) error { + t.EventCalls["OnCollectionBeforeUpdateRequest"]++ + return nil + }) + + t.OnCollectionAfterUpdateRequest().Add(func(e *core.CollectionUpdateEvent) error { + t.EventCalls["OnCollectionAfterUpdateRequest"]++ + return nil + }) + + t.OnCollectionBeforeDeleteRequest().Add(func(e *core.CollectionDeleteEvent) error { + t.EventCalls["OnCollectionBeforeDeleteRequest"]++ + return nil + }) + + t.OnCollectionAfterDeleteRequest().Add(func(e *core.CollectionDeleteEvent) error { + t.EventCalls["OnCollectionAfterDeleteRequest"]++ + return nil + }) + + t.OnAdminsListRequest().Add(func(e *core.AdminsListEvent) error { + t.EventCalls["OnAdminsListRequest"]++ + return nil + }) + + t.OnAdminViewRequest().Add(func(e *core.AdminViewEvent) error { + t.EventCalls["OnAdminViewRequest"]++ + return nil + }) + + t.OnAdminBeforeCreateRequest().Add(func(e *core.AdminCreateEvent) error { + t.EventCalls["OnAdminBeforeCreateRequest"]++ + return nil + }) + + t.OnAdminAfterCreateRequest().Add(func(e *core.AdminCreateEvent) error { + t.EventCalls["OnAdminAfterCreateRequest"]++ + return nil + }) + + t.OnAdminBeforeUpdateRequest().Add(func(e *core.AdminUpdateEvent) error { + t.EventCalls["OnAdminBeforeUpdateRequest"]++ + return nil + }) + + t.OnAdminAfterUpdateRequest().Add(func(e *core.AdminUpdateEvent) error { + t.EventCalls["OnAdminAfterUpdateRequest"]++ + return nil + }) + + t.OnAdminBeforeDeleteRequest().Add(func(e *core.AdminDeleteEvent) error { + t.EventCalls["OnAdminBeforeDeleteRequest"]++ + return nil + }) + + t.OnAdminAfterDeleteRequest().Add(func(e *core.AdminDeleteEvent) error { + t.EventCalls["OnAdminAfterDeleteRequest"]++ + return nil + }) + + t.OnAdminAuthRequest().Add(func(e *core.AdminAuthEvent) error { + t.EventCalls["OnAdminAuthRequest"]++ + return nil + }) + + t.OnFileDownloadRequest().Add(func(e *core.FileDownloadEvent) error { + t.EventCalls["OnFileDownloadRequest"]++ + return nil + }) + + return t, nil +} + +// NewTempDataDir creates a new temporary directory copy of the test data. +// +// It is the caller's responsibility to call `os.RemoveAll(dir)` +// when the directory is no longer needed. +func NewTempDataDir() (string, error) { + tempDir, err := os.MkdirTemp("", "pb_test_*") + if err != nil { + return "", err + } + + _, currentFile, _, _ := runtime.Caller(0) + testDataDir := filepath.Join(path.Dir(currentFile), "data") + + // copy everything from testDataDir to tempDir + if err := copyDir(testDataDir, tempDir); err != nil { + return "", err + } + + return tempDir, nil +} + +// ------------------------------------------------------------------- +// Helpers +// ------------------------------------------------------------------- + +func copyDir(src string, dest string) error { + if err := os.MkdirAll(dest, os.ModePerm); err != nil { + return err + } + + sourceDir, err := os.Open(src) + if err != nil { + return err + } + defer sourceDir.Close() + + items, err := sourceDir.Readdir(-1) + if err != nil { + return err + } + + for _, item := range items { + fullSrcPath := filepath.Join(src, item.Name()) + fullDestPath := filepath.Join(dest, item.Name()) + + var copyErr error + if item.IsDir() { + copyErr = copyDir(fullSrcPath, fullDestPath) + } else { + copyErr = copyFile(fullSrcPath, fullDestPath) + } + + if copyErr != nil { + return copyErr + } + } + + return nil +} + +func copyFile(src string, dest string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + destFile, err := os.Create(dest) + if err != nil { + return err + } + defer destFile.Close() + + if _, err := io.Copy(destFile, srcFile); err != nil { + return err + } + + return nil +} diff --git a/tests/data/.gitignore b/tests/data/.gitignore new file mode 100644 index 00000000..b7eca6ed --- /dev/null +++ b/tests/data/.gitignore @@ -0,0 +1,2 @@ +*.db-shm +*.db-wal diff --git a/tests/data/data.db b/tests/data/data.db new file mode 100644 index 0000000000000000000000000000000000000000..a8513f945a83514692c2fa34cad3595478e754db GIT binary patch literal 159744 zcmeI5Yiu0Xz1VlT6e&_9H?l3S6f|dY-lL@Q-41Gk0*XT z@t-ID^ytgui{meh{A~2^Mt^!l9=S7oEB3p@-wr*K_&D(~^YEySdncb6P33a&`zJhN z6{+k>hGDn{FX*bnE2c|%(-BOb5}lf=CFVsl&GhB@n{eAJ zctyI$mDPa0muzs^gxD2M_f zyk&q!DxzcZ#54rnG*wx#i9tk7&x=u9olO2Nc9a#S)f4+g7)jP*6j#sgzV~!ki-<*a zL((+fbrdLJgc9D=B!@R_L6i+*tCD1eMI6QbGu>Jg#Y;GfK#QXI>e8Zn%JHD;xa@0D z6#r4&Jk?!wMDfCsMK*~?YW*sJFHotJN$G*>lt!J_F16~aR#kTT^z`ae`zcNN zuAbiYrbbgIPsZ<`_vMqXk{T%mf0NyegPTBcd<+i1hE21>uZ6?aZ18-Y?%eJ?&t1Fp zZf?F)LkDg)NwERw(&={=p^9o{pDQf#a{;ZyXyaSBgbydxc6l-#MMwgSPXemoMNMfVjR>_h^an zOQ+|qU%xUx+ktp-_R3-p??6+1_d%Hb#ZtG|Xo=kR-E~W5d8J1##X{X%Xt0*Cna-9N z{g${X=K|e2$W0*Mz*Sen9ttOr?fz~YTBIJ`-%VSW_NF=9iP)n~`#AMAN}U`@U3zJt zMs*~dmqLm#rA|ckCWgFCesXMdG?hxlKWhY9#_Ewg`*U=UX0dyHbqey+J_hL5Cao3R z0_$mYACYtoG^kMA>l4E9US*w@FSqKAPMrIAsginqt5S2`CiS&&WWJYne=rbb zk$|SsI`tN5*_-v+&V9Ent?ktS=IuS7giTm6ZuvgITUf_BS=m!YdSh!>UtvFRJ}5R{ z_ol~3Qm>~5>YlG0vacS;rpFF;D)~;wr;i@&RP>#W-G4sew<4b&57d!3r9wIH{~g|= zkp9iV8tLb=w>4=s+-)kCt}V^KHNVhqG=#lvOOO8fAxBiaecbkFobUD^Rr4JU*0COC zIlz67mJD+5tH+swFvcAx}_v7=}fCb`_{Id z$ns(+P3(Ohj;~Km!rw&l-^G%@O#T%9;eiB@01`j~NB{{S0VIF~kN^@u0!RP}JZ1tf zBog|tsHqBk{nIov@6oy^N^)kkV*T6y{@lr-gvg!>hM)?%nRjTZ!XE8T{KJ!R|B)h? zh5?_l^M!K3Yd#u>JSCF100|%gB!C2v01`j~NB{{S z0VIF~_93wQ?V<5p{M^Rwk;$1E*)?QSw{2ds6v7ipvv|c3L|%~`*_It$w`^mGeYuw2 zq}!xaEz*o#DXpI0bT5nQb!jJm;hp+ofv#W3&COqMZd7K?ynO4{9i#g8n(ba9m+d*@ z=B2lHN-s;~Wl?z9xuC6XzAbIpH?P)jctz#1GuK$U-jKE~zNN3$=$d=+`l5W{u5Id)6$#=~X6$+9dNxY!)f|wTNtRQ41C8HZ=__o@-4WG+*Z^g%RNBJOoELVQ^SU!WC z`FU*S%+DpuT{v@o4mwHSC$G(HK7`CEykw*WJ*&!DNy|Vkg3R^YX5asZl0S+iKZief zAOR$R1dsp{Kmter2_OL^fCP{L5PzyAN^ zH)F|TQ)l5H9!LNQAOR$R1dsp{Kmter2_S(#Rs#3N6UTC6Y-;-q9BksZ?8pwUSw!Mx zLw0#fkqFP`v@4FJE23bt53F){_BTvcXK!tn1uyH*d{u%WI8u?_XTn z2+d&^#H=X7H1^KyZ-2&n-`jGU-4^z`?!FE4{Ey|1`t$r@qJJyX0SupmPec)0m;xAG z^ZX&>{dhcT7*hZ2b>H5t#X;81dHyqO(Spu82L5b+f1-b|f`KA|9{92ji~3d|!-#qO zazx!D)9y?M>;Hf1&;L(;`o~(#G2ci42_OL^fCP{L5bjnszfxudD? zyy6EwGvkVqD^pA3Ez=Nq#d6_HqzW%<4iywtunmhkv{X@+Kji8=b&r;~tV@b@dX_7b z66La7iIjJ0w8)+1yqzk$0T;w8RLXFtMmHLT8g<%!8s)-90~|Gjs8pH5I+y(rg4tfA zMQVFo)-=y@cBRNJ;OQms%|%Hj#AlPbO&odw0`gk1eDu-TK}oNa>BCLA_~j+sEfi^o zdGqSQctffg{lZ;9rb>!r7y{fWl;{!&6oj+90MU0!o=0nNljHVLmNEdK zwNA9{>rTW?t!~BKN6A^#0d6i^ZWN3A#Aj71p+eQXiXPWKA=45mgo18eMh}~#+cvFw z7b*}CE4?fyXSqtbxYNG13~t(c%5p3Fq^3~|r6ss{Oet}QM?Fycj*QtIxH?37N=(8W z_<<62fI`)S>aJaHwyzHpU8mLrjou{1hF@Sg0x7WI*M?tbU;KCKeofu1yPH?hQ?QR< zA7c$Qu*MIyP5ZT0-8D#`a@@h*Tjk3x)S!lI@Pe*7ykbHnXF7t(Q=(H-6=4Tphl%Bo zZB?Tlu6&wT(d86wUs;k5*HAwEHXgKfPZR%O*qEgD(Ls=RzL{4*{)CADSZDRRkyLS6@xDM&k7+(S56DAnIvRVY8c6HKS{C`?kS$ z|5nYcU}j9y_(xC2Cv#(`zC)XRRAN!xkTi{V9R&&$p@cUz$>9xK5M_hdsw7!WZ1qr| zfq`iEQdxn4o@84GdJ0hBy47?CSK&@WcZu&VKFsw#O6Yf#+cEcaSEBVAf;QG~Rf5xK zy%w@bJQC`)Jlc*TS16HHdhSkj6-c+vY3HI^?wr>*SI=yhSlb^sTrIdlQ z+RO5{_8w5gjHrsPt(lSzn_t*Og}nwu4K~j*smrUHXu)P$wrg3U2V01=chr!?v|zF= zxKt^l>$<7iw(Q6buUSOmWkYs(OOXh#TehJ%5V9!PkmANnZgM7fv?md&n2|Nv5oK2s zh{CJ7rox5?w$7VWbabX$;o>|1|meVe|*x zA^{|T1dsp{Kmter2_OL^fCP{L51c}BVkpL1v0!RP}AOR$R1dsp{KmthM2_f*^ncPb= zEA976BP|$N0W8^-j3NoTF50#!faSfV7`(z}0}x6@o>-DB5lwP5*=1YKr5#$N9_~}b!~~ahzr3OuFhMArSpO*XtF_Eh~6w#YP8hy4`%tIoRtkWApvGB>>e5N!|!Ak z!WXito>jDrVoHW$5YeH8R~-k!hs^Or5F8$IpenirQM-`Gx+k{6XD{K9I5jKj8C}o} zmujK|(-br!QC@LHkvDWp<{g8`qApmjqqrGw+uJ=7@0SjiP9b9$nhMhd98HJViEZ(U zphJ#zhcH}%K&d3ce26Z?|8_cLF(bjO2*VI92WDyrnx*gx%aP%jIk^_g$#E4ke9_Is9Z#;t(ayPDAznVD9Do$HDT*44IU>ZYo>j`?99yKq&@D$I!9So6GUJ$vq4D9m%d z{k9-MpeiqbnYxq}1o+7ag0RdH?ks09ly3{gwQ`^5po6AgY=qfk`;kbe0-P zieZYVV~IeGja8BH_% zJcY?=vMmwI5Ww1>Fhh1Q`6sYEKFk0R6r#(bs~c|ozxv*~Wzi0Dd8OPa7NdOfj>Y1d zFW(a_`L=?*cXH^d?)2}T$+aE^rt|Ii9!eGrHmg8XnDk9ii=KsLE=op0H==ym=T@S) zV&DIVl0RbW|KsZaq1pmZkpL1v0!RP}AOR$R1dsp{Kmter2|PvuY@$GvK^5QrKgzh( z?m$doL`uqU7AlX&CXq!Pv4H><#zhD+jfl-k2<&0OYe3Bxulzik1xz8a8lo?d$e@hcQkkX`jz?FYi-CEXRj>IcQ^+g%A^EL zgToIqckR-zzQk<&}`^f-sAPy0_3^ywlxc_T?M6*(}hlgWLq9 z16N(@5(+1f{r+x(1zYxa)7HzqX%4Gqko2~??cO>X#0VaLEe0Yxc`mBXzKar`vMw& zA&%<;J=*R_gwXZLk<{f^;xWHXu5T0z9?g@6SMjg&;ifS!g|DXciO3$su*b=XKaKx3 zLc-9=9}_9%ZOigT&ye1sp4;#_PxKa%`i?6W5T>254OntJJ_`0iUl*QHcg zt@+)odcJcy)YXN#9eM(Ojaf@PW*7FKJ@m)Q^(ea zPL2PMv41sIJNom{-^PDE`d{LI7XPy+0Q!DO7@8V-Hul&1x%dNm2BMP=mmU7aeM?!D z)JUlgiV~c>I{o}e>h;th-D1mr%6V8X6aIZ;`niLgN`X`9bzbTvM```kB2Q>9dV*YsbHT~Tsn zYH7S>f-$OMxo{>@g_kvl3W_S&hDF0e*sSSMxF(28*DlO|kiLD`Fm9(|K-h1@6oA)m zrlzOAaZpRC1TCfV;yvM%-%@_^M!Thi#hf8&>24VX$qhv^kPNh!{hPcfp9{Es8id!5=n^j^O1k!SBBObzhqIPPe5gGs(%PdZpRr^8uv! z;9rG+;K?vS$rc4zZ=AqN>+n%b5gn5!tPq)|Dl0aD1=BTsFG1;rZi0smqeD>YCiv{L zkhBx=UHySROflH0?vx$&+g2xKT?spVVk9+lV!w`ZP^6#THR03di4*aAxwa0lRCRwg z^ec^U_+P35Ko!xqxK8hW<7uFOD!%)MPoG#+HzZBtVO4whWJ+L>c@y>n;0;?4VKIJN zh4uQwiczgq{G$F$w*(FwOdx?)U|~wr=bjo#oj)~53N0^rv32(Jz1Z>5)TvYP`-{Fb zTK8N3pXx4`t=kYM7kp3X(=l$>^M{ui$G$rD3o_En1@z%BIemO{1U{>7Lb!e3pQZ!* z|DVK?zfAt*fx92*5eXmxB!C2v01`j~NB{{S0VIF~kN^_+atQRU4cK1OplAI*SeK%; z*2N^GE}s17G5Em)2_OL^fCP{L5HXJf}Mjb|snKlS$H|9otz z#E6jq5+ivi=d!I(Npgr}5VO@Yl~lHEwq#~SH6ux|ZMJ1tL^WL4g-L=f*A>C!y18>%G;tL^NIqghkf5|o@hjN zVDEC5*u)QYg{|KvEw9|0iBB*bv6bDK)Nb2yx5-1us#-?WbwO2J(*;vqfe3_GC~Prh z=$6P^w(h!;CCeI-LOcXBtEgF3h0WFV?NWe+XY;4MYbcu~<@$hBZew$fx`X|ig|rpjvuY$C6iR0Rpi zDv%}`1ul-|$}CXMV}$)f7WqAUU#7d)sk;X#NZ59?#vf+h(%b!^|mS&pib=CEYg zD%<{Dv@M>OV;we3|+H&N&rNdo~!DU;laDXSnW zN#$Kt1RHY){usKe@UR&g6ckw@hGY85=Ie#L``(dpNc8TRT&!QJwTvbUQOa{qs%0Ug z3d%AUCCgB5qh&5iFG}F#ARJltkbhx!Qryp zmPOXVex?fcBmN$YSNeE+>Q-r$C8t>e8nsZ`f{VwJvc^tDwKu9}CuI)ts8@ikQrkMh?!eU{ z(&raX4*d9qla7ceV0bC?>XvZzrbaRrHt`@mAi0 zBDz?(3stBp!IHKHR2wA_?GjYG49E)PXbma^|9oH?@2n{WM_daPtza|qPAY4(NLbYj zcRDHUuFv{Z;av%}D@7)E2n4p-&2mvTj)vH5Qn!giFMzN?MT2%?U_R<4QmM!~6Mm}@ z!mhrZTr`5KP7V%kg}SqFn8a*BvD+>eE1kr&cYklISzoM&NfapmL*dFqIS^~1q*mmT z9ID$O-T9_uI&}MH<}v6Wz)4jEudPqD0V82Jc zUMZ}J#f|L_6jlsAy%C@E<9UM}Gc%7W9;o5lbzD!S+pPw{Rk$0xUzN*T53Hw*qUDJ@ z_Esm)ek;&U$s>6Q8PM%9TurZ+EVq-E=8pI6)I8dTpDUEeDm{0nx(X?1ozu<*n~>6a zP~TiVvt44&oHqLDFWp8|b>4r2NYBY0e)7o?@_0YIRq*2+!3?IVy@t)zs zN0AY_rm4ldN_R#c*BH${pK$7hVq?3qXTX-Fvkszbei-;dZ|%X@))yYG_qOtDPRUQz zL;c!%_@OtK==D1_ve7AS?Bkg~itw_}D+GJ@8B=uVaV_y4hl82sRY1dsp{Kmter2_OL^@Bjox{{25W6Ccf;`WG+4+n^3B zoGA({eJer+NUGIq1gx8~+^p>z zIl+w_7-MC*HP5SN&z&n)Y*JjS)V-`Bm;&@&>$L39TGP{Fqgt)hn0-=kd)9GkvnJ&+aR*bbSHRNr*s`?!3J1UMp{z8vM!^R`!PErC$B&~WNMEYGk=^{@?h>h8)~~wfFUrc| zoayQF>-yaLvzyesYP{txzF%0^%IAx-?`ZU9W8K@xbw_6SF)_92pBZaMI{)7MJFE2O z`3vRRoT^vv=AG^Lz0wE8`Lexn(JZ(dx%IlJZP{A2bd7AY@8$P)j!yIXH_<4-x1YqSme;XT%KLsS-ZM+_tLwBnVN;->f|M)vAxLF zE~>Ta-MRd&_iDn0jm8xaMBS_uZrw1?zkA_odGUQs(@Iw>hD}%9)w%kdG7wp~!1l&- zapSEmC3k&iRc(BbugSG-abYWWXMXYRD{n0v!hWuIy-n z5@g6PQ%pKtnjBa>9Bg{pG=46WTkx;Y@+u3<01aEMR91_$C+z0n4=ENaTMJM;>RzCn zP^_Ad>|%kIJ>LXRl$cdwaAUDm2H+RrV*zXrtwa0lmn4=H z2(Czag9~u>%W?1|lwl9d!SX>ceKW488W^$$ANmzu0`n<7t14MR%78Ic>-)a|7CjnR zT4eN(Xjsz*(aR^IWrqU{{Gg+f5I_I{1Q0*~fsaz4_$p(4lmlZ>A4b5+WJ`8g*Sua^J{vymsBg+n zYZUaS&mG5?Pe*bX3>|B}y7K&sbgon`>wl+%){~B$Z%y99uGe-&*qZjkrnuMGzf)}-ire<#m085d??fTfh#U4#)d#hvs8mG#Zd24B)M{a5 zR|Y5jS-yDWu?!Ny!=M$6hSSXZ^`?E(Zip!Ju4%rRS~;AAqj)3r#3Yg28$?4E@o;PR zn;ldAS&3bIc_e-5Pxg!Nl877)rC&Xky}&&yQh~I>f&6@Mw?03>%I|+9vi|DJRv@Q( zXTATReOBb;%o)X3+V*=_=D4t$D}7(q$G4~D78aWmj> z@wRZ$@5PI582Cql*OgnBHV?NqyW5*>v3ct|(^Si)E5eNs45OLLmW;AKEB+|GH;9d4 z_0%!Uf)elSs+uq!WwWL2vi@7_7$>zA%bPnjZxoiyJS|ScUCEZLvi@f*_pHie*|Vo+ zUj!!ehf~EEYkIDwxV_@&yoeyiHp z+TGhTUZ3xaIVW0!LC2GA5zWkKq8oduY-xYvt)EegVW>~3#gnuu!_`SC7F5@ZFEq8# z5I_I{1Q0*~0R#|0009ILK;S$Je6FXhjP>TOn+_`h*%9Y2DA78t6 z?c&9Y_wL<$Vz@w`fq{j~)5S5Q;?~<4uQ$z65NP35c+IQ7`uD%}QjrIz^|YJ~(+XYr z>iuy|{%&Xf%L-9~P8^CY0)&Y__s;}pZ_Qn9nHgJUoRux-d}-CJb$5SX+q`O@lj+wL zsW%=bu95TIpQ1V?^0xd$!SWTyZ{D&j$;-*fb^XG$Bfh2d?#*8TlANw-f8R;{o9Alj zbi4fXj-%K9y;yp`e*cM->1_v(tvJ_vJfZ!Wmh!>_kA7cmUUlV7{uM4|sr>m{zl9rS z%I~ZJbZJ&3=npEj)hr3?d zeYw4-#pd+up8+SMLK3&7aq#p7OR?QEbmWNWf7$XX{+QgXf0FA1JM4J4PV)hM?&3O6 z(X4IF-g5%dk+->Ae3@Aszsj1;*yX3S{par@p;HU0#a~%3`?)o-Q)i|2cCDi>fmuCY zCpi?(&uUp}BxIM`vF-gLxi#4p3Z{)F*FSGwyP*Fv)8RuKB{}66-I}rOpvu8aPXi0z z6PMrJu%7+sb2itvcJ_$}*S7GlJiq*_#$t;c4fY$k4k8b(mK^nzHh)vMva?9YF8R=v zgx9PoY2t7=SmHf#2zjZ-Yub0>D*nA74PU|P20y-?#$w`hZD*@bI`bV^wFGC|eVfA!zi69Uy?B}=-oGZhsnt<>uXt|n|Ec2EDk7CXFUQPS^nQE%7q>#= z{j+DRxKnp~{kHfew|9Lpn;mwW<*9d>yURkGs=s%m)`XURzTg%o$!Vz|&7Su2LR-AJ z4wucp_usB+{+9`hwfMqRa_rW54~^@|lg#EnwY=_h*f&Qc*mACMc<)kOt?sv)%8Kn3 z&O%BPU#evpobcsdd%kC^}pI$eCFL}BU=bHEa%kBzC2WvGFkyD{cPS`(I-*s8pNb@P{ RHDDRT;OXk;vd$@?2>{;1Keqq? literal 0 HcmV?d00001 diff --git a/tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png.attrs b/tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png.attrs new file mode 100644 index 00000000..6d00eb88 --- /dev/null +++ b/tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/4881bdef-06b4-4dea-8d97-6125ad242677.png.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"n/jfHKrjRJEIh1wHLtCjQw=="} diff --git a/tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/100x100_4881bdef-06b4-4dea-8d97-6125ad242677.png b/tests/data/storage/3f2888f8-075d-49fe-9d09-ea7e951000dc/577bd676-aacb-4072-b7da-99d00ee210a4/thumbs_4881bdef-06b4-4dea-8d97-6125ad242677.png/100x100_4881bdef-06b4-4dea-8d97-6125ad242677.png new file mode 100644 index 0000000000000000000000000000000000000000..d727b11ecffd45efff46030ddb251ac3e8267ab6 GIT binary patch literal 2170 zcmc&${W}v18y=z6t1OqZmNRTB2Te`y(m0ZoYDCh?$BBc{u`nddG=v=vTT(F^O_Eh- zK0+w+k!6deg_yPIW3kqZG!EIn48xNy-wM0pMZ!#`SPrp&zj@?+Q`_=WG)yR|U_3R$ZTIr-E~(__`EGxwnz z{g3haN*vitZKJrBa2s!ln^lKGWo%kcDgw9EVO9jVT3xA!}8tMe50;>`HKIF^hStNi` zEJ3#=$UB%UJS30Av@s5ts*$ha&E~K7yC$}V1_cFCK-}^M-Mjio$ZEG)M)E8EbhDI1 zk9oGT$%{R9&fU7_=~K_#sHQI@W`Y>`qoZN%fe$Jz{&AK7`vBX~-QAY(Gu?zn+0!+=%&nFAW9=|sVTSM!y zjs;No-4gzEcw^85?DOsAE#Zx8b zs|&D4RSek|!oHW4l}(>_tr9r#&xdUu59^$K)jT^lw-aOTBU37s8qGfVT~WOCC zU^S1&8;-T`-o%B#@5)XYE#T- zR0)D_c_mLu`oKsbFlLg_@hHoO>x*n@k~Ea%kTm6jCq3rjdsk^&0*;mo1BG}Amyo8B zwtM_q5PZ}S4EWtW|Aglnp9c~&L~?Op1m%P=e|)7pnuA2}-xAt)?b_AR@EhG&KAluQ zNBk9N2#*c{OVvrvTPYD2J-hc?k{K+?!s7sY{n7i%CJAMrPb#gf$P;+NCl`k9h!(q4 zZUbFr;ggOm>QCq4$Pk-90?=BGX)i*2V)6_B<&)-7tir zKE^8<**g4iKTKXwY!tPoYwt4~O|Wn&yipj7I@_{kc5Bm{<0r2|*35Q}PHl{%o;=$U zQX9C%)C5?)4Kt(sA^vC(F{pfsF8Y|Z$y&gL1@cbZB%%vjny3E$5V@?;s^%3$l~$YS zQeibESu8eR=(3yMQ}F2=Q#vYpgmwIh+CLEeoo~B_aDhbnAx`W`Xcs5G?|ydt`61?& zY-c;mN*|Knr{9`y2dpXu`>l9Sy1M?vt}lb&N&-2^qPLtufwD4%DRrGnnK1E~A!WK) zct$@OeM7XYZjfvCvB!MMbJ(`OK&=}YtSxy4Wgp(x03&sQJ_4{n8k7BCzjlR#qz%VD zI`fCIUDcq>}l zKa*n;2|34@>DYpA;8HOvPOt?2ot1;+n)K;V<^S` z_eNt;^h>V>zg}<~I-9clre{+PK9oJ?wqx_){f|kH_46uSx#b{RTgrpl5_7MwOKxx@ zx~sd_HjLrrg~qS%~G#8-}3Kofn!Spj#Kk_ot>Sd5@_K>k9IY#MMOKi_1aYr@=AbXX?Lb3 z{WH~hT)yOX>y=13CzA~F8K8G|{D^<=!^wyVNjoTM+xy^=+T9tqJ1P zNpz9-fbCn3TOn+_`h*%9Y2DA78t6 z?c&9Y_wL<$Vz@w`fq{j~)5S5Q;?~<4uQ$z65NP35c+IQ7`uD%}QjrIz^|YJ~(+XYr z>iuy|{%&Xf%L-9~P8^CY0)&Y__s;}pZ_Qn9nHgJUoRux-d}-CJb$5SX+q`O@lj+wL zsW%=bu95TIpQ1V?^0xd$!SWTyZ{D&j$;-*fb^XG$Bfh2d?#*8TlANw-f8R;{o9Alj zbi4fXj-%K9y;yp`e*cM->1_v(tvJ_vJfZ!Wmh!>_kA7cmUUlV7{uM4|sr>m{zl9rS z%I~ZJbZJ&3=npEj)hr3?d zeYw4-#pd+up8+SMLK3&7aq#p7OR?QEbmWNWf7$XX{+QgXf0FA1JM4J4PV)hM?&3O6 z(X4IF-g5%dk+->Ae3@Aszsj1;*yX3S{par@p;HU0#a~%3`?)o-Q)i|2cCDi>fmuCY zCpi?(&uUp}BxIM`vF-gLxi#4p3Z{)F*FSGwyP*Fv)8RuKB{}66-I}rOpvu8aPXi0z z6PMrJu%7+sb2itvcJ_$}*S7GlJiq*_#$t;c4fY$k4k8b(mK^nzHh)vMva?9YF8R=v zgx9PoY2t7=SmHf#2zjZ-Yub0>D*nA74PU|P20y-?#$w`hZD*@bI`bV^wFGC|eVfA!zi69Uy?B}=-oGZhsnt<>uXt|n|Ec2EDk7CXFUQPS^nQE%7q>#= z{j+DRxKnp~{kHfew|9Lpn;mwW<*9d>yURkGs=s%m)`XURzTg%o$!Vz|&7Su2LR-AJ z4wucp_usB+{+9`hwfMqRa_rW54~^@|lg#EnwY=_h*f&Qc*mACMc<)kOt?sv)%8Kn3 z&O%BPU#evpobcsdd%kC^}pI$eCFL}BU=bHEa%kBzC2WvGFkyD{cPS`(I-*s8pNb@P{ RHDDRT;OXk;vd$@?2>{;1Keqq? literal 0 HcmV?d00001 diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/315d3131-c1f7-453a-8a91-f12c06207edc.png.attrs b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/315d3131-c1f7-453a-8a91-f12c06207edc.png.attrs new file mode 100644 index 00000000..6d00eb88 --- /dev/null +++ b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/315d3131-c1f7-453a-8a91-f12c06207edc.png.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"n/jfHKrjRJEIh1wHLtCjQw=="} diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt new file mode 100644 index 00000000..16b14f5d --- /dev/null +++ b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt @@ -0,0 +1 @@ +test file diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt.attrs b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt.attrs new file mode 100644 index 00000000..5fad56f7 --- /dev/null +++ b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/55aa6938-e53c-4b58-b446-146f3d80b2c4.txt.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"text/plain; charset=utf-8","user.metadata":null,"md5":"sFQDISxmvcjMxZf+32zV/g=="} diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/b635c395-6837-49e5-8535-b0a6ebfbdbf3.png b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/b635c395-6837-49e5-8535-b0a6ebfbdbf3.png new file mode 100644 index 0000000000000000000000000000000000000000..dc980c39ca5e224af2d51cfa940173886ead3c3a GIT binary patch literal 1169 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k8A#4*i(3PvoC178T+f_2^Z)<<^XJe1{rmUp z*RSv2zyJOF_sf?rA3l8e{rmS%pFTZ(`t-()8<#F!`uX$cn>TOn+_`h*%9Y2DA78t6 z?c&9Y_wL<$Vz@w`fq{j~)5S5Q;?~<4uQ$z65NP35c+IQ7`uD%}QjrIz^|YJ~(+XYr z>iuy|{%&Xf%L-9~P8^CY0)&Y__s;}pZ_Qn9nHgJUoRux-d}-CJb$5SX+q`O@lj+wL zsW%=bu95TIpQ1V?^0xd$!SWTyZ{D&j$;-*fb^XG$Bfh2d?#*8TlANw-f8R;{o9Alj zbi4fXj-%K9y;yp`e*cM->1_v(tvJ_vJfZ!Wmh!>_kA7cmUUlV7{uM4|sr>m{zl9rS z%I~ZJbZJ&3=npEj)hr3?d zeYw4-#pd+up8+SMLK3&7aq#p7OR?QEbmWNWf7$XX{+QgXf0FA1JM4J4PV)hM?&3O6 z(X4IF-g5%dk+->Ae3@Aszsj1;*yX3S{par@p;HU0#a~%3`?)o-Q)i|2cCDi>fmuCY zCpi?(&uUp}BxIM`vF-gLxi#4p3Z{)F*FSGwyP*Fv)8RuKB{}66-I}rOpvu8aPXi0z z6PMrJu%7+sb2itvcJ_$}*S7GlJiq*_#$t;c4fY$k4k8b(mK^nzHh)vMva?9YF8R=v zgx9PoY2t7=SmHf#2zjZ-Yub0>D*nA74PU|P20y-?#$w`hZD*@bI`bV^wFGC|eVfA!zi69Uy?B}=-oGZhsnt<>uXt|n|Ec2EDk7CXFUQPS^nQE%7q>#= z{j+DRxKnp~{kHfew|9Lpn;mwW<*9d>yURkGs=s%m)`XURzTg%o$!Vz|&7Su2LR-AJ z4wucp_usB+{+9`hwfMqRa_rW54~^@|lg#EnwY=_h*f&Qc*mACMc<)kOt?sv)%8Kn3 z&O%BPU#evpobcsdd%kC^}pI$eCFL}BU=bHEa%kBzC2WvGFkyD{cPS`(I-*s8pNb@P{ RHDDRT;OXk;vd$@?2>{;1Keqq? literal 0 HcmV?d00001 diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/b635c395-6837-49e5-8535-b0a6ebfbdbf3.png.attrs b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/b635c395-6837-49e5-8535-b0a6ebfbdbf3.png.attrs new file mode 100644 index 00000000..6d00eb88 --- /dev/null +++ b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/b635c395-6837-49e5-8535-b0a6ebfbdbf3.png.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"n/jfHKrjRJEIh1wHLtCjQw=="} diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/c2c58441-27f5-4574-96f8-6f79dae9ff4d.png b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/c2c58441-27f5-4574-96f8-6f79dae9ff4d.png new file mode 100644 index 0000000000000000000000000000000000000000..dc980c39ca5e224af2d51cfa940173886ead3c3a GIT binary patch literal 1169 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k8A#4*i(3PvoC178T+f_2^Z)<<^XJe1{rmUp z*RSv2zyJOF_sf?rA3l8e{rmS%pFTZ(`t-()8<#F!`uX$cn>TOn+_`h*%9Y2DA78t6 z?c&9Y_wL<$Vz@w`fq{j~)5S5Q;?~<4uQ$z65NP35c+IQ7`uD%}QjrIz^|YJ~(+XYr z>iuy|{%&Xf%L-9~P8^CY0)&Y__s;}pZ_Qn9nHgJUoRux-d}-CJb$5SX+q`O@lj+wL zsW%=bu95TIpQ1V?^0xd$!SWTyZ{D&j$;-*fb^XG$Bfh2d?#*8TlANw-f8R;{o9Alj zbi4fXj-%K9y;yp`e*cM->1_v(tvJ_vJfZ!Wmh!>_kA7cmUUlV7{uM4|sr>m{zl9rS z%I~ZJbZJ&3=npEj)hr3?d zeYw4-#pd+up8+SMLK3&7aq#p7OR?QEbmWNWf7$XX{+QgXf0FA1JM4J4PV)hM?&3O6 z(X4IF-g5%dk+->Ae3@Aszsj1;*yX3S{par@p;HU0#a~%3`?)o-Q)i|2cCDi>fmuCY zCpi?(&uUp}BxIM`vF-gLxi#4p3Z{)F*FSGwyP*Fv)8RuKB{}66-I}rOpvu8aPXi0z z6PMrJu%7+sb2itvcJ_$}*S7GlJiq*_#$t;c4fY$k4k8b(mK^nzHh)vMva?9YF8R=v zgx9PoY2t7=SmHf#2zjZ-Yub0>D*nA74PU|P20y-?#$w`hZD*@bI`bV^wFGC|eVfA!zi69Uy?B}=-oGZhsnt<>uXt|n|Ec2EDk7CXFUQPS^nQE%7q>#= z{j+DRxKnp~{kHfew|9Lpn;mwW<*9d>yURkGs=s%m)`XURzTg%o$!Vz|&7Su2LR-AJ z4wucp_usB+{+9`hwfMqRa_rW54~^@|lg#EnwY=_h*f&Qc*mACMc<)kOt?sv)%8Kn3 z&O%BPU#evpobcsdd%kC^}pI$eCFL}BU=bHEa%kBzC2WvGFkyD{cPS`(I-*s8pNb@P{ RHDDRT;OXk;vd$@?2>{;1Keqq? literal 0 HcmV?d00001 diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/c2c58441-27f5-4574-96f8-6f79dae9ff4d.png.attrs b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/c2c58441-27f5-4574-96f8-6f79dae9ff4d.png.attrs new file mode 100644 index 00000000..6d00eb88 --- /dev/null +++ b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/c2c58441-27f5-4574-96f8-6f79dae9ff4d.png.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"n/jfHKrjRJEIh1wHLtCjQw=="} diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/e526d938-c5ab-41cb-a334-85b9c3e37f72.png b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/e526d938-c5ab-41cb-a334-85b9c3e37f72.png new file mode 100644 index 0000000000000000000000000000000000000000..dc980c39ca5e224af2d51cfa940173886ead3c3a GIT binary patch literal 1169 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k8A#4*i(3PvoC178T+f_2^Z)<<^XJe1{rmUp z*RSv2zyJOF_sf?rA3l8e{rmS%pFTZ(`t-()8<#F!`uX$cn>TOn+_`h*%9Y2DA78t6 z?c&9Y_wL<$Vz@w`fq{j~)5S5Q;?~<4uQ$z65NP35c+IQ7`uD%}QjrIz^|YJ~(+XYr z>iuy|{%&Xf%L-9~P8^CY0)&Y__s;}pZ_Qn9nHgJUoRux-d}-CJb$5SX+q`O@lj+wL zsW%=bu95TIpQ1V?^0xd$!SWTyZ{D&j$;-*fb^XG$Bfh2d?#*8TlANw-f8R;{o9Alj zbi4fXj-%K9y;yp`e*cM->1_v(tvJ_vJfZ!Wmh!>_kA7cmUUlV7{uM4|sr>m{zl9rS z%I~ZJbZJ&3=npEj)hr3?d zeYw4-#pd+up8+SMLK3&7aq#p7OR?QEbmWNWf7$XX{+QgXf0FA1JM4J4PV)hM?&3O6 z(X4IF-g5%dk+->Ae3@Aszsj1;*yX3S{par@p;HU0#a~%3`?)o-Q)i|2cCDi>fmuCY zCpi?(&uUp}BxIM`vF-gLxi#4p3Z{)F*FSGwyP*Fv)8RuKB{}66-I}rOpvu8aPXi0z z6PMrJu%7+sb2itvcJ_$}*S7GlJiq*_#$t;c4fY$k4k8b(mK^nzHh)vMva?9YF8R=v zgx9PoY2t7=SmHf#2zjZ-Yub0>D*nA74PU|P20y-?#$w`hZD*@bI`bV^wFGC|eVfA!zi69Uy?B}=-oGZhsnt<>uXt|n|Ec2EDk7CXFUQPS^nQE%7q>#= z{j+DRxKnp~{kHfew|9Lpn;mwW<*9d>yURkGs=s%m)`XURzTg%o$!Vz|&7Su2LR-AJ z4wucp_usB+{+9`hwfMqRa_rW54~^@|lg#EnwY=_h*f&Qc*mACMc<)kOt?sv)%8Kn3 z&O%BPU#evpobcsdd%kC^}pI$eCFL}BU=bHEa%kBzC2WvGFkyD{cPS`(I-*s8pNb@P{ RHDDRT;OXk;vd$@?2>{;1Keqq? literal 0 HcmV?d00001 diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/e526d938-c5ab-41cb-a334-85b9c3e37f72.png.attrs b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/e526d938-c5ab-41cb-a334-85b9c3e37f72.png.attrs new file mode 100644 index 00000000..6d00eb88 --- /dev/null +++ b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/e526d938-c5ab-41cb-a334-85b9c3e37f72.png.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"n/jfHKrjRJEIh1wHLtCjQw=="} diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_315d3131-c1f7-453a-8a91-f12c06207edc.png/100x100_315d3131-c1f7-453a-8a91-f12c06207edc.png b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/054f9f24-0a0a-4e09-87b1-bc7ff2b336a2/thumbs_315d3131-c1f7-453a-8a91-f12c06207edc.png/100x100_315d3131-c1f7-453a-8a91-f12c06207edc.png new file mode 100644 index 0000000000000000000000000000000000000000..d727b11ecffd45efff46030ddb251ac3e8267ab6 GIT binary patch literal 2170 zcmc&${W}v18y=z6t1OqZmNRTB2Te`y(m0ZoYDCh?$BBc{u`nddG=v=vTT(F^O_Eh- zK0+w+k!6deg_yPIW3kqZG!EIn48xNy-wM0pMZ!#`SPrp&zj@?+Q`_=WG)yR|U_3R$ZTIr-E~(__`EGxwnz z{g3haN*vitZKJrBa2s!ln^lKGWo%kcDgw9EVO9jVT3xA!}8tMe50;>`HKIF^hStNi` zEJ3#=$UB%UJS30Av@s5ts*$ha&E~K7yC$}V1_cFCK-}^M-Mjio$ZEG)M)E8EbhDI1 zk9oGT$%{R9&fU7_=~K_#sHQI@W`Y>`qoZN%fe$Jz{&AK7`vBX~-QAY(Gu?zn+0!+=%&nFAW9=|sVTSM!y zjs;No-4gzEcw^85?DOsAE#Zx8b zs|&D4RSek|!oHW4l}(>_tr9r#&xdUu59^$K)jT^lw-aOTBU37s8qGfVT~WOCC zU^S1&8;-T`-o%B#@5)XYE#T- zR0)D_c_mLu`oKsbFlLg_@hHoO>x*n@k~Ea%kTm6jCq3rjdsk^&0*;mo1BG}Amyo8B zwtM_q5PZ}S4EWtW|Aglnp9c~&L~?Op1m%P=e|)7pnuA2}-xAt)?b_AR@EhG&KAluQ zNBk9N2#*c{OVvrvTPYD2J-hc?k{K+?!s7sY{n7i%CJAMrPb#gf$P;+NCl`k9h!(q4 zZUbFr;ggOm>QCq4$Pk-90?=BGX)i*2V)6_B<&)-7tir zKE^8<**g4iKTKXwY!tPoYwt4~O|Wn&yipj7I@_{kc5Bm{<0r2|*35Q}PHl{%o;=$U zQX9C%)C5?)4Kt(sA^vC(F{pfsF8Y|Z$y&gL1@cbZB%%vjny3E$5V@?;s^%3$l~$YS zQeibESu8eR=(3yMQ}F2=Q#vYpgmwIh+CLEeoo~B_aDhbnAx`W`Xcs5G?|ydt`61?& zY-c;mN*|Knr{9`y2dpXu`>l9Sy1M?vt}lb&N&-2^qPLtufwD4%DRrGnnK1E~A!WK) zct$@OeM7XYZjfvCvB!MMbJ(`OK&=}YtSxy4Wgp(x03&sQJ_4{n8k7BCzjlR#qz%VD zI`fCIUDcq>}l zKa*n;2|34@>DYpA;8HOvPOt?2ot1;+n)K;V<^S` z_eNt;^h>V>zg}<~I-9clre{+PK9oJ?wqx_){f|kH_46uSx#b{RTgrpl5_7MwOKxx@ zx~sd_HjLrrg~qS%~G#8-}3Kofn!Spj#Kk_ot>Sd5@_K>k9IY#MMOKi_1aYr@=AbXX?Lb3 z{WH~hT)yOX>y=13CzA~F8K8G|{D^<=!^wyVNjoTM+xy^=+T9tqJ1P zNpz9-fbCn3n48xNy-wM0pMZ!#`SPrp&zj@?+Q`_=WG)yR|U_3R$ZTIr-E~(__`EGxwnz z{g3haN*vitZKJrBa2s!ln^lKGWo%kcDgw9EVO9jVT3xA!}8tMe50;>`HKIF^hStNi` zEJ3#=$UB%UJS30Av@s5ts*$ha&E~K7yC$}V1_cFCK-}^M-Mjio$ZEG)M)E8EbhDI1 zk9oGT$%{R9&fU7_=~K_#sHQI@W`Y>`qoZN%fe$Jz{&AK7`vBX~-QAY(Gu?zn+0!+=%&nFAW9=|sVTSM!y zjs;No-4gzEcw^85?DOsAE#Zx8b zs|&D4RSek|!oHW4l}(>_tr9r#&xdUu59^$K)jT^lw-aOTBU37s8qGfVT~WOCC zU^S1&8;-T`-o%B#@5)XYE#T- zR0)D_c_mLu`oKsbFlLg_@hHoO>x*n@k~Ea%kTm6jCq3rjdsk^&0*;mo1BG}Amyo8B zwtM_q5PZ}S4EWtW|Aglnp9c~&L~?Op1m%P=e|)7pnuA2}-xAt)?b_AR@EhG&KAluQ zNBk9N2#*c{OVvrvTPYD2J-hc?k{K+?!s7sY{n7i%CJAMrPb#gf$P;+NCl`k9h!(q4 zZUbFr;ggOm>QCq4$Pk-90?=BGX)i*2V)6_B<&)-7tir zKE^8<**g4iKTKXwY!tPoYwt4~O|Wn&yipj7I@_{kc5Bm{<0r2|*35Q}PHl{%o;=$U zQX9C%)C5?)4Kt(sA^vC(F{pfsF8Y|Z$y&gL1@cbZB%%vjny3E$5V@?;s^%3$l~$YS zQeibESu8eR=(3yMQ}F2=Q#vYpgmwIh+CLEeoo~B_aDhbnAx`W`Xcs5G?|ydt`61?& zY-c;mN*|Knr{9`y2dpXu`>l9Sy1M?vt}lb&N&-2^qPLtufwD4%DRrGnnK1E~A!WK) zct$@OeM7XYZjfvCvB!MMbJ(`OK&=}YtSxy4Wgp(x03&sQJ_4{n8k7BCzjlR#qz%VD zI`fCIUDcq>}l zKa*n;2|34@>DYpA;8HOvPOt?2ot1;+n)K;V<^S` z_eNt;^h>V>zg}<~I-9clre{+PK9oJ?wqx_){f|kH_46uSx#b{RTgrpl5_7MwOKxx@ zx~sd_HjLrrg~qS%~G#8-}3Kofn!Spj#Kk_ot>Sd5@_K>k9IY#MMOKi_1aYr@=AbXX?Lb3 z{WH~hT)yOX>y=13CzA~F8K8G|{D^<=!^wyVNjoTM+xy^=+T9tqJ1P zNpz9-fbCn3n48xNy-wM0pMZ!#`SPrp&zj@?+Q`_=WG)yR|U_3R$ZTIr-E~(__`EGxwnz z{g3haN*vitZKJrBa2s!ln^lKGWo%kcDgw9EVO9jVT3xA!}8tMe50;>`HKIF^hStNi` zEJ3#=$UB%UJS30Av@s5ts*$ha&E~K7yC$}V1_cFCK-}^M-Mjio$ZEG)M)E8EbhDI1 zk9oGT$%{R9&fU7_=~K_#sHQI@W`Y>`qoZN%fe$Jz{&AK7`vBX~-QAY(Gu?zn+0!+=%&nFAW9=|sVTSM!y zjs;No-4gzEcw^85?DOsAE#Zx8b zs|&D4RSek|!oHW4l}(>_tr9r#&xdUu59^$K)jT^lw-aOTBU37s8qGfVT~WOCC zU^S1&8;-T`-o%B#@5)XYE#T- zR0)D_c_mLu`oKsbFlLg_@hHoO>x*n@k~Ea%kTm6jCq3rjdsk^&0*;mo1BG}Amyo8B zwtM_q5PZ}S4EWtW|Aglnp9c~&L~?Op1m%P=e|)7pnuA2}-xAt)?b_AR@EhG&KAluQ zNBk9N2#*c{OVvrvTPYD2J-hc?k{K+?!s7sY{n7i%CJAMrPb#gf$P;+NCl`k9h!(q4 zZUbFr;ggOm>QCq4$Pk-90?=BGX)i*2V)6_B<&)-7tir zKE^8<**g4iKTKXwY!tPoYwt4~O|Wn&yipj7I@_{kc5Bm{<0r2|*35Q}PHl{%o;=$U zQX9C%)C5?)4Kt(sA^vC(F{pfsF8Y|Z$y&gL1@cbZB%%vjny3E$5V@?;s^%3$l~$YS zQeibESu8eR=(3yMQ}F2=Q#vYpgmwIh+CLEeoo~B_aDhbnAx`W`Xcs5G?|ydt`61?& zY-c;mN*|Knr{9`y2dpXu`>l9Sy1M?vt}lb&N&-2^qPLtufwD4%DRrGnnK1E~A!WK) zct$@OeM7XYZjfvCvB!MMbJ(`OK&=}YtSxy4Wgp(x03&sQJ_4{n8k7BCzjlR#qz%VD zI`fCIUDcq>}l zKa*n;2|34@>DYpA;8HOvPOt?2ot1;+n)K;V<^S` z_eNt;^h>V>zg}<~I-9clre{+PK9oJ?wqx_){f|kH_46uSx#b{RTgrpl5_7MwOKxx@ zx~sd_HjLrrg~qS%~G#8-}3Kofn!Spj#Kk_ot>Sd5@_K>k9IY#MMOKi_1aYr@=AbXX?Lb3 z{WH~hT)yOX>y=13CzA~F8K8G|{D^<=!^wyVNjoTM+xy^=+T9tqJ1P zNpz9-fbCn3n48xNy-wM0pMZ!#`SPrp&zj@?+Q`_=WG)yR|U_3R$ZTIr-E~(__`EGxwnz z{g3haN*vitZKJrBa2s!ln^lKGWo%kcDgw9EVO9jVT3xA!}8tMe50;>`HKIF^hStNi` zEJ3#=$UB%UJS30Av@s5ts*$ha&E~K7yC$}V1_cFCK-}^M-Mjio$ZEG)M)E8EbhDI1 zk9oGT$%{R9&fU7_=~K_#sHQI@W`Y>`qoZN%fe$Jz{&AK7`vBX~-QAY(Gu?zn+0!+=%&nFAW9=|sVTSM!y zjs;No-4gzEcw^85?DOsAE#Zx8b zs|&D4RSek|!oHW4l}(>_tr9r#&xdUu59^$K)jT^lw-aOTBU37s8qGfVT~WOCC zU^S1&8;-T`-o%B#@5)XYE#T- zR0)D_c_mLu`oKsbFlLg_@hHoO>x*n@k~Ea%kTm6jCq3rjdsk^&0*;mo1BG}Amyo8B zwtM_q5PZ}S4EWtW|Aglnp9c~&L~?Op1m%P=e|)7pnuA2}-xAt)?b_AR@EhG&KAluQ zNBk9N2#*c{OVvrvTPYD2J-hc?k{K+?!s7sY{n7i%CJAMrPb#gf$P;+NCl`k9h!(q4 zZUbFr;ggOm>QCq4$Pk-90?=BGX)i*2V)6_B<&)-7tir zKE^8<**g4iKTKXwY!tPoYwt4~O|Wn&yipj7I@_{kc5Bm{<0r2|*35Q}PHl{%o;=$U zQX9C%)C5?)4Kt(sA^vC(F{pfsF8Y|Z$y&gL1@cbZB%%vjny3E$5V@?;s^%3$l~$YS zQeibESu8eR=(3yMQ}F2=Q#vYpgmwIh+CLEeoo~B_aDhbnAx`W`Xcs5G?|ydt`61?& zY-c;mN*|Knr{9`y2dpXu`>l9Sy1M?vt}lb&N&-2^qPLtufwD4%DRrGnnK1E~A!WK) zct$@OeM7XYZjfvCvB!MMbJ(`OK&=}YtSxy4Wgp(x03&sQJ_4{n8k7BCzjlR#qz%VD zI`fCIUDcq>}l zKa*n;2|34@>DYpA;8HOvPOt?2ot1;+n)K;V<^S` z_eNt;^h>V>zg}<~I-9clre{+PK9oJ?wqx_){f|kH_46uSx#b{RTgrpl5_7MwOKxx@ zx~sd_HjLrrg~qS%~G#8-}3Kofn!Spj#Kk_ot>Sd5@_K>k9IY#MMOKi_1aYr@=AbXX?Lb3 z{WH~hT)yOX>y=13CzA~F8K8G|{D^<=!^wyVNjoTM+xy^=+T9tqJ1P zNpz9-fbCn3TOn+_`h*%9Y2DA78t6 z?c&9Y_wL<$Vz@w`fq{j~)5S5Q;?~<4uQ$z65NP35c+IQ7`uD%}QjrIz^|YJ~(+XYr z>iuy|{%&Xf%L-9~P8^CY0)&Y__s;}pZ_Qn9nHgJUoRux-d}-CJb$5SX+q`O@lj+wL zsW%=bu95TIpQ1V?^0xd$!SWTyZ{D&j$;-*fb^XG$Bfh2d?#*8TlANw-f8R;{o9Alj zbi4fXj-%K9y;yp`e*cM->1_v(tvJ_vJfZ!Wmh!>_kA7cmUUlV7{uM4|sr>m{zl9rS z%I~ZJbZJ&3=npEj)hr3?d zeYw4-#pd+up8+SMLK3&7aq#p7OR?QEbmWNWf7$XX{+QgXf0FA1JM4J4PV)hM?&3O6 z(X4IF-g5%dk+->Ae3@Aszsj1;*yX3S{par@p;HU0#a~%3`?)o-Q)i|2cCDi>fmuCY zCpi?(&uUp}BxIM`vF-gLxi#4p3Z{)F*FSGwyP*Fv)8RuKB{}66-I}rOpvu8aPXi0z z6PMrJu%7+sb2itvcJ_$}*S7GlJiq*_#$t;c4fY$k4k8b(mK^nzHh)vMva?9YF8R=v zgx9PoY2t7=SmHf#2zjZ-Yub0>D*nA74PU|P20y-?#$w`hZD*@bI`bV^wFGC|eVfA!zi69Uy?B}=-oGZhsnt<>uXt|n|Ec2EDk7CXFUQPS^nQE%7q>#= z{j+DRxKnp~{kHfew|9Lpn;mwW<*9d>yURkGs=s%m)`XURzTg%o$!Vz|&7Su2LR-AJ z4wucp_usB+{+9`hwfMqRa_rW54~^@|lg#EnwY=_h*f&Qc*mACMc<)kOt?sv)%8Kn3 z&O%BPU#evpobcsdd%kC^}pI$eCFL}BU=bHEa%kBzC2WvGFkyD{cPS`(I-*s8pNb@P{ RHDDRT;OXk;vd$@?2>{;1Keqq? literal 0 HcmV?d00001 diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/935a3325-f511-4d11-87f4-51034234a8d9.png.attrs b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/935a3325-f511-4d11-87f4-51034234a8d9.png.attrs new file mode 100644 index 00000000..6d00eb88 --- /dev/null +++ b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/935a3325-f511-4d11-87f4-51034234a8d9.png.attrs @@ -0,0 +1 @@ +{"user.cache_control":"","user.content_disposition":"","user.content_encoding":"","user.content_language":"","user.content_type":"image/png","user.metadata":null,"md5":"n/jfHKrjRJEIh1wHLtCjQw=="} diff --git a/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/thumbs_935a3325-f511-4d11-87f4-51034234a8d9.png/100x100_935a3325-f511-4d11-87f4-51034234a8d9.png b/tests/data/storage/f12f3eb6-b980-4bf6-b1e4-36de0450c8be/df55c8ff-45ef-4c82-8aed-6e2183fe1125/thumbs_935a3325-f511-4d11-87f4-51034234a8d9.png/100x100_935a3325-f511-4d11-87f4-51034234a8d9.png new file mode 100644 index 0000000000000000000000000000000000000000..d727b11ecffd45efff46030ddb251ac3e8267ab6 GIT binary patch literal 2170 zcmc&${W}v18y=z6t1OqZmNRTB2Te`y(m0ZoYDCh?$BBc{u`nddG=v=vTT(F^O_Eh- zK0+w+k!6deg_yPIW3kqZG!EIn48xNy-wM0pMZ!#`SPrp&zj@?+Q`_=WG)yR|U_3R$ZTIr-E~(__`EGxwnz z{g3haN*vitZKJrBa2s!ln^lKGWo%kcDgw9EVO9jVT3xA!}8tMe50;>`HKIF^hStNi` zEJ3#=$UB%UJS30Av@s5ts*$ha&E~K7yC$}V1_cFCK-}^M-Mjio$ZEG)M)E8EbhDI1 zk9oGT$%{R9&fU7_=~K_#sHQI@W`Y>`qoZN%fe$Jz{&AK7`vBX~-QAY(Gu?zn+0!+=%&nFAW9=|sVTSM!y zjs;No-4gzEcw^85?DOsAE#Zx8b zs|&D4RSek|!oHW4l}(>_tr9r#&xdUu59^$K)jT^lw-aOTBU37s8qGfVT~WOCC zU^S1&8;-T`-o%B#@5)XYE#T- zR0)D_c_mLu`oKsbFlLg_@hHoO>x*n@k~Ea%kTm6jCq3rjdsk^&0*;mo1BG}Amyo8B zwtM_q5PZ}S4EWtW|Aglnp9c~&L~?Op1m%P=e|)7pnuA2}-xAt)?b_AR@EhG&KAluQ zNBk9N2#*c{OVvrvTPYD2J-hc?k{K+?!s7sY{n7i%CJAMrPb#gf$P;+NCl`k9h!(q4 zZUbFr;ggOm>QCq4$Pk-90?=BGX)i*2V)6_B<&)-7tir zKE^8<**g4iKTKXwY!tPoYwt4~O|Wn&yipj7I@_{kc5Bm{<0r2|*35Q}PHl{%o;=$U zQX9C%)C5?)4Kt(sA^vC(F{pfsF8Y|Z$y&gL1@cbZB%%vjny3E$5V@?;s^%3$l~$YS zQeibESu8eR=(3yMQ}F2=Q#vYpgmwIh+CLEeoo~B_aDhbnAx`W`Xcs5G?|ydt`61?& zY-c;mN*|Knr{9`y2dpXu`>l9Sy1M?vt}lb&N&-2^qPLtufwD4%DRrGnnK1E~A!WK) zct$@OeM7XYZjfvCvB!MMbJ(`OK&=}YtSxy4Wgp(x03&sQJ_4{n8k7BCzjlR#qz%VD zI`fCIUDcq>}l zKa*n;2|34@>DYpA;8HOvPOt?2ot1;+n)K;V<^S` z_eNt;^h>V>zg}<~I-9clre{+PK9oJ?wqx_){f|kH_46uSx#b{RTgrpl5_7MwOKxx@ zx~sd_HjLrrg~qS%~G#8-}3Kofn!Spj#Kk_ot>Sd5@_K>k9IY#MMOKi_1aYr@=AbXX?Lb3 z{WH~hT)yOX>y=13CzA~F8K8G|{D^<=!^wyVNjoTM+xy^=+T9tqJ1P zNpz9-fbCn3 len(strings.Split(dirs[j], "/")) + }) + + // delete dirs + for _, d := range dirs { + if d != "" { + s.Delete(d) + } + } + // --- + + return failed +} + +// Serve serves the file at fileKey location to an HTTP response. +func (s *System) Serve(response http.ResponseWriter, fileKey string, name string) error { + r, readErr := s.bucket.NewReader(s.ctx, fileKey, nil) + if readErr != nil { + return readErr + } + defer r.Close() + + // All HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT) + // (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1) + location, _ := time.LoadLocation("GMT") + + response.Header().Set("Content-Disposition", "attachment; filename="+name) + response.Header().Set("Content-Type", r.ContentType()) + response.Header().Set("Content-Length", strconv.FormatInt(r.Size(), 10)) + response.Header().Set("Last-Modified", r.ModTime().In(location).Format("Mon, 02 Jan 06 15:04:05 MST")) + + // copy from the read range to response. + _, err := io.Copy(response, r) + + return err +} + +// CreateThumb creates a new thumb image for the file at originalKey location. +// The new thumb file is stored at thumbKey location. +// +// thumbSize is in the format "WxH", eg. "100x50". +func (s *System) CreateThumb(originalKey string, thumbKey, thumbSize string, cropCenter bool) error { + thumbSizeParts := strings.SplitN(thumbSize, "x", 2) + if len(thumbSizeParts) != 2 { + return errors.New("Thumb size must be in WxH format.") + } + + width, _ := strconv.Atoi(thumbSizeParts[0]) + height, _ := strconv.Atoi(thumbSizeParts[1]) + + // fetch the original + r, readErr := s.bucket.NewReader(s.ctx, originalKey, nil) + if readErr != nil { + return readErr + } + defer r.Close() + + // create imaging object from the origial reader + img, decodeErr := imaging.Decode(r) + if decodeErr != nil { + return decodeErr + } + + // determine crop anchor + cropAnchor := imaging.Center + if !cropCenter { + cropAnchor = imaging.Top + } + + // create thumb imaging object + thumbImg := imaging.Fill(img, width, height, cropAnchor, imaging.CatmullRom) + + // open a thumb storage writer (aka. prepare for upload) + w, writerErr := s.bucket.NewWriter(s.ctx, thumbKey, nil) + if writerErr != nil { + return writerErr + } + + // thumb encode (aka. upload) + if err := imaging.Encode(w, thumbImg, imaging.PNG); err != nil { + w.Close() + + return err + } + + // check for close errors to ensure that the thumb was really saved + return w.Close() +} diff --git a/tools/filesystem/filesystem_test.go b/tools/filesystem/filesystem_test.go new file mode 100644 index 00000000..79991172 --- /dev/null +++ b/tools/filesystem/filesystem_test.go @@ -0,0 +1,272 @@ +package filesystem_test + +import ( + "image" + "image/png" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/pocketbase/pocketbase/tools/filesystem" +) + +func TestFileSystemExists(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fs, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + scenarios := []struct { + file string + exists bool + }{ + {"sub1.txt", false}, + {"test/sub1.txt", true}, + {"test/sub2.txt", true}, + {"file.png", true}, + } + + for i, scenario := range scenarios { + exists, _ := fs.Exists(scenario.file) + + if exists != scenario.exists { + t.Errorf("(%d) Expected %v, got %v", i, scenario.exists, exists) + } + } +} + +func TestFileSystemAttributes(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fs, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + scenarios := []struct { + file string + expectError bool + }{ + {"sub1.txt", true}, + {"test/sub1.txt", false}, + {"test/sub2.txt", false}, + {"file.png", false}, + } + + for i, scenario := range scenarios { + attr, err := fs.Attributes(scenario.file) + + if err == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + + if err != nil && !scenario.expectError { + t.Errorf("(%d) Expected nil, got error, %v", i, err) + } + + if err == nil && attr.ContentType != "application/octet-stream" { + t.Errorf("(%d) Expected attr.ContentType to be %q, got %q", i, "application/octet-stream", attr.ContentType) + } + } +} + +func TestFileSystemDelete(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fs, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + if err := fs.Delete("missing.txt"); err == nil { + t.Fatal("Expected error, got nil") + } + + if err := fs.Delete("file.png"); err != nil { + t.Fatalf("Expected nil, got error %v", err) + } +} + +func TestFileSystemDeletePrefix(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fs, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + if errs := fs.DeletePrefix(""); len(errs) == 0 { + t.Fatal("Expected error, got nil", errs) + } + + if errs := fs.DeletePrefix("missing/"); len(errs) != 0 { + t.Fatalf("Not existing prefix shouldn't error, got %v", errs) + } + + if errs := fs.DeletePrefix("test"); len(errs) != 0 { + t.Fatalf("Expected nil, got errors %v", errs) + } + + // ensure that the test/ files are deleted + if exists, _ := fs.Exists("test/sub1.txt"); exists { + t.Fatalf("Expected test/sub1.txt to be deleted") + } + if exists, _ := fs.Exists("test/sub2.txt"); exists { + t.Fatalf("Expected test/sub2.txt to be deleted") + } +} + +func TestFileSystemUpload(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fs, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + uploadErr := fs.Upload([]byte("demo"), "newdir/newkey.txt") + if uploadErr != nil { + t.Fatal(uploadErr) + } + + if exists, _ := fs.Exists("newdir/newkey.txt"); !exists { + t.Fatalf("Expected newdir/newkey.txt to exist") + } +} + +func TestFileSystemServe(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fs, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + r := httptest.NewRecorder() + + // serve missing file + if err := fs.Serve(r, "missing.txt", "download.txt"); err == nil { + t.Fatal("Expected error, got nil") + } + + // serve existing file + if err := fs.Serve(r, "test/sub1.txt", "download.txt"); err != nil { + t.Fatal("Expected nil, got error") + } + + result := r.Result() + + // check headers + scenarios := []struct { + header string + expected string + }{ + {"Content-Disposition", "attachment; filename=download.txt"}, + {"Content-Type", "application/octet-stream"}, + {"Content-Length", "0"}, + } + for i, scenario := range scenarios { + v := result.Header.Get(scenario.header) + if v != scenario.expected { + t.Errorf("(%d) Expected value %q for header %q, got %q", i, scenario.expected, scenario.header, v) + } + } +} + +func TestFileSystemCreateThumb(t *testing.T) { + dir := createTestDir(t) + defer os.RemoveAll(dir) + + fs, err := filesystem.NewLocal(dir) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + scenarios := []struct { + file string + thumb string + cropCenter bool + expectError bool + }{ + // missing + {"missing.txt", "thumb_test_missing", true, true}, + // non-image existing file + {"test/sub1.txt", "thumb_test_sub1", true, true}, + // existing image file - crop center + {"file.png", "thumb_file_center", true, false}, + // existing image file - crop top + {"file.png", "thumb_file_top", false, false}, + // existing image file with existing thumb path = should fail + {"file.png", "test", true, true}, + } + + for i, scenario := range scenarios { + err := fs.CreateThumb(scenario.file, scenario.thumb, "100x100", scenario.cropCenter) + + hasErr := err != nil + if hasErr != scenario.expectError { + t.Errorf("(%d) Expected hasErr to be %v, got %v (%v)", i, scenario.expectError, hasErr, err) + continue + } + + if scenario.expectError { + continue + } + + if exists, _ := fs.Exists(scenario.thumb); !exists { + t.Errorf("(%d) Couldn't find %q thumb", i, scenario.thumb) + } + } +} + +// --- + +func createTestDir(t *testing.T) string { + dir, err := os.MkdirTemp(os.TempDir(), "pb_test") + if err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(filepath.Join(dir, "test"), os.ModePerm); err != nil { + t.Fatal(err) + } + + file1, err := os.OpenFile(filepath.Join(dir, "test/sub1.txt"), os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatal(err) + } + file1.Close() + + file2, err := os.OpenFile(filepath.Join(dir, "test/sub2.txt"), os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatal(err) + } + file2.Close() + + file3, err := os.OpenFile(filepath.Join(dir, "file.png"), os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + t.Fatal(err) + } + // tiny 1x1 png + imgRect := image.Rect(0, 0, 1, 1) + png.Encode(file3, imgRect) + file3.Close() + + return dir +} diff --git a/tools/hook/hook.go b/tools/hook/hook.go new file mode 100644 index 00000000..da88b8e9 --- /dev/null +++ b/tools/hook/hook.go @@ -0,0 +1,64 @@ +package hook + +import ( + "errors" + "sync" +) + +var StopPropagation = errors.New("Event hook propagation stopped") + +// Handler defines a hook handler function. +type Handler[T any] func(data T) error + +// Hook defines a concurrent safe structure for handling event hooks +// (aka. callbacks propagation). +type Hook[T any] struct { + mux sync.RWMutex + handlers []Handler[T] +} + +// Add registers a new handler to the hook. +func (h *Hook[T]) Add(fn Handler[T]) { + h.mux.Lock() + defer h.mux.Unlock() + + h.handlers = append(h.handlers, fn) +} + +// Reset removes all registered handlers. +func (h *Hook[T]) Reset() { + h.mux.Lock() + defer h.mux.Unlock() + + h.handlers = nil +} + +// Trigger executes all registered hook handlers one by one +// with the specified `data` as an argument. +// +// Optionally, this method allows also to register additional one off +// handlers that will be temporary appended to the handlers queue. +// +// The execution stops when: +// - hook.StopPropagation is returned in one of the handlers +// - any non-nil error is returned in one of the handlers +func (h *Hook[T]) Trigger(data T, oneOffHandlers ...Handler[T]) error { + h.mux.Lock() + handlers := append(h.handlers, oneOffHandlers...) + h.mux.Unlock() // unlock is not deferred to avoid deadlocks when Trigger is called recursive in the handlers + + for _, fn := range handlers { + err := fn(data) + if err == nil { + continue + } + + if errors.Is(err, StopPropagation) { + return nil + } + + return err + } + + return nil +} diff --git a/tools/hook/hook_test.go b/tools/hook/hook_test.go new file mode 100644 index 00000000..24e46e88 --- /dev/null +++ b/tools/hook/hook_test.go @@ -0,0 +1,129 @@ +package hook + +import ( + "errors" + "testing" +) + +func TestAdd(t *testing.T) { + h := Hook[int]{} + + if total := len(h.handlers); total != 0 { + t.Fatalf("Expected no handlers, found %d", total) + } + + h.Add(func(data int) error { return nil }) + h.Add(func(data int) error { return nil }) + + if total := len(h.handlers); total != 2 { + t.Fatalf("Expected 2 handlers, found %d", total) + } +} + +func TestReset(t *testing.T) { + h := Hook[int]{} + + h.Reset() // should do nothing and not panic + + h.Add(func(data int) error { return nil }) + h.Add(func(data int) error { return nil }) + + if total := len(h.handlers); total != 2 { + t.Fatalf("Expected 2 handlers before Reset, found %d", total) + } + + h.Reset() + + if total := len(h.handlers); total != 0 { + t.Fatalf("Expected no handlers after Reset, found %d", total) + } +} + +func TestTrigger(t *testing.T) { + err1 := errors.New("demo") + err2 := errors.New("demo") + + scenarios := []struct { + handlers []Handler[int] + expectedError error + }{ + { + []Handler[int]{ + func(data int) error { return nil }, + func(data int) error { return nil }, + }, + nil, + }, + { + []Handler[int]{ + func(data int) error { return nil }, + func(data int) error { return err1 }, + func(data int) error { return err2 }, + }, + err1, + }, + } + + for i, scenario := range scenarios { + h := Hook[int]{} + for _, handler := range scenario.handlers { + h.Add(handler) + } + result := h.Trigger(1) + if result != scenario.expectedError { + t.Fatalf("(%d) Expected %v, got %v", i, scenario.expectedError, result) + } + } +} + +func TestTriggerStopPropagation(t *testing.T) { + called1 := false + f1 := func(data int) error { called1 = true; return nil } + + called2 := false + f2 := func(data int) error { called2 = true; return nil } + + called3 := false + f3 := func(data int) error { called3 = true; return nil } + + called4 := false + f4 := func(data int) error { called4 = true; return StopPropagation } + + called5 := false + f5 := func(data int) error { called5 = true; return nil } + + called6 := false + f6 := func(data int) error { called6 = true; return nil } + + h := Hook[int]{} + h.Add(f1) + h.Add(f2) + + result := h.Trigger(123, f3, f4, f5, f6) + + if result != nil { + t.Fatalf("Expected nil after StopPropagation, got %v", result) + } + + // ensure that the trigger handler were not persisted + if total := len(h.handlers); total != 2 { + t.Fatalf("Expected 2 handlers, found %d", total) + } + + scenarios := []struct { + called bool + expected bool + }{ + {called1, true}, + {called2, true}, + {called3, true}, + {called4, true}, // StopPropagation + {called5, false}, + {called6, false}, + } + for i, scenario := range scenarios { + if scenario.called != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, scenario.called) + } + } +} diff --git a/tools/inflector/inflector.go b/tools/inflector/inflector.go new file mode 100644 index 00000000..6e39efaf --- /dev/null +++ b/tools/inflector/inflector.go @@ -0,0 +1,118 @@ +package inflector + +import ( + "regexp" + "strings" + "unicode" +) + +var columnifyRemoveRegex = regexp.MustCompile(`[^\w\.\*\-\_\@\#]+`) +var snakecaseSplitRegex = regexp.MustCompile(`[\W_]+`) +var usernamifySplitRegex = regexp.MustCompile(`\W+`) + +// UcFirst converts the first character of a string into uppercase. +func UcFirst(str string) string { + if str == "" { + return "" + } + + s := []rune(str) + + return string(unicode.ToUpper(s[0])) + string(s[1:]) +} + +// Columnify strips invalid db identifier characters. +func Columnify(str string) string { + return columnifyRemoveRegex.ReplaceAllString(str, "") +} + +// Sentenize converts and normalizes string into a sentence. +func Sentenize(str string) string { + str = strings.TrimSpace(str) + if str == "" { + return "" + } + + s := []rune(str) + sentence := string(unicode.ToUpper(s[0])) + string(s[1:]) + + lastChar := string(s[len(s)-1:]) + if lastChar != "." && lastChar != "?" && lastChar != "!" { + return sentence + "." + } + + return sentence +} + +// Sanitize sanitizes `str` by removing all characters satisfying `removePattern`. +// Returns an error if the pattern is not valid regex string. +func Sanitize(str string, removePattern string) (string, error) { + exp, err := regexp.Compile(removePattern) + if err != nil { + return "", err + } + + return exp.ReplaceAllString(str, ""), nil +} + +// Snakecase removes all non word characters and converts any english text into a snakecase. +// "ABBREVIATIONS" are preserved, eg. "myTestDB" will become "my_test_db". +func Snakecase(str string) string { + var result strings.Builder + + // split at any non word character and underscore + words := snakecaseSplitRegex.Split(str, -1) + + for _, word := range words { + if word == "" { + continue + } + + if result.Len() > 0 { + result.WriteString("_") + } + + for i, c := range word { + if unicode.IsUpper(c) && i > 0 && + // is not a following uppercase character + !unicode.IsUpper(rune(word[i-1])) { + result.WriteString("_") + } + + result.WriteRune(c) + } + } + + return strings.ToLower(result.String()) +} + +// Usernamify generates a properly formatted username from the provided string. +// Returns "unknown" if `str` is empty or contains only non word characters. +// +// ```go +// Usernamify("John Doe, hello") // "john.doe.hello" +// ``` +func Usernamify(str string) string { + // split at any non word character + words := usernamifySplitRegex.Split(strings.ToLower(str), -1) + + // concatenate any non empty word with a dot + var result strings.Builder + for _, word := range words { + if word == "" { + continue + } + + if result.Len() > 0 { + result.WriteString(".") + } + + result.WriteString(word) + } + + if result.Len() == 0 { + return "unknown" + } + + return result.String() +} diff --git a/tools/inflector/inflector_test.go b/tools/inflector/inflector_test.go new file mode 100644 index 00000000..8777bbe6 --- /dev/null +++ b/tools/inflector/inflector_test.go @@ -0,0 +1,153 @@ +package inflector_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/tools/inflector" +) + +func TestUcFirst(t *testing.T) { + scenarios := []struct { + val string + expected string + }{ + {"", ""}, + {"Test", "Test"}, + {"test", "Test"}, + {"test test2", "Test test2"}, + } + + for i, scenario := range scenarios { + if result := inflector.UcFirst(scenario.val); result != scenario.expected { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) + } + } +} + +func TestColumnify(t *testing.T) { + scenarios := []struct { + val string + expected string + }{ + {"", ""}, + {" ", ""}, + {"123", "123"}, + {"Test.", "Test."}, + {" test ", "test"}, + {"test1.test2", "test1.test2"}, + {"@test!abc", "@testabc"}, + {"#test?abc", "#testabc"}, + {"123test(123)#", "123test123#"}, + {"test1--test2", "test1--test2"}, + } + + for i, scenario := range scenarios { + if result := inflector.Columnify(scenario.val); result != scenario.expected { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) + } + } +} + +func TestSentenize(t *testing.T) { + scenarios := []struct { + val string + expected string + }{ + {"", ""}, + {" ", ""}, + {"Test", "Test."}, + {" test ", "Test."}, + {"hello world", "Hello world."}, + {"hello world.", "Hello world."}, + {"hello world!", "Hello world!"}, + {"hello world?", "Hello world?"}, + } + + for i, scenario := range scenarios { + if result := inflector.Sentenize(scenario.val); result != scenario.expected { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) + } + } +} + +func TestSanitize(t *testing.T) { + scenarios := []struct { + val string + pattern string + expected string + expectErr bool + }{ + {"", ``, "", false}, + {" ", ``, " ", false}, + {" ", ` `, "", false}, + {"", `[A-Z]`, "", false}, + {"abcABC", `[A-Z]`, "abc", false}, + {"abcABC", `[A-Z`, "", true}, // invlid pattern + } + + for i, scenario := range scenarios { + result, err := inflector.Sanitize(scenario.val, scenario.pattern) + hasErr := err != nil + + if scenario.expectErr != hasErr { + if scenario.expectErr { + t.Errorf("(%d) Expected error, got nil", i) + } else { + t.Errorf("(%d) Didn't expect error, got", err) + } + } + + if result != scenario.expected { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) + } + } +} + +func TestSnakecase(t *testing.T) { + scenarios := []struct { + val string + expected string + }{ + {"", ""}, + {" ", ""}, + {"!@#$%^", ""}, + {"...", ""}, + {"_", ""}, + {"John Doe", "john_doe"}, + {"John_Doe", "john_doe"}, + {".a!b@c#d$e%123. ", "a_b_c_d_e_123"}, + {"HelloWorld", "hello_world"}, + {"HelloWorld1HelloWorld2", "hello_world1_hello_world2"}, + {"TEST", "test"}, + {"testABR", "test_abr"}, + } + + for i, scenario := range scenarios { + if result := inflector.Snakecase(scenario.val); result != scenario.expected { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) + } + } +} + +func TestUsernamify(t *testing.T) { + scenarios := []struct { + val string + expected string + }{ + {"", "unknown"}, + {" ", "unknown"}, + {"!@#$%^", "unknown"}, + {"...", "unknown"}, + {"_", "_"}, // underscore is valid word character + {"John Doe", "john.doe"}, + {"John_Doe", "john_doe"}, + {".a!b@c#d$e%123. ", "a.b.c.d.e.123"}, + {"Hello, world", "hello.world"}, + } + + for i, scenario := range scenarios { + if result := inflector.Usernamify(scenario.val); result != scenario.expected { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) + } + } +} diff --git a/tools/list/list.go b/tools/list/list.go new file mode 100644 index 00000000..008b2730 --- /dev/null +++ b/tools/list/list.go @@ -0,0 +1,117 @@ +package list + +import ( + "encoding/json" + "regexp" + "strings" + + "github.com/spf13/cast" +) + +var cachedPatterns = map[string]*regexp.Regexp{} + +// ExustInSlice checks whether a comparable element exists in a slice of the same type. +func ExistInSlice[T comparable](item T, list []T) bool { + if len(list) == 0 { + return false + } + + for _, v := range list { + if v == item { + return true + } + } + + return false +} + +// ExistInSliceWithRegex checks whether a string exists in a slice +// either by direct match, or by a regular expression (eg. `^\w+$`). +// +// _Note: Only list items starting with '^' and ending with '$' are treated as regular expressions!_ +func ExistInSliceWithRegex(str string, list []string) bool { + for _, field := range list { + isRegex := strings.HasPrefix(field, "^") && strings.HasSuffix(field, "$") + + if !isRegex { + // check for direct match + if str == field { + return true + } + } else { + // check for regex match + pattern, ok := cachedPatterns[field] + if !ok { + var patternErr error + pattern, patternErr = regexp.Compile(field) + if patternErr != nil { + continue + } + // "cache" the pattern to avoid compiling it every time + cachedPatterns[field] = pattern + } + if pattern != nil && pattern.MatchString(str) { + return true + } + } + } + + return false +} + +// ToInterfaceSlice converts a generic slice to slice of interfaces. +func ToInterfaceSlice[T any](list []T) []any { + result := make([]any, len(list)) + + for i := range list { + result[i] = list[i] + } + + return result +} + +// NonzeroUniques returns only the nonzero unique values from a slice. +func NonzeroUniques[T comparable](list []T) []T { + result := []T{} + existMap := map[T]bool{} + + var zeroVal T + + for _, val := range list { + if !existMap[val] && val != zeroVal { + existMap[val] = true + result = append(result, val) + } + } + + return result +} + +// ToUniqueStringSlice casts `value` to a slice of non-zero unique strings. +func ToUniqueStringSlice(value any) []string { + strings := []string{} + + switch val := value.(type) { + case nil: + // nothing to cast + case []string: + strings = val + case string: + if val == "" { + break + } + + // check if it is a json encoded array of strings + if err := json.Unmarshal([]byte(val), &strings); err != nil { + // not a json array, just add the string as single array element + strings = append(strings, val) + } + case json.Marshaler: // eg. JsonArray + raw, _ := val.MarshalJSON() + json.Unmarshal(raw, &strings) + default: + strings = cast.ToStringSlice(value) + } + + return NonzeroUniques(strings) +} diff --git a/tools/list/list_test.go b/tools/list/list_test.go new file mode 100644 index 00000000..16bbacd0 --- /dev/null +++ b/tools/list/list_test.go @@ -0,0 +1,174 @@ +package list_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/tools/list" + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestExistInSliceString(t *testing.T) { + scenarios := []struct { + item string + list []string + expected bool + }{ + {"", []string{""}, true}, + {"", []string{"1", "2", "test 123"}, false}, + {"test", []string{}, false}, + {"test", []string{"TEST"}, false}, + {"test", []string{"1", "2", "test 123"}, false}, + {"test", []string{"1", "2", "test"}, true}, + } + + for i, scenario := range scenarios { + result := list.ExistInSlice(scenario.item, scenario.list) + if result != scenario.expected { + if scenario.expected { + t.Errorf("(%d) Expected to exist in the list", i) + } else { + t.Errorf("(%d) Expected NOT to exist in the list", i) + } + } + } +} + +func TestExistInSliceInt(t *testing.T) { + scenarios := []struct { + item int + list []int + expected bool + }{ + {0, []int{}, false}, + {0, []int{0}, true}, + {4, []int{1, 2, 3}, false}, + {1, []int{1, 2, 3}, true}, + {-1, []int{0, 1, 2, 3}, false}, + {-1, []int{0, -1, -2, -3, -4}, true}, + } + + for i, scenario := range scenarios { + result := list.ExistInSlice(scenario.item, scenario.list) + if result != scenario.expected { + if scenario.expected { + t.Errorf("(%d) Expected to exist in the list", i) + } else { + t.Errorf("(%d) Expected NOT to exist in the list", i) + } + } + } +} + +func TestExistInSliceWithRegex(t *testing.T) { + scenarios := []struct { + item string + list []string + expected bool + }{ + {"", []string{``}, true}, + {"", []string{`^\W+$`}, false}, + {" ", []string{`^\W+$`}, true}, + {"test", []string{`^\invalid[+$`}, false}, // invalid regex + {"test", []string{`^\W+$`, "test"}, true}, + {`^\W+$`, []string{`^\W+$`, "test"}, false}, // direct match shouldn't work for this case + {`\W+$`, []string{`\W+$`, "test"}, true}, // direct match should work for this case because it is not an actual supported pattern format + {"!?@", []string{`\W+$`, "test"}, false}, // the method requires the pattern elems to start with '^' + {"!?@", []string{`^\W+`, "test"}, false}, // the method requires the pattern elems to end with '$' + {"!?@", []string{`^\W+$`, "test"}, true}, + {"!?@test", []string{`^\W+$`, "test"}, false}, + } + + for i, scenario := range scenarios { + result := list.ExistInSliceWithRegex(scenario.item, scenario.list) + if result != scenario.expected { + if scenario.expected { + t.Errorf("(%d) Expected the string to exist in the list", i) + } else { + t.Errorf("(%d) Expected the string NOT to exist in the list", i) + } + } + } +} + +func TestToInterfaceSlice(t *testing.T) { + scenarios := []struct { + items []string + }{ + {[]string{}}, + {[]string{""}}, + {[]string{"1", "test"}}, + {[]string{"test1", "test2", "test3"}}, + } + + for i, scenario := range scenarios { + result := list.ToInterfaceSlice(scenario.items) + + if len(result) != len(scenario.items) { + t.Errorf("(%d) Result list length doesn't match with the original list", i) + } + + for j, v := range result { + if v != scenario.items[j] { + t.Errorf("(%d:%d) Result list item should match with the original list item", i, j) + } + } + } +} + +func TestNonzeroUniquesString(t *testing.T) { + scenarios := []struct { + items []string + expected []string + }{ + {[]string{}, []string{}}, + {[]string{""}, []string{}}, + {[]string{"1", "test"}, []string{"1", "test"}}, + {[]string{"test1", "", "test2", "Test2", "test1", "test3"}, []string{"test1", "test2", "Test2", "test3"}}, + } + + for i, scenario := range scenarios { + result := list.NonzeroUniques(scenario.items) + + if len(result) != len(scenario.expected) { + t.Errorf("(%d) Result list length doesn't match with the expected list", i) + } + + for j, v := range result { + if v != scenario.expected[j] { + t.Errorf("(%d:%d) Result list item should match with the expected list item", i, j) + } + } + } +} + +func TestToUniqueStringSlice(t *testing.T) { + scenarios := []struct { + value any + expected []string + }{ + {nil, []string{}}, + {"", []string{}}, + {[]any{}, []string{}}, + {[]int{}, []string{}}, + {"test", []string{"test"}}, + {[]int{1, 2, 3}, []string{"1", "2", "3"}}, + {[]any{0, 1, "test", ""}, []string{"0", "1", "test"}}, + {[]string{"test1", "test2", "test1"}, []string{"test1", "test2"}}, + {`["test1", "test2", "test2"]`, []string{"test1", "test2"}}, + {types.JsonArray{"test1", "test2", "test1"}, []string{"test1", "test2"}}, + } + + for i, scenario := range scenarios { + result := list.ToUniqueStringSlice(scenario.value) + + if len(result) != len(scenario.expected) { + t.Errorf("(%d) Result list length doesn't match with the expected list", i) + } + + for j, v := range result { + if v != scenario.expected[j] { + t.Errorf("(%d:%d) Result list item should match with the expected list item", i, j) + } + } + } +} diff --git a/tools/mailer/mailer.go b/tools/mailer/mailer.go new file mode 100644 index 00000000..a4fa41d8 --- /dev/null +++ b/tools/mailer/mailer.go @@ -0,0 +1,18 @@ +package mailer + +import ( + "io" + "net/mail" +) + +// Mailer defines a base mail client interface. +type Mailer interface { + // Send sends an email with HTML body to the specified recipient. + Send( + fromEmail mail.Address, + toEmail mail.Address, + subject string, + htmlBody string, + attachments map[string]io.Reader, + ) error +} diff --git a/tools/mailer/sendmail.go b/tools/mailer/sendmail.go new file mode 100644 index 00000000..b8dc8d61 --- /dev/null +++ b/tools/mailer/sendmail.go @@ -0,0 +1,79 @@ +package mailer + +import ( + "bytes" + "errors" + "io" + "mime" + "net/http" + "net/mail" + "os/exec" +) + +var _ Mailer = (*Sendmail)(nil) + +// Sendmail implements `mailer.Mailer` interface and defines a mail +// client that sends emails via the `sendmail` *nix command. +// +// This client is usually recommended only for development and testing. +type Sendmail struct { +} + +// Send implements `mailer.Mailer` interface. +// +// Attachments are currently not supported. +func (m *Sendmail) Send( + fromEmail mail.Address, + toEmail mail.Address, + subject string, + htmlBody string, + attachments map[string]io.Reader, +) error { + headers := make(http.Header) + headers.Set("Subject", mime.QEncoding.Encode("utf-8", subject)) + headers.Set("From", fromEmail.String()) + headers.Set("To", toEmail.String()) + headers.Set("Content-Type", "text/html; charset=UTF-8") + + cmdPath, err := findSendmailPath() + if err != nil { + return err + } + + var buffer bytes.Buffer + + // write + // --- + if err := headers.Write(&buffer); err != nil { + return err + } + if _, err := buffer.Write([]byte("\r\n")); err != nil { + return err + } + if _, err := buffer.Write([]byte(htmlBody)); err != nil { + return err + } + // --- + + sendmail := exec.Command(cmdPath, toEmail.Address) + sendmail.Stdin = &buffer + + return sendmail.Run() +} + +func findSendmailPath() (string, error) { + options := []string{ + "/usr/sbin/sendmail", + "/usr/bin/sendmail", + "sendmail", + } + + for _, option := range options { + path, err := exec.LookPath(option) + if err == nil { + return path, err + } + } + + return "", errors.New("Failed to locate a sendmail executable path.") +} diff --git a/tools/mailer/smtp.go b/tools/mailer/smtp.go new file mode 100644 index 00000000..0902492e --- /dev/null +++ b/tools/mailer/smtp.go @@ -0,0 +1,88 @@ +package mailer + +import ( + "fmt" + "io" + "net/mail" + "net/smtp" + "regexp" + "strings" + + "github.com/domodwyer/mailyak/v3" + "github.com/microcosm-cc/bluemonday" +) + +var _ Mailer = (*SmtpClient)(nil) + +// regex to select all tabs +var tabsRegex = regexp.MustCompile(`\t+`) + +// NewSmtpClient creates new `SmtpClient` with the provided configuration. +func NewSmtpClient( + host string, + port int, + username string, + password string, + tls bool, +) *SmtpClient { + return &SmtpClient{ + host: host, + port: port, + username: username, + password: password, + tls: tls, + } +} + +// SmtpClient defines a SMTP mail client structure that implements +// `mailer.Mailer` interface. +type SmtpClient struct { + mail *mailyak.MailYak + + host string + port int + username string + password string + tls bool +} + +// Send implements `mailer.Mailer` interface. +func (m *SmtpClient) Send( + fromEmail mail.Address, + toEmail mail.Address, + subject string, + htmlBody string, + attachments map[string]io.Reader, +) error { + smtpAuth := smtp.PlainAuth("", m.username, m.password, m.host) + + // create mail instance + var yak *mailyak.MailYak + if m.tls { + var tlsErr error + yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", m.host, m.port), smtpAuth, nil) + if tlsErr != nil { + return tlsErr + } + } else { + yak = mailyak.New(fmt.Sprintf("%s:%d", m.host, m.port), smtpAuth) + } + + if fromEmail.Name != "" { + yak.FromName(fromEmail.Name) + } + yak.From(fromEmail.Address) + yak.To(toEmail.Address) + yak.Subject(subject) + yak.HTML().Set(htmlBody) + + // set also plain text content + policy := bluemonday.StrictPolicy() // strips all tags + yak.Plain().Set(strings.TrimSpace(tabsRegex.ReplaceAllString(policy.Sanitize(htmlBody), ""))) + + for name, data := range attachments { + yak.Attach(name, data) + } + + return yak.Send() +} diff --git a/tools/migrate/list.go b/tools/migrate/list.go new file mode 100644 index 00000000..858c13d7 --- /dev/null +++ b/tools/migrate/list.go @@ -0,0 +1,59 @@ +package migrate + +import ( + "path/filepath" + "runtime" + "sort" + + "github.com/pocketbase/dbx" +) + +type migration struct { + file string + up func(db dbx.Builder) error + down func(db dbx.Builder) error +} + +// MigrationsList defines a list with migration definitions +type MigrationsList struct { + list []*migration +} + +// Item returns a single migration from the list by its index. +func (l *MigrationsList) Item(index int) *migration { + return l.list[index] +} + +// Items returns the internal migrations list slice. +func (l *MigrationsList) Items() []*migration { + return l.list +} + +// Register adds new migration definition to the list. +// +// If `optFilename` is not provided, it will try to get the name from its .go file. +// +// The list will be sorted automatically based on the migrations file name. +func (l *MigrationsList) Register( + up func(db dbx.Builder) error, + down func(db dbx.Builder) error, + optFilename ...string, +) { + var file string + if len(optFilename) > 0 { + file = optFilename[0] + } else { + _, path, _, _ := runtime.Caller(1) + file = filepath.Base(path) + } + + l.list = append(l.list, &migration{ + file: file, + up: up, + down: down, + }) + + sort.Slice(l.list, func(i int, j int) bool { + return l.list[i].file < l.list[j].file + }) +} diff --git a/tools/migrate/list_test.go b/tools/migrate/list_test.go new file mode 100644 index 00000000..4e96e4ee --- /dev/null +++ b/tools/migrate/list_test.go @@ -0,0 +1,33 @@ +package migrate + +import ( + "testing" +) + +func TestMigrationsList(t *testing.T) { + l := MigrationsList{} + + l.Register(nil, nil, "3_test.go") + l.Register(nil, nil, "1_test.go") + l.Register(nil, nil, "2_test.go") + l.Register(nil, nil /* auto detect file name */) + + expected := []string{ + "1_test.go", + "2_test.go", + "3_test.go", + "list_test.go", + } + + items := l.Items() + if len(items) != len(expected) { + t.Fatalf("Expected %d items, got %d: \n%#v", len(expected), len(items), items) + } + + for i, name := range expected { + item := l.Item(i) + if item.file != name { + t.Fatalf("Expected name %s for index %d, got %s", name, i, item.file) + } + } +} diff --git a/tools/migrate/runner.go b/tools/migrate/runner.go new file mode 100644 index 00000000..3e8688c1 --- /dev/null +++ b/tools/migrate/runner.go @@ -0,0 +1,271 @@ +package migrate + +import ( + "fmt" + "os" + "path" + "time" + + "github.com/AlecAivazis/survey/v2" + "github.com/fatih/color" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/inflector" + "github.com/spf13/cast" +) + +const migrationsTable = "_migrations" + +// Runner defines a simple struct for managing the execution of db migrations. +type Runner struct { + db *dbx.DB + migrationsList MigrationsList + tableName string +} + +// NewRunner creates and initializes a new db migrations Runner instance. +func NewRunner(db *dbx.DB, migrationsList MigrationsList) (*Runner, error) { + runner := &Runner{ + db: db, + migrationsList: migrationsList, + tableName: migrationsTable, + } + + if err := runner.createMigrationsTable(); err != nil { + return nil, err + } + + return runner, nil +} + +// Run interactively executes the current runner with the provided args. +// +// The following commands are supported: +// - up - applies all migrations +// - down [n] - reverts the last n applied migrations +// - create NEW_MIGRATION_NAME - create NEW_MIGRATION_NAME.go file from a migration template +func (r *Runner) Run(args ...string) error { + cmd := "up" + if len(args) > 0 { + cmd = args[0] + } + + switch cmd { + case "up": + applied, err := r.Up() + if err != nil { + color.Red(err.Error()) + return err + } + + if len(applied) == 0 { + color.Green("No new migrations to apply.") + } else { + for _, file := range applied { + color.Green("Applied %s", file) + } + } + + return nil + case "down": + toRevertCount := 1 + if len(args) > 1 { + toRevertCount = cast.ToInt(args[1]) + if toRevertCount < 0 { + // revert all applied migrations + toRevertCount = len(r.migrationsList.Items()) + } + } + + confirm := false + prompt := &survey.Confirm{ + Message: fmt.Sprintf("Do you really want to revert the last %d applied migration(s)?", toRevertCount), + } + survey.AskOne(prompt, &confirm) + if !confirm { + fmt.Println("The command has been cancelled") + return nil + } + + reverted, err := r.Down(toRevertCount) + if err != nil { + color.Red(err.Error()) + return err + } + + if len(reverted) == 0 { + color.Green("No migrations to revert.") + } else { + for _, file := range reverted { + color.Green("Reverted %s", file) + } + } + + return nil + case "create": + if len(args) < 2 { + return fmt.Errorf("Missing migration file name") + } + + name := args[1] + + var dir string + if len(args) == 3 { + dir = args[2] + } + if dir == "" { + // If not specified, auto point to the default migrations folder. + // + // NB! + // Since the create command makes sense only during development, + // it is expected the user to be in the app working directory + // and to be using `go run ...` + wd, err := os.Getwd() + if err != nil { + return err + } + dir = path.Join(wd, "migrations") + } + + resultFilePath := path.Join( + dir, + fmt.Sprintf("%d_%s.go", time.Now().Unix(), inflector.Snakecase(name)), + ) + + confirm := false + prompt := &survey.Confirm{ + Message: fmt.Sprintf("Do you really want to create migration %q?", resultFilePath), + } + survey.AskOne(prompt, &confirm) + if !confirm { + fmt.Println("The command has been cancelled") + return nil + } + + // ensure that migrations dir exist + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + if err := os.WriteFile(resultFilePath, []byte(createTemplateContent), 0644); err != nil { + return fmt.Errorf("Failed to save migration file %q\n", resultFilePath) + } + + fmt.Printf("Successfully created file %q\n", resultFilePath) + return nil + default: + return fmt.Errorf("Unsupported command: %q\n", cmd) + } +} + +// Up executes all unapplied migrations for the provided runner. +// +// On success returns list with the applied migrations file names. +func (r *Runner) Up() ([]string, error) { + applied := []string{} + + err := r.db.Transactional(func(tx *dbx.Tx) error { + for _, m := range r.migrationsList.Items() { + // skip applied + if r.isMigrationApplied(tx, m.file) { + continue + } + + if err := m.up(tx); err != nil { + return fmt.Errorf("Failed to apply migration %s: %w", m.file, err) + } + + if err := r.saveAppliedMigration(tx, m.file); err != nil { + return fmt.Errorf("Failed to save applied migration info for %s: %w", m.file, err) + } + + applied = append(applied, m.file) + } + + return nil + }) + + if err != nil { + return nil, err + } + return applied, nil +} + +// Down reverts the last `toRevertCount` applied migrations. +// +// On success returns list with the reverted migrations file names. +func (r *Runner) Down(toRevertCount int) ([]string, error) { + applied := []string{} + + err := r.db.Transactional(func(tx *dbx.Tx) error { + totalReverted := 0 + + for i := len(r.migrationsList.Items()) - 1; i >= 0; i-- { + m := r.migrationsList.Item(i) + + // skip unapplied + if !r.isMigrationApplied(tx, m.file) { + continue + } + + // revert limit reached + if toRevertCount-totalReverted <= 0 { + break + } + + if err := m.down(tx); err != nil { + return fmt.Errorf("Failed to revert migration %s: %w", m.file, err) + } + + if err := r.saveRevertedMigration(tx, m.file); err != nil { + return fmt.Errorf("Failed to save reverted migration info for %s: %w", m.file, err) + } + + applied = append(applied, m.file) + } + + return nil + }) + + if err != nil { + return nil, err + } + return applied, nil +} + +func (r *Runner) createMigrationsTable() error { + rawQuery := fmt.Sprintf( + "CREATE TABLE IF NOT EXISTS %v (file VARCHAR(255) PRIMARY KEY NOT NULL, applied INTEGER NOT NULL)", + r.db.QuoteTableName(r.tableName), + ) + + _, err := r.db.NewQuery(rawQuery).Execute() + + return err +} + +func (r *Runner) isMigrationApplied(tx dbx.Builder, file string) bool { + var exists bool + + err := tx.Select("count(*)"). + From(r.tableName). + Where(dbx.HashExp{"file": file}). + Limit(1). + Row(&exists) + + return err == nil && exists +} + +func (r *Runner) saveAppliedMigration(tx dbx.Builder, file string) error { + _, err := tx.Insert(r.tableName, dbx.Params{ + "file": file, + "applied": time.Now().Unix(), + }).Execute() + + return err +} + +func (r *Runner) saveRevertedMigration(tx dbx.Builder, file string) error { + _, err := tx.Delete(r.tableName, dbx.HashExp{"file": file}).Execute() + + return err +} diff --git a/tools/migrate/runner_test.go b/tools/migrate/runner_test.go new file mode 100644 index 00000000..f23f1de0 --- /dev/null +++ b/tools/migrate/runner_test.go @@ -0,0 +1,145 @@ +package migrate + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/list" + _ "modernc.org/sqlite" +) + +func TestNewRunner(t *testing.T) { + testDB, err := createTestDB() + if err != nil { + t.Fatal(err) + } + defer testDB.Close() + + l := MigrationsList{} + l.Register(nil, nil, "1_test.go") + l.Register(nil, nil, "2_test.go") + l.Register(nil, nil, "3_test.go") + + r, err := NewRunner(testDB.DB, l) + if err != nil { + t.Fatal(err) + } + + if len(r.migrationsList.Items()) != len(l.Items()) { + t.Fatalf("Expected the same migrations list to be assigned, got \n%#v", r.migrationsList) + } + + expectedQueries := []string{ + "CREATE TABLE IF NOT EXISTS `_migrations` (file VARCHAR(255) PRIMARY KEY NOT NULL, applied INTEGER NOT NULL)", + } + if len(expectedQueries) != len(testDB.CalledQueries) { + t.Fatalf("Expected %d queries, got %d: \n%v", len(expectedQueries), len(testDB.CalledQueries), testDB.CalledQueries) + } + for _, q := range expectedQueries { + if !list.ExistInSlice(q, testDB.CalledQueries) { + t.Fatalf("Query %s was not found in \n%v", q, testDB.CalledQueries) + } + } +} + +func TestRunnerUpAndDown(t *testing.T) { + testDB, err := createTestDB() + if err != nil { + t.Fatal(err) + } + defer testDB.Close() + + var test1UpCalled bool + var test1DownCalled bool + var test2UpCalled bool + var test2DownCalled bool + + l := MigrationsList{} + l.Register(func(db dbx.Builder) error { + test1UpCalled = true + return nil + }, func(db dbx.Builder) error { + test1DownCalled = true + return nil + }, "1_test") + l.Register(func(db dbx.Builder) error { + test2UpCalled = true + return nil + }, func(db dbx.Builder) error { + test2DownCalled = true + return nil + }, "2_test") + + r, err := NewRunner(testDB.DB, l) + if err != nil { + t.Fatal(err) + } + + // simulate partially run migration + r.saveAppliedMigration(testDB, r.migrationsList.Item(0).file) + + // Up() + // --- + if _, err := r.Up(); err != nil { + t.Fatal(err) + } + + if test1UpCalled { + t.Fatalf("Didn't expect 1_test to be called") + } + + if !test2UpCalled { + t.Fatalf("Expected 2_test to be called") + } + + // simulate unrun migration + var test3DownCalled bool + r.migrationsList.Register(nil, func(db dbx.Builder) error { + test3DownCalled = true + return nil + }, "3_test") + + // Down() + // --- + if _, err := r.Down(2); err != nil { + t.Fatal(err) + } + + if test3DownCalled { + t.Fatal("Didn't expect 3_test to be reverted.") + } + + if !test1DownCalled || !test2DownCalled { + t.Fatalf("Expected 1_test and 2_test to be reverted, got %v and %v", test1DownCalled, test2DownCalled) + } +} + +// ------------------------------------------------------------------- +// Helpers +// ------------------------------------------------------------------- + +type testDB struct { + *dbx.DB + CalledQueries []string +} + +// NB! Don't forget to call `db.Close()` at the end of the test. +func createTestDB() (*testDB, error) { + sqlDB, err := sql.Open("sqlite", ":memory:") + if err != nil { + return nil, err + } + + db := testDB{DB: dbx.NewFromDB(sqlDB, "sqlite")} + db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + db.CalledQueries = append(db.CalledQueries, sql) + } + db.ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) { + db.CalledQueries = append(db.CalledQueries, sql) + } + + return &db, nil +} diff --git a/tools/migrate/template.go b/tools/migrate/template.go new file mode 100644 index 00000000..9a97458a --- /dev/null +++ b/tools/migrate/template.go @@ -0,0 +1,21 @@ +package migrate + +const createTemplateContent = `package migrations + +import ( + "github.com/pocketbase/dbx" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(db dbx.Builder) error { + // add up queries... + + return nil + }, func(db dbx.Builder) error { + // add down queries... + + return nil + }) +} +` diff --git a/tools/rest/api_error.go b/tools/rest/api_error.go new file mode 100644 index 00000000..bef430de --- /dev/null +++ b/tools/rest/api_error.go @@ -0,0 +1,107 @@ +package rest + +import ( + "net/http" + "strings" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/inflector" +) + +// ApiError defines the properties for a basic api error response. +type ApiError struct { + Code int `json:"code"` + Message string `json:"message"` + Data map[string]any `json:"data"` + + // stores unformatted error data (could be an internal error, text, etc.) + rawData any +} + +// Error makes it compatible with the `error` interface. +func (e *ApiError) Error() string { + return e.Message +} + +func (e *ApiError) RawData() any { + return e.rawData +} + +// NewNotFoundError creates and returns 404 `ApiError`. +func NewNotFoundError(message string, data any) *ApiError { + if message == "" { + message = "The requested resource wasn't found." + } + + return NewApiError(http.StatusNotFound, message, data) +} + +// NewBadRequestError creates and returns 400 `ApiError`. +func NewBadRequestError(message string, data any) *ApiError { + if message == "" { + message = "Something went wrong while processing your request." + } + + return NewApiError(http.StatusBadRequest, message, data) +} + +// NewForbiddenError creates and returns 403 `ApiError`. +func NewForbiddenError(message string, data any) *ApiError { + if message == "" { + message = "You are not allowed to perform this request." + } + + return NewApiError(http.StatusForbidden, message, data) +} + +// NewUnauthorizedError creates and returns 401 `ApiError`. +func NewUnauthorizedError(message string, data any) *ApiError { + if message == "" { + message = "Missing or invalid authentication token." + } + + return NewApiError(http.StatusUnauthorized, message, data) +} + +// NewApiError creates and returns new normalized `ApiError` instance. +func NewApiError(status int, message string, data any) *ApiError { + message = inflector.Sentenize(message) + + formattedData := map[string]any{} + + if v, ok := data.(validation.Errors); ok { + formattedData = resolveValidationErrors(v) + } + + return &ApiError{ + rawData: data, + Data: formattedData, + Code: status, + Message: strings.TrimSpace(message), + } +} + +func resolveValidationErrors(validationErrors validation.Errors) map[string]any { + result := map[string]any{} + + // extract from each validation error its error code and message. + for name, err := range validationErrors { + // check for nested errors + if nestedErrs, ok := err.(validation.Errors); ok { + result[name] = resolveValidationErrors(nestedErrs) + continue + } + + errCode := "validation_invalid_value" // default + if errObj, ok := err.(validation.ErrorObject); ok { + errCode = errObj.Code() + } + + result[name] = map[string]string{ + "code": errCode, + "message": inflector.Sentenize(err.Error()), + } + } + + return result +} diff --git a/tools/rest/api_error_test.go b/tools/rest/api_error_test.go new file mode 100644 index 00000000..89d52797 --- /dev/null +++ b/tools/rest/api_error_test.go @@ -0,0 +1,150 @@ +package rest_test + +import ( + "encoding/json" + "errors" + "testing" + + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/pocketbase/pocketbase/tools/rest" +) + +func TestNewApiErrorWithRawData(t *testing.T) { + e := rest.NewApiError( + 300, + "message_test", + "rawData_test", + ) + + result, _ := json.Marshal(e) + expected := `{"code":300,"message":"Message_test.","data":{}}` + + if string(result) != expected { + t.Errorf("Expected %v, got %v", expected, string(result)) + } + + if e.Error() != "Message_test." { + t.Errorf("Expected %q, got %q", "Message_test.", e.Error()) + } + + if e.RawData() != "rawData_test" { + t.Errorf("Expected rawData %v, got %v", "rawData_test", e.RawData()) + } +} + +func TestNewApiErrorWithValidationData(t *testing.T) { + e := rest.NewApiError( + 300, + "message_test", + validation.Errors{ + "err1": errors.New("test error"), + "err2": validation.ErrRequired, + "err3": validation.Errors{ + "sub1": errors.New("test error"), + "sub2": validation.ErrRequired, + "sub3": validation.Errors{ + "sub11": validation.ErrRequired, + }, + }, + }, + ) + + result, _ := json.Marshal(e) + expected := `{"code":300,"message":"Message_test.","data":{"err1":{"code":"validation_invalid_value","message":"Test error."},"err2":{"code":"validation_required","message":"Cannot be blank."},"err3":{"sub1":{"code":"validation_invalid_value","message":"Test error."},"sub2":{"code":"validation_required","message":"Cannot be blank."},"sub3":{"sub11":{"code":"validation_required","message":"Cannot be blank."}}}}}` + + if string(result) != expected { + t.Errorf("Expected %v, got %v", expected, string(result)) + } + + if e.Error() != "Message_test." { + t.Errorf("Expected %q, got %q", "Message_test.", e.Error()) + } + + if e.RawData() == nil { + t.Error("Expected non-nil rawData") + } +} + +func TestNewNotFoundError(t *testing.T) { + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"code":404,"message":"The requested resource wasn't found.","data":{}}`}, + {"demo", "rawData_test", `{"code":404,"message":"Demo.","data":{}}`}, + {"demo", validation.Errors{"err1": errors.New("test error")}, `{"code":404,"message":"Demo.","data":{"err1":{"code":"validation_invalid_value","message":"Test error."}}}`}, + } + + for i, scenario := range scenarios { + e := rest.NewNotFoundError(scenario.message, scenario.data) + result, _ := json.Marshal(e) + + if string(result) != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, string(result)) + } + } +} + +func TestNewBadRequestError(t *testing.T) { + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"code":400,"message":"Something went wrong while processing your request.","data":{}}`}, + {"demo", "rawData_test", `{"code":400,"message":"Demo.","data":{}}`}, + {"demo", validation.Errors{"err1": errors.New("test error")}, `{"code":400,"message":"Demo.","data":{"err1":{"code":"validation_invalid_value","message":"Test error."}}}`}, + } + + for i, scenario := range scenarios { + e := rest.NewBadRequestError(scenario.message, scenario.data) + result, _ := json.Marshal(e) + + if string(result) != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, string(result)) + } + } +} + +func TestNewForbiddenError(t *testing.T) { + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"code":403,"message":"You are not allowed to perform this request.","data":{}}`}, + {"demo", "rawData_test", `{"code":403,"message":"Demo.","data":{}}`}, + {"demo", validation.Errors{"err1": errors.New("test error")}, `{"code":403,"message":"Demo.","data":{"err1":{"code":"validation_invalid_value","message":"Test error."}}}`}, + } + + for i, scenario := range scenarios { + e := rest.NewForbiddenError(scenario.message, scenario.data) + result, _ := json.Marshal(e) + + if string(result) != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, string(result)) + } + } +} + +func TestNewUnauthorizedError(t *testing.T) { + scenarios := []struct { + message string + data any + expected string + }{ + {"", nil, `{"code":401,"message":"Missing or invalid authentication token.","data":{}}`}, + {"demo", "rawData_test", `{"code":401,"message":"Demo.","data":{}}`}, + {"demo", validation.Errors{"err1": errors.New("test error")}, `{"code":401,"message":"Demo.","data":{"err1":{"code":"validation_invalid_value","message":"Test error."}}}`}, + } + + for i, scenario := range scenarios { + e := rest.NewUnauthorizedError(scenario.message, scenario.data) + result, _ := json.Marshal(e) + + if string(result) != scenario.expected { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, string(result)) + } + } +} diff --git a/tools/rest/multi_binder.go b/tools/rest/multi_binder.go new file mode 100644 index 00000000..757237da --- /dev/null +++ b/tools/rest/multi_binder.go @@ -0,0 +1,59 @@ +package rest + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + + "github.com/labstack/echo/v5" +) + +// BindBody binds request body content to i. +// +// This is similar to `echo.BindBody()`, but for JSON requests uses +// custom json reader that **copies** the request body, allowing multiple reads. +func BindBody(c echo.Context, i interface{}) error { + req := c.Request() + if req.ContentLength == 0 { + return nil + } + + ctype := req.Header.Get(echo.HeaderContentType) + switch { + case strings.HasPrefix(ctype, echo.MIMEApplicationJSON): + err := ReadJsonBodyCopy(c.Request(), i) + if err != nil { + return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error()) + } + return nil + default: + // fallback to the default binder + return echo.BindBody(c, i) + } +} + +// ReadJsonBodyCopy reads the request body into i by +// creating a copy of `r.Body` to allow multiple reads. +func ReadJsonBodyCopy(r *http.Request, i interface{}) error { + body := r.Body + + // this usually shouldn't be needed because the Server calls close for us + // but we are changing the request body with a new reader + defer body.Close() + + limitReader := io.LimitReader(body, DefaultMaxMemory) + + bodyBytes, readErr := io.ReadAll(limitReader) + if readErr != nil { + return readErr + } + + err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(i) + + // set new body reader + r.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + + return err +} diff --git a/tools/rest/multi_binder_test.go b/tools/rest/multi_binder_test.go new file mode 100644 index 00000000..6dc90538 --- /dev/null +++ b/tools/rest/multi_binder_test.go @@ -0,0 +1,102 @@ +package rest_test + +import ( + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/tools/rest" +) + +func TestBindBody(t *testing.T) { + scenarios := []struct { + body io.Reader + contentType string + result map[string]string + expectError bool + }{ + { + strings.NewReader(""), + echo.MIMEApplicationJSON, + map[string]string{}, + false, + }, + { + strings.NewReader(`{"test":"invalid`), + echo.MIMEApplicationJSON, + map[string]string{}, + true, + }, + { + strings.NewReader(`{"test":"test123"}`), + echo.MIMEApplicationJSON, + map[string]string{"test": "test123"}, + false, + }, + { + strings.NewReader(url.Values{"test": []string{"test123"}}.Encode()), + echo.MIMEApplicationForm, + map[string]string{"test": "test123"}, + false, + }, + } + + for i, scenario := range scenarios { + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", scenario.body) + req.Header.Set(echo.HeaderContentType, scenario.contentType) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + result := map[string]string{} + err := rest.BindBody(c, &result) + + if err == nil && scenario.expectError { + t.Errorf("(%d) Expected error, got nil", i) + } + + if err != nil && !scenario.expectError { + t.Errorf("(%d) Expected nil, got error %v", i, err) + } + + if len(result) != len(scenario.result) { + t.Errorf("(%d) Expected %v, got %v", i, scenario.result, result) + } + + for k, v := range result { + if sv, ok := scenario.result[k]; !ok || v != sv { + t.Errorf("(%d) Expected value %v for key %s, got %v", i, sv, k, v) + } + } + } +} + +func TestReadJsonBodyCopy(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(`{"test":"test123"}`)) + + // simulate multiple reads from the same request + result1 := map[string]string{} + rest.ReadJsonBodyCopy(req, &result1) + result2 := map[string]string{} + rest.ReadJsonBodyCopy(req, &result2) + + if len(result1) == 0 { + t.Error("Expected result1 to be filled") + } + + if len(result2) == 0 { + t.Error("Expected result2 to be filled") + } + + if v, ok := result1["test"]; !ok || v != "test123" { + t.Errorf("Expected result1.test to be %q, got %q", "test123", v) + } + + if v, ok := result2["test"]; !ok || v != "test123" { + t.Errorf("Expected result2.test to be %q, got %q", "test123", v) + } +} diff --git a/tools/rest/uploaded_file.go b/tools/rest/uploaded_file.go new file mode 100644 index 00000000..57b311f8 --- /dev/null +++ b/tools/rest/uploaded_file.go @@ -0,0 +1,76 @@ +package rest + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "path/filepath" + + "github.com/pocketbase/pocketbase/tools/security" +) + +// DefaultMaxMemory defines the default max memory bytes that +// will be used when parsing a form request body. +const DefaultMaxMemory = 32 << 20 // 32mb + +// UploadedFile defines a single multipart uploaded file instance. +type UploadedFile struct { + name string + header *multipart.FileHeader + bytes []byte +} + +// Name returns an assigned unique name to the uploaded file. +func (f *UploadedFile) Name() string { + return f.name +} + +// Header returns the file header that comes with the multipart request. +func (f *UploadedFile) Header() *multipart.FileHeader { + return f.header +} + +// Bytes returns a slice with the file content. +func (f *UploadedFile) Bytes() []byte { + return f.bytes +} + +// FindUploadedFiles extracts all form files of `key` from a http request +// and returns a slice with `UploadedFile` instances (if any). +func FindUploadedFiles(r *http.Request, key string) ([]*UploadedFile, error) { + if r.MultipartForm == nil { + err := r.ParseMultipartForm(DefaultMaxMemory) + if err != nil { + return nil, err + } + } + + if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File[key]) == 0 { + return nil, http.ErrMissingFile + } + + result := make([]*UploadedFile, len(r.MultipartForm.File[key])) + + for i, fh := range r.MultipartForm.File[key] { + file, err := fh.Open() + if err != nil { + return nil, err + } + defer file.Close() + + buf := bytes.NewBuffer(nil) + if _, err := io.Copy(buf, file); err != nil { + return nil, err + } + + result[i] = &UploadedFile{ + name: fmt.Sprintf("%s%s", security.RandomString(32), filepath.Ext(fh.Filename)), + header: fh, + bytes: buf.Bytes(), + } + } + + return result, nil +} diff --git a/tools/rest/uploaded_file_test.go b/tools/rest/uploaded_file_test.go new file mode 100644 index 00000000..7b5975b8 --- /dev/null +++ b/tools/rest/uploaded_file_test.go @@ -0,0 +1,84 @@ +package rest_test + +import ( + "bytes" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/pocketbase/pocketbase/tools/rest" +) + +func TestFindUploadedFiles(t *testing.T) { + // create a test temporary file + tmpFile, err := os.CreateTemp(os.TempDir(), "tmpfile-*.txt") + if err != nil { + t.Fatal(err) + } + if _, err := tmpFile.Write([]byte("test")); err != nil { + t.Fatal(err) + } + tmpFile.Seek(0, 0) + defer tmpFile.Close() + defer os.Remove(tmpFile.Name()) + // --- + + // stub multipart form file body + body := new(bytes.Buffer) + mp := multipart.NewWriter(body) + w, err := mp.CreateFormFile("test", tmpFile.Name()) + if err != nil { + t.Fatal(err) + } + if _, err := io.Copy(w, tmpFile); err != nil { + t.Fatal(err) + } + mp.Close() + // --- + + req := httptest.NewRequest(http.MethodPost, "/", body) + req.Header.Add("Content-Type", mp.FormDataContentType()) + + result, err := rest.FindUploadedFiles(req, "test") + if err != nil { + t.Fatal(err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 file, got %d", len(result)) + } + + if result[0].Header().Size != 4 { + t.Fatalf("Expected the file size to be 4 bytes, got %d", result[0].Header().Size) + } + + if !strings.HasSuffix(result[0].Name(), ".txt") { + t.Fatalf("Expected the file name to have suffix .txt - %v", result[0].Name()) + } + + if string(result[0].Bytes()) != "test" { + t.Fatalf("Expected the file content to be %q, got %q", "test", string(result[0].Bytes())) + } +} + +func TestFindUploadedFilesMissing(t *testing.T) { + body := new(bytes.Buffer) + mp := multipart.NewWriter(body) + mp.Close() + + req := httptest.NewRequest(http.MethodPost, "/", body) + req.Header.Add("Content-Type", mp.FormDataContentType()) + + result, err := rest.FindUploadedFiles(req, "test") + if err == nil { + t.Error("Expected error, got nil") + } + + if result != nil { + t.Errorf("Expected result to be nil, got %v", result) + } +} diff --git a/tools/routine/routine.go b/tools/routine/routine.go new file mode 100644 index 00000000..7974d58b --- /dev/null +++ b/tools/routine/routine.go @@ -0,0 +1,32 @@ +package routine + +import ( + "log" + "runtime/debug" + "sync" +) + +// FireAndForget executes `f()` in a new go routine and auto recovers if panic. +// +// **Note:** Use this only if you are not interested in the result of `f()` +// and don't want to block the parent go routine. +func FireAndForget(f func(), wg ...*sync.WaitGroup) { + if len(wg) > 0 && wg[0] != nil { + wg[0].Add(1) + } + + go func() { + if len(wg) > 0 && wg[0] != nil { + defer wg[0].Done() + } + + defer func() { + if err := recover(); err != nil { + log.Printf("RECOVERED FROM PANIC: %v", err) + log.Printf("%s\n", string(debug.Stack())) + } + }() + + f() + }() +} diff --git a/tools/routine/routine_test.go b/tools/routine/routine_test.go new file mode 100644 index 00000000..dcb6ace6 --- /dev/null +++ b/tools/routine/routine_test.go @@ -0,0 +1,27 @@ +package routine_test + +import ( + "sync" + "testing" + + "github.com/pocketbase/pocketbase/tools/routine" +) + +func TestFireAndForget(t *testing.T) { + called := false + + fn := func() { + called = true + panic("test") + } + + wg := &sync.WaitGroup{} + + routine.FireAndForget(fn, wg) + + wg.Wait() + + if !called { + t.Error("Expected fn to be called.") + } +} diff --git a/tools/search/filter.go b/tools/search/filter.go new file mode 100644 index 00000000..f2e76d1f --- /dev/null +++ b/tools/search/filter.go @@ -0,0 +1,198 @@ +package search + +import ( + "errors" + "fmt" + "strings" + + "github.com/ganigeorgiev/fexpr" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/security" + "github.com/pocketbase/pocketbase/tools/store" + "github.com/spf13/cast" +) + +// FilterData is a filter expession string following the `fexpr` package grammar. +// +// Example: +// var filter FilterData = "id = null || (name = 'test' && status = true)" +// resolver := search.NewSimpleFieldResolver("id", "name", "status") +// expr, err := filter.BuildExpr(resolver) +type FilterData string + +// parsedFilterData holds a cache with previously parsed filter data expressions +// (initialized with some prealocated empty data map) +var parsedFilterData = store.New(make(map[string][]fexpr.ExprGroup, 50)) + +// BuildExpr parses the current filter data and returns a new db WHERE expression. +func (f FilterData) BuildExpr(fieldResolver FieldResolver) (dbx.Expression, error) { + raw := string(f) + var data []fexpr.ExprGroup + + if parsedFilterData.Has(raw) { + data = parsedFilterData.Get(raw) + } else { + var err error + data, err = fexpr.Parse(raw) + if err != nil { + return nil, err + } + // store in cache + // (the limit size is arbitrary and it is there to prevent the cache growing too big) + parsedFilterData.SetIfLessThanLimit(raw, data, 500) + } + + return f.build(data, fieldResolver) +} + +func (f FilterData) build(data []fexpr.ExprGroup, fieldResolver FieldResolver) (dbx.Expression, error) { + if len(data) == 0 { + return nil, errors.New("Empty filter expression.") + } + + var result dbx.Expression + + for _, group := range data { + var expr dbx.Expression + var exprErr error + + switch item := group.Item.(type) { + case fexpr.Expr: + expr, exprErr = f.resolveTokenizedExpr(item, fieldResolver) + case fexpr.ExprGroup: + expr, exprErr = f.build([]fexpr.ExprGroup{item}, fieldResolver) + case []fexpr.ExprGroup: + expr, exprErr = f.build(item, fieldResolver) + default: + exprErr = errors.New("Unsupported expression item.") + } + + if exprErr != nil { + return nil, exprErr + } + + if group.Join == fexpr.JoinAnd { + result = dbx.And(result, expr) + } else { + result = dbx.Or(result, expr) + } + } + + return result, nil +} + +func (f FilterData) resolveTokenizedExpr(expr fexpr.Expr, fieldResolver FieldResolver) (dbx.Expression, error) { + lName, lParams, lErr := f.resolveToken(expr.Left, fieldResolver) + if lName == "" || lErr != nil { + return nil, fmt.Errorf("Invalid left operand %q - %v.", expr.Left.Literal, lErr) + } + + rName, rParams, rErr := f.resolveToken(expr.Right, fieldResolver) + if rName == "" || rErr != nil { + return nil, fmt.Errorf("Invalid right operand %q - %v.", expr.Right.Literal, rErr) + } + + // merge both operands parameters (if any) + params := dbx.Params{} + if len(lParams) > 0 { + for k, v := range lParams { + params[k] = v + } + } + if len(rParams) > 0 { + for k, v := range rParams { + params[k] = v + } + } + + switch expr.Op { + case fexpr.SignEq: + op := "=" + if strings.ToLower(lName) == "null" || strings.ToLower(rName) == "null" { + op = "IS" + } + return dbx.NewExp(fmt.Sprintf("%s %s %s", lName, op, rName), params), nil + case fexpr.SignNeq: + op := "!=" + if strings.ToLower(lName) == "null" || strings.ToLower(rName) == "null" { + op = "IS NOT" + } + return dbx.NewExp(fmt.Sprintf("%s %s %s", lName, op, rName), params), nil + case fexpr.SignLike: + // normalize operands and switch sides if the left operand is a number or text + if len(lParams) > 0 { + return dbx.NewExp(fmt.Sprintf("%s LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil + } + return dbx.NewExp(fmt.Sprintf("%s LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil + case fexpr.SignNlike: + // normalize operands and switch sides if the left operand is a number or text + if len(lParams) > 0 { + return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", rName, lName), f.normalizeLikeParams(params)), nil + } + return dbx.NewExp(fmt.Sprintf("%s NOT LIKE %s", lName, rName), f.normalizeLikeParams(params)), nil + case fexpr.SignLt: + return dbx.NewExp(fmt.Sprintf("%s < %s", lName, rName), params), nil + case fexpr.SignLte: + return dbx.NewExp(fmt.Sprintf("%s <= %s", lName, rName), params), nil + case fexpr.SignGt: + return dbx.NewExp(fmt.Sprintf("%s > %s", lName, rName), params), nil + case fexpr.SignGte: + return dbx.NewExp(fmt.Sprintf("%s >= %s", lName, rName), params), nil + } + + return nil, fmt.Errorf("Unknown expression operator %q", expr.Op) +} + +func (f FilterData) resolveToken(token fexpr.Token, fieldResolver FieldResolver) (name string, params dbx.Params, err error) { + if token.Type == fexpr.TokenIdentifier { + name, params, err := fieldResolver.Resolve(token.Literal) + + if name == "" || err != nil { + // if `null` field is missing, treat `null` identifier as NULL token + if strings.ToLower(token.Literal) == "null" { + return "NULL", nil, nil + } + + // if `true` field is missing, treat `true` identifier as TRUE token + if strings.ToLower(token.Literal) == "true" { + return "1", nil, nil + } + + // if `false` field is missing, treat `false` identifier as FALSE token + if strings.ToLower(token.Literal) == "false" { + return "0", nil, nil + } + + return "", nil, err + } + + return name, params, err + } + + if token.Type == fexpr.TokenNumber || token.Type == fexpr.TokenText { + placeholder := "t" + security.RandomString(7) + name := fmt.Sprintf("{:%s}", placeholder) + params := dbx.Params{placeholder: token.Literal} + return name, params, nil + } + + return "", nil, errors.New("Unresolvable token type.") +} + +func (f FilterData) normalizeLikeParams(params dbx.Params) dbx.Params { + result := dbx.Params{} + + if len(params) == 0 { + return result + } + + for k, v := range params { + vStr := cast.ToString(v) + if !strings.Contains(vStr, "%") { + vStr = "%" + vStr + "%" + } + result[k] = vStr + } + + return result +} diff --git a/tools/search/filter_test.go b/tools/search/filter_test.go new file mode 100644 index 00000000..9750426d --- /dev/null +++ b/tools/search/filter_test.go @@ -0,0 +1,104 @@ +package search_test + +import ( + "regexp" + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/search" +) + +func TestFilterDataBuildExpr(t *testing.T) { + resolver := search.NewSimpleFieldResolver("test1", "test2", "test3", "test4.sub") + + scenarios := []struct { + filterData search.FilterData + expectError bool + expectPattern string + }{ + // empty + {"", true, ""}, + // invalid format + {"(test1 > 1", true, ""}, + // invalid operator + {"test1 + 123", true, ""}, + // unknown field + {"test1 = 'example' && unknown > 1", true, ""}, + // simple expression + {"test1 > 1", false, + "^" + + regexp.QuoteMeta("[[test1]] > {:") + + ".+" + + regexp.QuoteMeta("}") + + "$", + }, + // complex expression + { + "((test1 > 1) || (test2 != 2)) && test3 ~ '%%example' && test4.sub = null", + false, + "^" + + regexp.QuoteMeta("((([[test1]] > {:") + + ".+" + + regexp.QuoteMeta("}) OR ([[test2]] != {:") + + ".+" + + regexp.QuoteMeta("})) AND ([[test3]] LIKE {:") + + ".+" + + regexp.QuoteMeta("})) AND ([[test4.sub]] IS NULL)") + + "$", + }, + // combination of special literals (null, true, false) + { + "test1=true && test2 != false && test3 = null || test4.sub != null", + false, + "^" + regexp.QuoteMeta("((([[test1]] = 1) AND ([[test2]] != 0)) AND ([[test3]] IS NULL)) OR ([[test4.sub]] IS NOT NULL)") + "$", + }, + // all operators + { + "(test1 = test2 || test2 != test3) && (test2 ~ 'example' || test2 !~ '%%abc') && 'switch1%%' ~ test1 && 'switch2' !~ test2 && test3 > 1 && test3 >= 0 && test3 <= 4 && 2 < 5", + false, + "^" + + regexp.QuoteMeta("(((((((([[test1]] = [[test2]]) OR ([[test2]] != [[test3]])) AND (([[test2]] LIKE {:") + + ".+" + + regexp.QuoteMeta("}) OR ([[test2]] NOT LIKE {:") + + ".+" + + regexp.QuoteMeta("}))) AND ([[test1]] LIKE {:") + + ".+" + + regexp.QuoteMeta("})) AND ([[test2]] NOT LIKE {:") + + ".+" + + regexp.QuoteMeta("})) AND ([[test3]] > {:") + + ".+" + + regexp.QuoteMeta("})) AND ([[test3]] >= {:") + + ".+" + + regexp.QuoteMeta("})) AND ([[test3]] <= {:") + + ".+" + + regexp.QuoteMeta("})) AND ({:") + + ".+" + + regexp.QuoteMeta("} < {:") + + ".+" + + regexp.QuoteMeta("})") + + "$", + }, + } + + for i, s := range scenarios { + expr, err := s.filterData.BuildExpr(resolver) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if hasErr { + continue + } + + dummyDB := &dbx.DB{} + rawSql := expr.Build(dummyDB, map[string]any{}) + + pattern := regexp.MustCompile(s.expectPattern) + if !pattern.MatchString(rawSql) { + t.Errorf("(%d) Pattern %v don't match with expression: \n%v", i, s.expectPattern, rawSql) + } + } +} diff --git a/tools/search/provider.go b/tools/search/provider.go new file mode 100644 index 00000000..a9fb9119 --- /dev/null +++ b/tools/search/provider.go @@ -0,0 +1,245 @@ +package search + +import ( + "errors" + "math" + "net/url" + "strconv" + + "github.com/pocketbase/dbx" +) + +// DefaultPerPage specifies the default returned search result items. +const DefaultPerPage int = 30 + +// MaxPerPage specifies the maximum allowed search result items returned in a single page. +const MaxPerPage int = 200 + +// url search query params +const ( + PageQueryParam string = "page" + PerPageQueryParam string = "perPage" + SortQueryParam string = "sort" + FilterQueryParam string = "filter" +) + +// Result defines the returned search result structure. +type Result struct { + Page int `json:"page"` + PerPage int `json:"perPage"` + TotalItems int `json:"totalItems"` + Items any `json:"items"` +} + +// Provider represents a single configured search provider instance. +type Provider struct { + fieldResolver FieldResolver + query *dbx.SelectQuery + page int + perPage int + sort []SortField + filter []FilterData +} + +// NewProvider creates and returns a new search provider. +// +// Example: +// baseQuery := db.Select("*").From("user") +// fieldResolver := search.NewSimpleFieldResolver("id", "name") +// models := []*YourDataStruct{} +// +// result, err := search.NewProvider(fieldResolver). +// Query(baseQuery). +// ParseAndExec("page=2&filter=id>0&sort=-name", &models) +func NewProvider(fieldResolver FieldResolver) *Provider { + return &Provider{ + fieldResolver: fieldResolver, + page: 1, + perPage: DefaultPerPage, + sort: []SortField{}, + filter: []FilterData{}, + } +} + +// Query sets the base query that will be used to fetch the search items. +func (s *Provider) Query(query *dbx.SelectQuery) *Provider { + s.query = query + return s +} + +// Page sets the `page` field of the current search provider. +// +// Normalization on the `page` value is done during `Exec()`. +func (s *Provider) Page(page int) *Provider { + s.page = page + return s +} + +// PerPage sets the `perPage` field of the current search provider. +// +// Normalization on the `perPage` value is done during `Exec()`. +func (s *Provider) PerPage(perPage int) *Provider { + s.perPage = perPage + return s +} + +// Sort sets the `sort` field of the current search provider. +func (s *Provider) Sort(sort []SortField) *Provider { + s.sort = sort + return s +} + +// AddSort appends the provided SortField to the existing provider's sort field. +func (s *Provider) AddSort(field SortField) *Provider { + s.sort = append(s.sort, field) + return s +} + +// Filter sets the `filter` field of the current search provider. +func (s *Provider) Filter(filter []FilterData) *Provider { + s.filter = filter + return s +} + +// AddFilter appends the provided FilterData to the existing provider's filter field. +func (s *Provider) AddFilter(filter FilterData) *Provider { + if filter != "" { + s.filter = append(s.filter, filter) + } + return s +} + +// Parse parses the search query parameter from the provided query string +// and assigns the found fields to the current search provider. +// +// The data from the "sort" and "filter" query parameters are appended +// to the existing provider's `sort` and `filter` fields +// (aka. using `AddSort` and `AddFilter`). +func (s *Provider) Parse(urlQuery string) error { + params, err := url.ParseQuery(urlQuery) + if err != nil { + return err + } + + rawPage := params.Get(PageQueryParam) + if rawPage != "" { + page, err := strconv.Atoi(rawPage) + if err != nil { + return err + } + s.Page(page) + } + + rawPerPage := params.Get(PerPageQueryParam) + if rawPerPage != "" { + perPage, err := strconv.Atoi(rawPerPage) + if err != nil { + return err + } + s.PerPage(perPage) + } + + rawSort := params.Get(SortQueryParam) + if rawSort != "" { + for _, sortField := range ParseSortFromString(rawSort) { + s.AddSort(sortField) + } + } + + rawFilter := params.Get(FilterQueryParam) + if rawFilter != "" { + s.AddFilter(FilterData(rawFilter)) + } + + return nil +} + +// Exec executes the search provider and fills/scans +// the provided `items` slice with the found models. +func (s *Provider) Exec(items any) (*Result, error) { + if s.query == nil { + return nil, errors.New("Query is not set.") + } + + // clone provider's query + modelsQuery := *s.query + + // apply filters + if len(s.filter) > 0 { + for _, f := range s.filter { + expr, err := f.BuildExpr(s.fieldResolver) + if err != nil { + return nil, err + } + if expr != nil { + modelsQuery.AndWhere(expr) + } + } + } + + // apply sorting + if len(s.sort) > 0 { + for _, sortField := range s.sort { + expr, err := sortField.BuildExpr(s.fieldResolver) + if err != nil { + return nil, err + } + if expr != "" { + modelsQuery.AndOrderBy(expr) + } + } + } + + // apply field resolver query modifications (if any) + updateQueryErr := s.fieldResolver.UpdateQuery(&modelsQuery) + if updateQueryErr != nil { + return nil, updateQueryErr + } + + // count + var totalCount int64 + countQuery := modelsQuery + if err := countQuery.Select("count(*)").Row(&totalCount); err != nil { + return nil, err + } + + // normalize perPage + if s.perPage <= 0 { + s.perPage = DefaultPerPage + } else if s.perPage > MaxPerPage { + s.perPage = MaxPerPage + } + + // normalize page accoring to the total count + if s.page <= 0 || totalCount == 0 { + s.page = 1 + } else if totalPages := int(math.Ceil(float64(totalCount) / float64(s.perPage))); s.page > totalPages { + s.page = totalPages + } + + // apply pagination + modelsQuery.Limit(int64(s.perPage)) + modelsQuery.Offset(int64(s.perPage * (s.page - 1))) + + // fetch models + if err := modelsQuery.All(items); err != nil { + return nil, err + } + + return &Result{ + Page: s.page, + PerPage: s.perPage, + TotalItems: int(totalCount), + Items: items, + }, nil +} + +// ParseAndExec is a short conventient method to trigger both +// `Parse()` and `Exec()` in a single call. +func (s *Provider) ParseAndExec(urlQuery string, modelsSlice any) (*Result, error) { + if err := s.Parse(urlQuery); err != nil { + return nil, err + } + + return s.Exec(modelsSlice) +} diff --git a/tools/search/provider_test.go b/tools/search/provider_test.go new file mode 100644 index 00000000..222878a7 --- /dev/null +++ b/tools/search/provider_test.go @@ -0,0 +1,505 @@ +package search + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/list" + _ "modernc.org/sqlite" +) + +func TestNewProvider(t *testing.T) { + r := &testFieldResolver{} + p := NewProvider(r) + + if p.page != 1 { + t.Fatalf("Expected page %d, got %d", 1, p.page) + } + + if p.perPage != DefaultPerPage { + t.Fatalf("Expected perPage %d, got %d", DefaultPerPage, p.perPage) + } +} + +func TestProviderQuery(t *testing.T) { + db := dbx.NewFromDB(nil, "") + query := db.Select("id").From("test") + querySql := query.Build().SQL() + + r := &testFieldResolver{} + p := NewProvider(r).Query(query) + + expected := p.query.Build().SQL() + + if querySql != expected { + t.Fatalf("Expected %v, got %v", expected, querySql) + } +} + +func TestProviderPage(t *testing.T) { + r := &testFieldResolver{} + p := NewProvider(r).Page(10) + + if p.page != 10 { + t.Fatalf("Expected page %v, got %v", 10, p.page) + } +} + +func TestProviderPerPage(t *testing.T) { + r := &testFieldResolver{} + p := NewProvider(r).PerPage(456) + + if p.perPage != 456 { + t.Fatalf("Expected perPage %v, got %v", 456, p.perPage) + } +} + +func TestProviderSort(t *testing.T) { + initialSort := []SortField{{"test1", SortAsc}, {"test2", SortAsc}} + r := &testFieldResolver{} + p := NewProvider(r). + Sort(initialSort). + AddSort(SortField{"test3", SortDesc}) + + encoded, _ := json.Marshal(p.sort) + expected := `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"ASC"},{"name":"test3","direction":"DESC"}]` + + if string(encoded) != expected { + t.Fatalf("Expected sort %v, got \n%v", expected, string(encoded)) + } +} + +func TestProviderFilter(t *testing.T) { + initialFilter := []FilterData{"test1", "test2"} + r := &testFieldResolver{} + p := NewProvider(r). + Filter(initialFilter). + AddFilter("test3") + + encoded, _ := json.Marshal(p.filter) + expected := `["test1","test2","test3"]` + + if string(encoded) != expected { + t.Fatalf("Expected filter %v, got \n%v", expected, string(encoded)) + } +} + +func TestProviderParse(t *testing.T) { + initialPage := 2 + initialPerPage := 123 + initialSort := []SortField{{"test1", SortAsc}, {"test2", SortAsc}} + initialFilter := []FilterData{"test1", "test2"} + + scenarios := []struct { + query string + expectError bool + expectPage int + expectPerPage int + expectSort string + expectFilter string + }{ + // empty + { + "", + false, + initialPage, + initialPerPage, + `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"ASC"}]`, + `["test1","test2"]`, + }, + // invalid query + { + "invalid;", + true, + initialPage, + initialPerPage, + `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"ASC"}]`, + `["test1","test2"]`, + }, + // invalid page + { + "page=a", + true, + initialPage, + initialPerPage, + `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"ASC"}]`, + `["test1","test2"]`, + }, + // invalid perPage + { + "perPage=a", + true, + initialPage, + initialPerPage, + `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"ASC"}]`, + `["test1","test2"]`, + }, + // valid query parameters + { + "page=3&perPage=456&filter=test3&sort=-a,b,+c&other=123", + false, + 3, + 456, + `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"ASC"},{"name":"a","direction":"DESC"},{"name":"b","direction":"ASC"},{"name":"c","direction":"ASC"}]`, + `["test1","test2","test3"]`, + }, + } + + for i, s := range scenarios { + r := &testFieldResolver{} + p := NewProvider(r). + Page(initialPage). + PerPage(initialPerPage). + Sort(initialSort). + Filter(initialFilter) + + err := p.Parse(s.query) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if p.page != s.expectPage { + t.Errorf("(%d) Expected page %v, got %v", i, s.expectPage, p.page) + } + + if p.perPage != s.expectPerPage { + t.Errorf("(%d) Expected perPage %v, got %v", i, s.expectPerPage, p.perPage) + } + + encodedSort, _ := json.Marshal(p.sort) + if string(encodedSort) != s.expectSort { + t.Errorf("(%d) Expected sort %v, got \n%v", i, s.expectSort, string(encodedSort)) + } + + encodedFilter, _ := json.Marshal(p.filter) + if string(encodedFilter) != s.expectFilter { + t.Errorf("(%d) Expected filter %v, got \n%v", i, s.expectFilter, string(encodedFilter)) + } + } +} + +func TestProviderExecEmptyQuery(t *testing.T) { + p := NewProvider(&testFieldResolver{}). + Query(nil) + + _, err := p.Exec(&[]testTableStruct{}) + if err == nil { + t.Fatalf("Expected error with empty query, got nil") + } +} + +func TestProviderExecNonEmptyQuery(t *testing.T) { + testDB, err := createTestDB() + if err != nil { + t.Fatal(err) + } + defer testDB.Close() + + query := testDB.Select("*"). + From("test"). + Where(dbx.Not(dbx.HashExp{"test1": nil})). + OrderBy("test1 ASC") + + scenarios := []struct { + page int + perPage int + sort []SortField + filter []FilterData + expectError bool + expectResult string + expectQueries []string + }{ + // page normalization + { + -1, + 10, + []SortField{}, + []FilterData{}, + false, + `{"page":1,"perPage":10,"totalItems":2,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`, + []string{ + "SELECT count(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC", + "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 10", + }, + }, + // perPage normalization + { + 10, // will be capped by total count + 0, // fallback to default + []SortField{}, + []FilterData{}, + false, + `{"page":1,"perPage":30,"totalItems":2,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`, + []string{ + "SELECT count(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC", + "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 30", + }, + }, + // invalid sort field + { + 1, + 10, + []SortField{{"unknown", SortAsc}}, + []FilterData{}, + true, + "", + nil, + }, + // invalid filter + { + 1, + 10, + []SortField{}, + []FilterData{"test2 = 'test2.1'", "invalid"}, + true, + "", + nil, + }, + // valid sort and filter fields + { + 1, + 5555, // will be limited by MaxPerPage + []SortField{{"test2", SortDesc}}, + []FilterData{"test2 != null", "test1 >= 2"}, + false, + `{"page":1,"perPage":` + fmt.Sprint(MaxPerPage) + `,"totalItems":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + []string{ + "SELECT count(*) FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (test2 IS NOT null)) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC", + "SELECT * FROM `test` WHERE ((NOT (`test1` IS NULL)) AND (test2 IS NOT null)) AND (test1 >= '2') ORDER BY `test1` ASC, `test2` DESC LIMIT 200", + }, + }, + // valid sort and filter fields (zero results) + { + 1, + 10, + []SortField{{"test3", SortAsc}}, + []FilterData{"test3 != ''"}, + false, + `{"page":1,"perPage":10,"totalItems":0,"items":[]}`, + []string{ + "SELECT count(*) FROM `test` WHERE (NOT (`test1` IS NULL)) AND (test3 != '') ORDER BY `test1` ASC, `test3` ASC", + "SELECT * FROM `test` WHERE (NOT (`test1` IS NULL)) AND (test3 != '') ORDER BY `test1` ASC, `test3` ASC LIMIT 10", + }, + }, + // pagination test + { + 3, + 1, + []SortField{}, + []FilterData{}, + false, + `{"page":2,"perPage":1,"totalItems":2,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + []string{ + "SELECT count(*) FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC", + "SELECT * FROM `test` WHERE NOT (`test1` IS NULL) ORDER BY `test1` ASC LIMIT 1 OFFSET 1", + }, + }, + } + + for i, s := range scenarios { + testDB.CalledQueries = []string{} // reset + + testResolver := &testFieldResolver{} + p := NewProvider(testResolver). + Query(query). + Page(s.page). + PerPage(s.perPage). + Sort(s.sort). + Filter(s.filter) + + result, err := p.Exec(&[]testTableStruct{}) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if hasErr { + continue + } + + if testResolver.UpdateQueryCalls != 1 { + t.Errorf("(%d) Expected resolver.Update to be called %d, got %d", i, 1, testResolver.UpdateQueryCalls) + } + + encoded, _ := json.Marshal(result) + if string(encoded) != s.expectResult { + t.Errorf("(%d) Expected result %v, got \n%v", i, s.expectResult, string(encoded)) + } + + if len(s.expectQueries) != len(testDB.CalledQueries) { + t.Errorf("(%d) Expected %d queries, got %d: \n%v", i, len(s.expectQueries), len(testDB.CalledQueries), testDB.CalledQueries) + continue + } + + for _, q := range testDB.CalledQueries { + if !list.ExistInSliceWithRegex(q, s.expectQueries) { + t.Errorf("(%d) Didn't expect query %v", i, q) + } + } + } +} + +func TestProviderParseAndExec(t *testing.T) { + testDB, err := createTestDB() + if err != nil { + t.Fatal(err) + } + defer testDB.Close() + + query := testDB.Select("*"). + From("test"). + Where(dbx.Not(dbx.HashExp{"test1": nil})). + OrderBy("test1 ASC") + + scenarios := []struct { + queryString string + expectError bool + expectResult string + }{ + // empty + { + "", + false, + `{"page":1,"perPage":123,"totalItems":2,"items":[{"test1":1,"test2":"test2.1","test3":""},{"test1":2,"test2":"test2.2","test3":""}]}`, + }, + // invalid query + { + "invalid;", + true, + "", + }, + // invalid page + { + "page=a", + true, + "", + }, + // invalid perPage + { + "perPage=a", + true, + "", + }, + // invalid sorting field + { + "sort=-unknown", + true, + "", + }, + // invalid filter field + { + "filter=unknown>1", + true, + "", + }, + // valid query params + { + "page=3&perPage=555&filter=test1>1&sort=-test2,test3", + false, + `{"page":1,"perPage":200,"totalItems":1,"items":[{"test1":2,"test2":"test2.2","test3":""}]}`, + }, + } + + for i, s := range scenarios { + testDB.CalledQueries = []string{} // reset + + testResolver := &testFieldResolver{} + provider := NewProvider(testResolver). + Query(query). + Page(2). + PerPage(123). + Sort([]SortField{{"test2", SortAsc}}). + Filter([]FilterData{"test1 > 0"}) + + result, err := provider.ParseAndExec(s.queryString, &[]testTableStruct{}) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if hasErr { + continue + } + + if testResolver.UpdateQueryCalls != 1 { + t.Errorf("(%d) Expected resolver.Update to be called %d, got %d", i, 1, testResolver.UpdateQueryCalls) + } + + if len(testDB.CalledQueries) != 2 { + t.Errorf("(%d) Expected %d db queries, got %d: \n%v", i, 2, len(testDB.CalledQueries), testDB.CalledQueries) + } + + encoded, _ := json.Marshal(result) + if string(encoded) != s.expectResult { + t.Errorf("(%d) Expected result %v, got \n%v", i, s.expectResult, string(encoded)) + } + } +} + +// ------------------------------------------------------------------- +// Helpers +// ------------------------------------------------------------------- + +type testTableStruct struct { + Test1 int `db:"test1" json:"test1"` + Test2 string `db:"test2" json:"test2"` + Test3 string `db:"test3" json:"test3"` +} + +type testDB struct { + *dbx.DB + CalledQueries []string +} + +// NB! Don't forget to call `db.Close()` at the end of the test. +func createTestDB() (*testDB, error) { + sqlDB, err := sql.Open("sqlite", ":memory:") + if err != nil { + return nil, err + } + + db := testDB{DB: dbx.NewFromDB(sqlDB, "sqlite")} + db.CreateTable("test", map[string]string{"test1": "int default 0", "test2": "text default ''", "test3": "text default ''"}).Execute() + db.Insert("test", dbx.Params{"test1": 1, "test2": "test2.1"}).Execute() + db.Insert("test", dbx.Params{"test1": 2, "test2": "test2.2"}).Execute() + db.QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) { + db.CalledQueries = append(db.CalledQueries, sql) + } + + return &db, nil +} + +// --- + +type testFieldResolver struct { + UpdateQueryCalls int + ResolveCalls int +} + +func (t *testFieldResolver) UpdateQuery(query *dbx.SelectQuery) error { + t.UpdateQueryCalls++ + return nil +} + +func (t *testFieldResolver) Resolve(field string) (name string, placeholderParams dbx.Params, err error) { + t.ResolveCalls++ + + if field == "unknown" { + return "", nil, errors.New("test error") + } + + return field, nil, nil +} diff --git a/tools/search/simple_field_resolver.go b/tools/search/simple_field_resolver.go new file mode 100644 index 00000000..4e07fb4a --- /dev/null +++ b/tools/search/simple_field_resolver.go @@ -0,0 +1,58 @@ +package search + +import ( + "fmt" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/inflector" + "github.com/pocketbase/pocketbase/tools/list" +) + +// FieldResolver defines an interface for managing search fields. +type FieldResolver interface { + // UpdateQuery allows to updated the provided db query based on the + // resolved search fields (eg. adding joins aliases, etc.). + // + // Called internally by `search.Provider` before executing the search request. + UpdateQuery(query *dbx.SelectQuery) error + + // Resolve parses the provided field and returns a properly + // formatted db identifier (eg. NULL, quoted column, placeholder parameter, etc.). + Resolve(field string) (name string, placeholderParams dbx.Params, err error) +} + +// NewSimpleFieldResolver creates a new `SimpleFieldResolver` with the +// provided `allowedFields`. +// +// Each `allowedFields` could be a plain string (eg. "name") +// or a regexp pattern (eg. `^\w+[\w\.]*$`). +func NewSimpleFieldResolver(allowedFields ...string) *SimpleFieldResolver { + return &SimpleFieldResolver{ + allowedFields: allowedFields, + } +} + +// SimpleFieldResolver defines a generic search resolver that allows +// only its listed fields to be resolved and take part in a search query. +// +// If `allowedFields` are empty no fields filtering is applied. +type SimpleFieldResolver struct { + allowedFields []string +} + +// UpdateQuery implements `search.UpdateQuery` interface. +func (r *SimpleFieldResolver) UpdateQuery(query *dbx.SelectQuery) error { + // nothing to update... + return nil +} + +// Resolve implements `search.Resolve` interface. +// +// Returns error if `field` is not in `r.allowedFields`. +func (r *SimpleFieldResolver) Resolve(field string) (resultName string, placeholderParams dbx.Params, err error) { + if !list.ExistInSliceWithRegex(field, r.allowedFields) { + return "", nil, fmt.Errorf("Failed to resolve field %q.", field) + } + + return fmt.Sprintf("[[%s]]", inflector.Columnify(field)), nil, nil +} diff --git a/tools/search/simple_field_resolver_test.go b/tools/search/simple_field_resolver_test.go new file mode 100644 index 00000000..f3f0f8ff --- /dev/null +++ b/tools/search/simple_field_resolver_test.go @@ -0,0 +1,81 @@ +package search_test + +import ( + "testing" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/tools/search" +) + +func TestSimpleFieldResolverUpdateQuery(t *testing.T) { + r := search.NewSimpleFieldResolver("test") + + scenarios := []struct { + fieldName string + expectQuery string + }{ + // missing field (the query shouldn't change) + {"", `SELECT "id" FROM "test"`}, + // unknown field (the query shouldn't change) + {"unknown", `SELECT "id" FROM "test"`}, + // allowed field (the query shouldn't change) + {"test", `SELECT "id" FROM "test"`}, + } + + for i, s := range scenarios { + db := dbx.NewFromDB(nil, "") + query := db.Select("id").From("test") + + r.Resolve(s.fieldName) + + if err := r.UpdateQuery(nil); err != nil { + t.Errorf("(%d) UpdateQuery failed with error %v", i, err) + continue + } + + rawQuery := query.Build().SQL() + // rawQuery := s.expectQuery + + if rawQuery != s.expectQuery { + t.Errorf("(%d) Expected query %v, got \n%v", i, s.expectQuery, rawQuery) + } + } +} + +func TestSimpleFieldResolverResolve(t *testing.T) { + r := search.NewSimpleFieldResolver("test", `^test_regex\d+$`, "Test columnify!") + + scenarios := []struct { + fieldName string + expectError bool + expectName string + }{ + {"", true, ""}, + {" ", true, ""}, + {"unknown", true, ""}, + {"test", false, "[[test]]"}, + {"test.sub", true, ""}, + {"test_regex", true, ""}, + {"test_regex1", false, "[[test_regex1]]"}, + {"Test columnify!", false, "[[Testcolumnify]]"}, + } + + for i, s := range scenarios { + name, params, err := r.Resolve(s.fieldName) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if name != s.expectName { + t.Errorf("(%d) Expected name %q, got %q", i, s.expectName, name) + } + + // params should be empty + if len(params) != 0 { + t.Errorf("(%d) Expected 0 params, got %v", i, params) + } + } +} diff --git a/tools/search/sort.go b/tools/search/sort.go new file mode 100644 index 00000000..cc48b8f9 --- /dev/null +++ b/tools/search/sort.go @@ -0,0 +1,59 @@ +package search + +import ( + "fmt" + "strings" +) + +// sort field directions +const ( + SortAsc string = "ASC" + SortDesc string = "DESC" +) + +// SortField defines a single search sort field. +type SortField struct { + Name string `json:"name"` + Direction string `json:"direction"` +} + +// BuildExpr resolves the sort field into a valid db sort expression. +func (s *SortField) BuildExpr(fieldResolver FieldResolver) (string, error) { + name, params, err := fieldResolver.Resolve(s.Name) + + // invalidate empty fields and non-column identifiers + if err != nil || len(params) > 0 || name == "" || strings.ToLower(name) == "null" { + return "", fmt.Errorf("Invalid sort field %q.", s.Name) + } + + return fmt.Sprintf("%s %s", name, s.Direction), nil +} + +// ParseSortFromString parses the provided string expression +// into a slice of SortFields. +// +// Example: +// fields := search.ParseSortFromString("-name,+created") +func ParseSortFromString(str string) []SortField { + result := []SortField{} + + data := strings.Split(str, ",") + + for _, field := range data { + // trim whitespaces + field = strings.TrimSpace(field) + + var dir string + if strings.HasPrefix(field, "-") { + dir = SortDesc + field = strings.TrimPrefix(field, "-") + } else { + dir = SortAsc + field = strings.TrimPrefix(field, "+") + } + + result = append(result, SortField{field, dir}) + } + + return result +} diff --git a/tools/search/sort_test.go b/tools/search/sort_test.go new file mode 100644 index 00000000..83323624 --- /dev/null +++ b/tools/search/sort_test.go @@ -0,0 +1,67 @@ +package search_test + +import ( + "encoding/json" + "testing" + + "github.com/pocketbase/pocketbase/tools/search" +) + +func TestSortFieldBuildExpr(t *testing.T) { + resolver := search.NewSimpleFieldResolver("test1", "test2", "test3", "test4.sub") + + scenarios := []struct { + sortField search.SortField + expectError bool + expectExpression string + }{ + // empty + {search.SortField{"", search.SortDesc}, true, ""}, + // unknown field + {search.SortField{"unknown", search.SortAsc}, true, ""}, + // placeholder field + {search.SortField{"'test'", search.SortAsc}, true, ""}, + // null field + {search.SortField{"null", search.SortAsc}, true, ""}, + // allowed field - asc + {search.SortField{"test1", search.SortAsc}, false, "[[test1]] ASC"}, + // allowed field - desc + {search.SortField{"test1", search.SortDesc}, false, "[[test1]] DESC"}, + } + + for i, s := range scenarios { + result, err := s.sortField.BuildExpr(resolver) + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err) + continue + } + + if result != s.expectExpression { + t.Errorf("(%d) Expected expression %v, got %v", i, s.expectExpression, result) + } + } +} + +func TestParseSortFromString(t *testing.T) { + scenarios := []struct { + value string + expectedJson string + }{ + {"", `[{"name":"","direction":"ASC"}]`}, + {"test", `[{"name":"test","direction":"ASC"}]`}, + {"+test", `[{"name":"test","direction":"ASC"}]`}, + {"-test", `[{"name":"test","direction":"DESC"}]`}, + {"test1,-test2,+test3", `[{"name":"test1","direction":"ASC"},{"name":"test2","direction":"DESC"},{"name":"test3","direction":"ASC"}]`}, + } + + for i, s := range scenarios { + result := search.ParseSortFromString(s.value) + encoded, _ := json.Marshal(result) + + if string(encoded) != s.expectedJson { + t.Errorf("(%d) Expected expression %v, got %v", i, s.expectedJson, string(encoded)) + } + } +} diff --git a/tools/security/encrypt.go b/tools/security/encrypt.go new file mode 100644 index 00000000..2dfe2b9e --- /dev/null +++ b/tools/security/encrypt.go @@ -0,0 +1,75 @@ +package security + +import ( + "crypto/aes" + "crypto/cipher" + crand "crypto/rand" + "crypto/sha256" + "encoding/base64" + "io" + "strings" +) + +// S256Challenge creates base64 encoded sha256 challenge string derived from code. +// The padding of the result base64 string is stripped per [RFC 7636]. +// +// [RFC 7636]: https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 +func S256Challenge(code string) string { + h := sha256.New() + h.Write([]byte(code)) + return strings.TrimRight(base64.URLEncoding.EncodeToString(h.Sum(nil)), "=") +} + +// Encrypt encrypts data with key (must be valid 32 char aes key). +func Encrypt(data []byte, key string) (string, error) { + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, gcm.NonceSize()) + + // populates the nonce with a cryptographically secure random sequence + if _, err := io.ReadFull(crand.Reader, nonce); err != nil { + return "", err + } + + cipherByte := gcm.Seal(nonce, nonce, data, nil) + + result := base64.StdEncoding.EncodeToString(cipherByte) + + return result, nil +} + +// Decrypt decrypts encrypted text with key (must be valid 32 chars aes key). +func Decrypt(cipherText string, key string) ([]byte, error) { + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + + cipherByte, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + return nil, err + } + + nonce, cipherByteClean := cipherByte[:nonceSize], cipherByte[nonceSize:] + plainData, err := gcm.Open(nil, nonce, cipherByteClean, nil) + if err != nil { + return nil, err + } + + return plainData, nil +} diff --git a/tools/security/encrypt_test.go b/tools/security/encrypt_test.go new file mode 100644 index 00000000..614601da --- /dev/null +++ b/tools/security/encrypt_test.go @@ -0,0 +1,93 @@ +package security_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestS256Challenge(t *testing.T) { + scenarios := []struct { + code string + expected string + }{ + {"", "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"}, + {"123", "pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM"}, + } + + for i, scenario := range scenarios { + result := security.S256Challenge(scenario.code) + + if result != scenario.expected { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expected, result) + } + } +} + +func TestEncrypt(t *testing.T) { + scenarios := []struct { + data string + key string + expectError bool + }{ + {"", "", true}, + {"123", "test", true}, // key must be valid 32 char aes string + {"123", "abcdabcdabcdabcdabcdabcdabcdabcd", false}, + } + + for i, scenario := range scenarios { + result, err := security.Encrypt([]byte(scenario.data), scenario.key) + + if scenario.expectError && err == nil { + t.Errorf("(%d) Expected error got nil", i) + } + if !scenario.expectError && err != nil { + t.Errorf("(%d) Expected nil got error %v", i, err) + } + + if scenario.expectError && result != "" { + t.Errorf("(%d) Expected empty string, got %q", i, result) + } + if !scenario.expectError && result == "" { + t.Errorf("(%d) Expected non empty encrypted result string", i) + } + + // try to decrypt + if result != "" { + decrypted, _ := security.Decrypt(result, scenario.key) + if string(decrypted) != scenario.data { + t.Errorf("(%d) Expected decrypted value to match with the data input, got %q", i, decrypted) + } + } + } +} + +func TestDecrypt(t *testing.T) { + scenarios := []struct { + cipher string + key string + expectError bool + expectedData string + }{ + {"", "", true, ""}, + {"123", "test", true, ""}, // key must be valid 32 char aes string + {"8kcEqilvvYKYcfnSr0aSC54gmnQCsB02SaB8ATlnA==", "abcdabcdabcdabcdabcdabcdabcdabcd", true, ""}, // illegal base64 encoded cipherText + {"8kcEqilvv+YKYcfnSr0aSC54gmnQCsB02SaB8ATlnA==", "abcdabcdabcdabcdabcdabcdabcdabcd", false, "123"}, + } + + for i, scenario := range scenarios { + result, err := security.Decrypt(scenario.cipher, scenario.key) + + if scenario.expectError && err == nil { + t.Errorf("(%d) Expected error got nil", i) + } + if !scenario.expectError && err != nil { + t.Errorf("(%d) Expected nil got error %v", i, err) + } + + resultStr := string(result) + if resultStr != scenario.expectedData { + t.Errorf("(%d) Expected %q, got %q", i, scenario.expectedData, resultStr) + } + } +} diff --git a/tools/security/jwt.go b/tools/security/jwt.go new file mode 100644 index 00000000..f8e5a72e --- /dev/null +++ b/tools/security/jwt.go @@ -0,0 +1,60 @@ +package security + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +// ParseUnverifiedJWT parses JWT token and returns its claims +// but DOES NOT verify the signature. +func ParseUnverifiedJWT(token string) (jwt.MapClaims, error) { + claims := jwt.MapClaims{} + + parser := &jwt.Parser{} + _, _, err := parser.ParseUnverified(token, claims) + + if err == nil { + err = claims.Valid() + } + + return claims, err +} + +// ParseJWT verifies and parses JWT token and returns its claims. +func ParseJWT(token string, verificationKey string) (jwt.MapClaims, error) { + parser := &jwt.Parser{ + ValidMethods: []string{"HS256"}, + } + + parsedToken, err := parser.Parse(token, func(t *jwt.Token) (any, error) { + return []byte(verificationKey), nil + }) + if err != nil { + return nil, err + } + + if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid { + return claims, nil + } + + return nil, errors.New("Unable to parse token.") +} + +// NewToken generates and returns new HS256 signed JWT token. +func NewToken(payload jwt.MapClaims, signingKey string, secondsDuration int64) (string, error) { + seconds := time.Duration(secondsDuration) * time.Second + + claims := jwt.MapClaims{ + "exp": time.Now().Add(seconds).Unix(), + } + + if len(payload) > 0 { + for k, v := range payload { + claims[k] = v + } + } + + return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(signingKey)) +} diff --git a/tools/security/jwt_test.go b/tools/security/jwt_test.go new file mode 100644 index 00000000..a523ad1a --- /dev/null +++ b/tools/security/jwt_test.go @@ -0,0 +1,179 @@ +package security_test + +import ( + "testing" + + "github.com/golang-jwt/jwt/v4" + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestParseUnverifiedJWT(t *testing.T) { + // invalid formatted JWT token + result1, err1 := security.ParseUnverifiedJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCJ9") + if err1 == nil { + t.Error("Expected error got nil") + } + if len(result1) > 0 { + t.Error("Expected no parsed claims, got", result1) + } + + // properly formatted JWT token with INVALID claims + // {"name": "test", "exp": 1516239022} + result2, err2 := security.ParseUnverifiedJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImV4cCI6MTUxNjIzOTAyMn0.xYHirwESfSEW3Cq2BL47CEASvD_p_ps3QCA54XtNktU") + if err2 == nil { + t.Error("Expected error got nil") + } + if len(result2) != 2 || result2["name"] != "test" { + t.Errorf("Expected to have 2 claims, got %v", result2) + } + + // properly formatted JWT token with VALID claims + // {"name": "test"} + result3, err3 := security.ParseUnverifiedJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCJ9.ml0QsTms3K9wMygTu41ZhKlTyjmW9zHQtoS8FUsCCjU") + if err3 != nil { + t.Error("Expected nil, got", err3) + } + if len(result3) != 1 || result3["name"] != "test" { + t.Errorf("Expected to have 2 claims, got %v", result3) + } +} + +func TestParseJWT(t *testing.T) { + scenarios := []struct { + token string + secret string + expectError bool + expectClaims jwt.MapClaims + }{ + // invalid formatted JWT token + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCJ9", + "test", + true, + nil, + }, + // properly formatted JWT token with INVALID claims and INVALID secret + // {"name": "test", "exp": 1516239022} + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImV4cCI6MTUxNjIzOTAyMn0.xYHirwESfSEW3Cq2BL47CEASvD_p_ps3QCA54XtNktU", + "invalid", + true, + nil, + }, + // properly formatted JWT token with INVALID claims and VALID secret + // {"name": "test", "exp": 1516239022} + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImV4cCI6MTUxNjIzOTAyMn0.xYHirwESfSEW3Cq2BL47CEASvD_p_ps3QCA54XtNktU", + "test", + true, + nil, + }, + // properly formatted JWT token with VALID claims and INVALID secret + // {"name": "test", "exp": 1898636137} + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImV4cCI6MTg5ODYzNjEzN30.gqRkHjpK5s1PxxBn9qPaWEWxTbpc1PPSD-an83TsXRY", + "invalid", + true, + nil, + }, + // properly formatted EXPIRED JWT token with VALID secret + // {"name": "test", "exp": 1652097610} + { + "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidGVzdCIsImV4cCI6OTU3ODczMzc0fQ.0oUUKUnsQHs4nZO1pnxQHahKtcHspHu4_AplN2sGC4A", + "test", + true, + nil, + }, + // properly formatted JWT token with VALID claims and VALID secret + // {"name": "test", "exp": 1898636137} + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCIsImV4cCI6MTg5ODYzNjEzN30.gqRkHjpK5s1PxxBn9qPaWEWxTbpc1PPSD-an83TsXRY", + "test", + false, + jwt.MapClaims{"name": "test", "exp": 1898636137.0}, + }, + // properly formatted JWT token with VALID claims (without exp) and VALID secret + // {"name": "test"} + { + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidGVzdCJ9.ml0QsTms3K9wMygTu41ZhKlTyjmW9zHQtoS8FUsCCjU", + "test", + false, + jwt.MapClaims{"name": "test"}, + }, + } + + for i, scenario := range scenarios { + result, err := security.ParseJWT(scenario.token, scenario.secret) + if scenario.expectError && err == nil { + t.Errorf("(%d) Expected error got nil", i) + } + if !scenario.expectError && err != nil { + t.Errorf("(%d) Expected nil got error %v", i, err) + } + if len(result) != len(scenario.expectClaims) { + t.Errorf("(%d) Expected %v got %v", i, scenario.expectClaims, result) + } + for k, v := range scenario.expectClaims { + v2, ok := result[k] + if !ok { + t.Errorf("(%d) Missing expected claim %q", i, k) + } + if v != v2 { + t.Errorf("(%d) Expected %v for %q claim, got %v", i, v, k, v2) + } + } + } +} + +func TestNewToken(t *testing.T) { + scenarios := []struct { + claims jwt.MapClaims + key string + duration int64 + expectError bool + }{ + // empty, zero duration + {jwt.MapClaims{}, "", 0, true}, + // empty, 10 seconds duration + {jwt.MapClaims{}, "", 10, false}, + // non-empty, 10 seconds duration + {jwt.MapClaims{"name": "test"}, "test", 10, false}, + } + + for i, scenario := range scenarios { + token, tokenErr := security.NewToken(scenario.claims, scenario.key, scenario.duration) + if tokenErr != nil { + t.Errorf("(%d) Expected NewToken to succeed, got error %v", i, tokenErr) + continue + } + + claims, parseErr := security.ParseJWT(token, scenario.key) + + hasParseErr := parseErr != nil + if hasParseErr != scenario.expectError { + t.Errorf("(%d) Expected hasParseErr to be %v, got %v (%v)", i, scenario.expectError, hasParseErr, parseErr) + continue + } + + if scenario.expectError { + continue + } + + if _, ok := claims["exp"]; !ok { + t.Errorf("(%d) Missing required claim exp, got %v", i, claims) + } + + // clear exp claim to match with the scenario ones + delete(claims, "exp") + + if len(claims) != len(scenario.claims) { + t.Errorf("(%d) Expected %v claims, got %v", i, scenario.claims, claims) + } + + for j, k := range claims { + if claims[j] != scenario.claims[j] { + t.Errorf("(%d) Expected %v for %q claim, got %v", i, claims[j], k, scenario.claims[j]) + } + } + } +} diff --git a/tools/security/random.go b/tools/security/random.go new file mode 100644 index 00000000..4a6ed33c --- /dev/null +++ b/tools/security/random.go @@ -0,0 +1,21 @@ +package security + +import ( + "crypto/rand" +) + +// RandomString generates a random string of specified length. +// +// The generated string is cryptographically random and matches +// [A-Za-z0-9]+ (aka. it's transparent to URL-encoding). +func RandomString(length int) string { + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + + bytes := make([]byte, length) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphabet[b%byte(len(alphabet))] + } + + return string(bytes) +} diff --git a/tools/security/random_test.go b/tools/security/random_test.go new file mode 100644 index 00000000..d4c40fb1 --- /dev/null +++ b/tools/security/random_test.go @@ -0,0 +1,33 @@ +package security_test + +import ( + "regexp" + "testing" + + "github.com/pocketbase/pocketbase/tools/security" +) + +func TestRandomString(t *testing.T) { + generated := []string{} + + for i := 0; i < 30; i++ { + length := 5 + i + result := security.RandomString(length) + + if len(result) != length { + t.Errorf("(%d) Expected the length of the string to be %d, got %d", i, length, len(result)) + } + + if match, _ := regexp.MatchString("[a-zA-Z0-9]+", result); !match { + t.Errorf("(%d) The generated strings should have only [a-zA-Z0-9]+ characters, got %q", i, result) + } + + for _, str := range generated { + if str == result { + t.Errorf("(%d) Repeating random string - found %q in \n%v", i, result, generated) + } + } + + generated = append(generated, result) + } +} diff --git a/tools/store/store.go b/tools/store/store.go new file mode 100644 index 00000000..22966fed --- /dev/null +++ b/tools/store/store.go @@ -0,0 +1,84 @@ +package store + +import "sync" + +// Store defines a concurrent safe in memory key-value data store. +type Store[T any] struct { + mux sync.RWMutex + data map[string]T +} + +// New creates a new Store[T] instance. +func New[T any](data map[string]T) *Store[T] { + return &Store[T]{data: data} +} + +// Remove removes a single entry from the store. +// +// Remove does nothing if key doesn't exist in the store. +func (s *Store[T]) Remove(key string) { + s.mux.Lock() + defer s.mux.Unlock() + + delete(s.data, key) +} + +// Has checks if element with the specified key exist or not. +func (s *Store[T]) Has(key string) bool { + s.mux.Lock() + defer s.mux.Unlock() + + _, ok := s.data[key] + + return ok +} + +// Get returns a single element value from the store. +// +// If key is not set, the zero T value is returned. +func (s *Store[T]) Get(key string) T { + s.mux.Lock() + defer s.mux.Unlock() + + return s.data[key] +} + +// Set sets (or overwrite if already exist) a new value for key. +func (s *Store[T]) Set(key string, value T) { + s.mux.Lock() + defer s.mux.Unlock() + + if s.data == nil { + s.data = make(map[string]T) + } + + s.data[key] = value +} + +// SetIfLessThanLimit sets (or overwrite if already exist) a new value for key. +// +// This is method is similar to Set() but **it will skip adding new elements** +// to the store if the store length has reached the specified limit. +// `false` is returned if maxAllowedElements limit is reached. +func (s *Store[T]) SetIfLessThanLimit(key string, value T, maxAllowedElements int) bool { + s.mux.Lock() + defer s.mux.Unlock() + + // init map if not already + if s.data == nil { + s.data = make(map[string]T) + } + + // check for existing item + _, ok := s.data[key] + + if !ok && len(s.data) >= maxAllowedElements { + // cannot add more items + return false + } + + // add/overwrite item + s.data[key] = value + + return true +} diff --git a/tools/store/store_test.go b/tools/store/store_test.go new file mode 100644 index 00000000..1627824a --- /dev/null +++ b/tools/store/store_test.go @@ -0,0 +1,126 @@ +package store_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/tools/store" +) + +func TestNew(t *testing.T) { + s := store.New(map[string]int{"test": 1}) + + if s.Get("test") != 1 { + t.Error("Expected the initizialized store map to be loaded") + } +} + +func TestRemove(t *testing.T) { + s := store.New(map[string]bool{"test": true}) + + keys := []string{"test", "missing"} + + for i, key := range keys { + s.Remove(key) + if s.Has(key) { + t.Errorf("(%d) Expected %q to be removed", i, key) + } + } +} + +func TestHas(t *testing.T) { + s := store.New(map[string]int{"test1": 0, "test2": 1}) + + scenarios := []struct { + key string + exist bool + }{ + {"test1", true}, + {"test2", true}, + {"missing", false}, + } + + for i, scenario := range scenarios { + exist := s.Has(scenario.key) + if exist != scenario.exist { + t.Errorf("(%d) Expected %v, got %v", i, scenario.exist, exist) + } + } +} + +func TestGet(t *testing.T) { + s := store.New(map[string]int{"test1": 0, "test2": 1}) + + scenarios := []struct { + key string + expect int + }{ + {"test1", 0}, + {"test2", 1}, + {"missing", 0}, // should auto fallback to the zero value + } + + for i, scenario := range scenarios { + val := s.Get(scenario.key) + if val != scenario.expect { + t.Errorf("(%d) Expected %v, got %v", i, scenario.expect, val) + } + } +} + +func TestSet(t *testing.T) { + s := store.New[int](nil) + + data := map[string]int{"test1": 0, "test2": 1, "test3": 3} + + // set values + for k, v := range data { + s.Set(k, v) + } + + // verify that the values are set + for k, v := range data { + if !s.Has(k) { + t.Errorf("Expected key %q", k) + } + + val := s.Get(k) + if val != v { + t.Errorf("Expected %v, got %v for key %q", v, val, k) + } + } +} + +func TestSetIfLessThanLimit(t *testing.T) { + s := store.New[int](nil) + + limit := 2 + + // set values + scenarios := []struct { + key string + value int + expected bool + }{ + {"test1", 1, true}, + {"test2", 2, true}, + {"test3", 3, false}, + {"test2", 4, true}, // overwrite + } + + for i, scenario := range scenarios { + result := s.SetIfLessThanLimit(scenario.key, scenario.value, limit) + + if result != scenario.expected { + t.Errorf("(%d) Expected result %v, got %v", i, scenario.expected, result) + } + + if !scenario.expected && s.Has(scenario.key) { + t.Errorf("(%d) Expected key %q to not be set", i, scenario.key) + } + + val := s.Get(scenario.key) + if scenario.expected && val != scenario.value { + t.Errorf("(%d) Expected value %v, got %v", i, scenario.value, val) + } + } +} diff --git a/tools/subscriptions/broker.go b/tools/subscriptions/broker.go new file mode 100644 index 00000000..0ca64871 --- /dev/null +++ b/tools/subscriptions/broker.go @@ -0,0 +1,58 @@ +package subscriptions + +import ( + "fmt" + "sync" +) + +// Broker defines a struct for managing subscriptions clients. +type Broker struct { + mux sync.RWMutex + clients map[string]Client +} + +// NewBroker initializes and returns a new Broker instance. +func NewBroker() *Broker { + return &Broker{ + clients: make(map[string]Client), + } +} + +// Clients returns all registered clients. +func (b *Broker) Clients() map[string]Client { + return b.clients +} + +// ClientById finds a registered client by its id. +// +// Returns non-nil error when client with clientId is not registered. +func (b *Broker) ClientById(clientId string) (Client, error) { + client, ok := b.clients[clientId] + if !ok { + return nil, fmt.Errorf("No client associated with connection ID %q", clientId) + } + + return client, nil +} + +// Register adds a new client to the broker instance. +func (b *Broker) Register(client Client) { + b.mux.Lock() + defer b.mux.Unlock() + + b.clients[client.Id()] = client +} + +// Unregister removes a single client by its id. +// +// If client with clientId doesn't exist, this method does nothing. +func (b *Broker) Unregister(clientId string) { + b.mux.Lock() + defer b.mux.Unlock() + + // Note: + // There is no need to explicitly close the client's channel since it will be GC-ed anyway. + // Addinitionally, closing the channel explicitly could panic when there are several + // subscriptions attached to the client that needs to receive the same event. + delete(b.clients, clientId) +} diff --git a/tools/subscriptions/broker_test.go b/tools/subscriptions/broker_test.go new file mode 100644 index 00000000..87774a61 --- /dev/null +++ b/tools/subscriptions/broker_test.go @@ -0,0 +1,86 @@ +package subscriptions_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +func TestNewBroker(t *testing.T) { + b := subscriptions.NewBroker() + + if b.Clients() == nil { + t.Fatal("Expected clients map to be initialized") + } +} + +func TestClients(t *testing.T) { + b := subscriptions.NewBroker() + + if total := len(b.Clients()); total != 0 { + t.Fatalf("Expected no clients, got %v", total) + } + + b.Register(subscriptions.NewDefaultClient()) + b.Register(subscriptions.NewDefaultClient()) + + if total := len(b.Clients()); total != 2 { + t.Fatalf("Expected 2 clients, got %v", total) + } +} + +func TestClientById(t *testing.T) { + b := subscriptions.NewBroker() + + clientA := subscriptions.NewDefaultClient() + clientB := subscriptions.NewDefaultClient() + b.Register(clientA) + b.Register(clientB) + + resultClient, err := b.ClientById(clientA.Id()) + if err != nil { + t.Fatalf("Expected client with id %s, got error %v", clientA.Id(), err) + } + if resultClient.Id() != clientA.Id() { + t.Fatalf("Expected client %s, got %s", clientA.Id(), resultClient.Id()) + } + + if c, err := b.ClientById("missing"); err == nil { + t.Fatalf("Expected error, found client %v", c) + } +} + +func TestRegister(t *testing.T) { + b := subscriptions.NewBroker() + + client := subscriptions.NewDefaultClient() + b.Register(client) + + if _, err := b.ClientById(client.Id()); err != nil { + t.Fatalf("Expected client with id %s, got error %v", client.Id(), err) + } +} + +func TestUnregister(t *testing.T) { + b := subscriptions.NewBroker() + + clientA := subscriptions.NewDefaultClient() + clientB := subscriptions.NewDefaultClient() + b.Register(clientA) + b.Register(clientB) + + if _, err := b.ClientById(clientA.Id()); err != nil { + t.Fatalf("Expected client with id %s, got error %v", clientA.Id(), err) + } + + b.Unregister(clientA.Id()) + + if c, err := b.ClientById(clientA.Id()); err == nil { + t.Fatalf("Expected error, found client %v", c) + } + + // clientB shouldn't have been removed + if _, err := b.ClientById(clientB.Id()); err != nil { + t.Fatalf("Expected client with id %s, got error %v", clientB.Id(), err) + } +} diff --git a/tools/subscriptions/client.go b/tools/subscriptions/client.go new file mode 100644 index 00000000..f95b1c31 --- /dev/null +++ b/tools/subscriptions/client.go @@ -0,0 +1,141 @@ +package subscriptions + +import ( + "sync" + + "github.com/pocketbase/pocketbase/tools/security" +) + +// Message defines a client's channel data. +type Message struct { + Name string + Data string +} + +// Client is an interface for a generic subscription client. +type Client interface { + // Id Returns the unique id of the client. + Id() string + + // Channel returns the client's communication channel. + Channel() chan Message + + // Subscriptions returns all subscriptions to which the client has subscribed to. + Subscriptions() map[string]struct{} + + // Subscribe subscribes the client to the provided subscriptions list. + Subscribe(subs ...string) + + // Unsubscribe unsubscribes the client from the provided subscriptions list. + Unsubscribe(subs ...string) + + // HasSubscription checks if the client is subscribed to `sub`. + HasSubscription(sub string) bool + + // Set stores any value to the client's context. + Set(key string, value any) + + // Get retrieves the key value from the client's context. + Get(key string) any +} + +// ensures that DefaultClient satisfies the Client interface +var _ Client = (*DefaultClient)(nil) + +// DefaultClient defines a generic subscription client. +type DefaultClient struct { + mux sync.RWMutex + id string + store map[string]any + channel chan Message + subscriptions map[string]struct{} +} + +// NewDefaultClient creates and returns a new DefaultClient instance. +func NewDefaultClient() *DefaultClient { + return &DefaultClient{ + id: security.RandomString(40), + store: map[string]any{}, + channel: make(chan Message), + subscriptions: make(map[string]struct{}), + } +} + +// Id implements the Client.Id interface method. +func (c *DefaultClient) Id() string { + return c.id +} + +// Channel implements the Client.Channel interface method. +func (c *DefaultClient) Channel() chan Message { + return c.channel +} + +// Subscriptions implements the Client.Subscriptions interface method. +func (c *DefaultClient) Subscriptions() map[string]struct{} { + c.mux.Lock() + defer c.mux.Unlock() + + return c.subscriptions +} + +// Subscribe implements the Client.Subscribe interface method. +// +// Empty subscriptions (aka. "") are ignored. +func (c *DefaultClient) Subscribe(subs ...string) { + c.mux.Lock() + defer c.mux.Unlock() + + for _, s := range subs { + if s == "" { + continue // skip empty + } + + c.subscriptions[s] = struct{}{} + } +} + +// Unsubscribe implements the Client.Unsubscribe interface method. +// +// If subs is not set, this method removes all registered client's subscriptions. +func (c *DefaultClient) Unsubscribe(subs ...string) { + c.mux.Lock() + defer c.mux.Unlock() + + if len(subs) > 0 { + for _, s := range subs { + delete(c.subscriptions, s) + } + } else { + // unsubsribe all + for s := range c.subscriptions { + delete(c.subscriptions, s) + } + } +} + +// HasSubscription implements the Client.HasSubscription interface method. +func (c *DefaultClient) HasSubscription(sub string) bool { + c.mux.Lock() + defer c.mux.Unlock() + + _, ok := c.subscriptions[sub] + + return ok +} + +// Get implements the Client.Get interface method. +func (c *DefaultClient) Get(key string) any { + c.mux.Lock() + defer c.mux.Unlock() + + return c.store[key] +} + +// Set implements the Client.Set interface method. +func (c *DefaultClient) Set(key string, value any) { + c.mux.Lock() + defer c.mux.Unlock() + + c.store[key] = value +} diff --git a/tools/subscriptions/client_test.go b/tools/subscriptions/client_test.go new file mode 100644 index 00000000..b26ae685 --- /dev/null +++ b/tools/subscriptions/client_test.go @@ -0,0 +1,131 @@ +package subscriptions_test + +import ( + "testing" + + "github.com/pocketbase/pocketbase/tools/subscriptions" +) + +func TestNewDefaultClient(t *testing.T) { + c := subscriptions.NewDefaultClient() + + if c.Channel() == nil { + t.Errorf("Expected channel to be initialized") + } + + if c.Subscriptions() == nil { + t.Errorf("Expected subscriptions map to be initialized") + } + + if c.Id() == "" { + t.Errorf("Expected unique id to be set") + } +} + +func TestId(t *testing.T) { + clients := []*subscriptions.DefaultClient{ + subscriptions.NewDefaultClient(), + subscriptions.NewDefaultClient(), + subscriptions.NewDefaultClient(), + subscriptions.NewDefaultClient(), + } + + ids := map[string]struct{}{} + for i, c := range clients { + // check uniqueness + if _, ok := ids[c.Id()]; ok { + t.Errorf("(%d) Expected unique id, got %v", i, c.Id()) + } else { + ids[c.Id()] = struct{}{} + } + + // check length + if len(c.Id()) != 40 { + t.Errorf("(%d) Expected unique id to have 40 chars length, got %v", i, c.Id()) + } + } +} + +func TestChannel(t *testing.T) { + c := subscriptions.NewDefaultClient() + + if c.Channel() == nil { + t.Errorf("Expected channel to be initialized, got") + } +} + +func TestSubscriptions(t *testing.T) { + c := subscriptions.NewDefaultClient() + + if len(c.Subscriptions()) != 0 { + t.Errorf("Expected subscriptions to be empty") + } + + c.Subscribe("sub1", "sub2", "sub3") + + if len(c.Subscriptions()) != 3 { + t.Errorf("Expected 3 subscriptions, got %v", c.Subscriptions()) + } +} + +func TestSubscribe(t *testing.T) { + c := subscriptions.NewDefaultClient() + + subs := []string{"", "sub1", "sub2", "sub3"} + expected := []string{"sub1", "sub2", "sub3"} + + c.Subscribe(subs...) // empty string should be skipped + + if len(c.Subscriptions()) != 3 { + t.Errorf("Expected 3 subscriptions, got %v", c.Subscriptions()) + } + + for i, s := range expected { + if !c.HasSubscription(s) { + t.Errorf("(%d) Expected sub %s", i, s) + } + } +} + +func TestUnsubscribe(t *testing.T) { + c := subscriptions.NewDefaultClient() + + c.Subscribe("sub1", "sub2", "sub3") + + c.Unsubscribe("sub1") + + if c.HasSubscription("sub1") { + t.Error("Expected sub1 to be removed") + } + + c.Unsubscribe( /* all */ ) + if len(c.Subscriptions()) != 0 { + t.Errorf("Expected all subscriptions to be removed, got %v", c.Subscriptions()) + } +} + +func TestHasSubscription(t *testing.T) { + c := subscriptions.NewDefaultClient() + + if c.HasSubscription("missing") { + t.Error("Expected false, got true") + } + + c.Subscribe("sub") + + if !c.HasSubscription("sub") { + t.Error("Expected true, got false") + } +} + +func TestSetAndGet(t *testing.T) { + c := subscriptions.NewDefaultClient() + + c.Set("demo", 1) + + result, _ := c.Get("demo").(int) + + if result != 1 { + t.Errorf("Expected 1, got %v", result) + } +} diff --git a/tools/types/datetime.go b/tools/types/datetime.go new file mode 100644 index 00000000..cbeca1e9 --- /dev/null +++ b/tools/types/datetime.go @@ -0,0 +1,93 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "github.com/spf13/cast" +) + +// DefaultDateLayout specifies the default app date strings layout. +const DefaultDateLayout = "2006-01-02 15:04:05.000" + +// NowDateTime returns new DateTime instance with the current local time. +func NowDateTime() DateTime { + return DateTime{t: time.Now()} +} + +// ParseDateTime creates a new DateTime from the provided value +// (could be [cast.ToTime] supported string, [time.Time], etc.). +func ParseDateTime(value any) (DateTime, error) { + d := DateTime{} + err := d.Scan(value) + return d, err +} + +// DateTime represents a [time.Time] instance in UTC that is wrapped +// and serialized using the app default date layout. +type DateTime struct { + t time.Time +} + +// Time returns the internal [time.Time] instance. +func (d DateTime) Time() time.Time { + return d.t +} + +// IsZero checks whether the current DateTime instance has zero time value. +func (d DateTime) IsZero() bool { + return d.Time().IsZero() +} + +// String serializes the current DateTime instance into a formated +// UTC date string. +// +// The zero value is serialized to an empty string. +func (d DateTime) String() string { + if d.IsZero() { + return "" + } + return d.Time().UTC().Format(DefaultDateLayout) +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (d DateTime) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (d *DateTime) UnmarshalJSON(b []byte) error { + var raw string + if err := json.Unmarshal(b, &raw); err != nil { + return err + } + return d.Scan(raw) +} + +// Value implements the [driver.Valuer] interface. +func (d DateTime) Value() (driver.Value, error) { + return d.String(), nil +} + +// Scan implements [sql.Scanner] interface to scan the provided value +// into the current DateTime instance. +func (d *DateTime) Scan(value any) error { + switch v := value.(type) { + case DateTime: + d.t = v.Time() + case time.Time: + d.t = v + case int: + d.t = cast.ToTime(v) + default: + str := cast.ToString(v) + if str == "" { + d.t = time.Time{} + } else { + d.t = cast.ToTime(str) + } + } + + return nil +} diff --git a/tools/types/datetime_test.go b/tools/types/datetime_test.go new file mode 100644 index 00000000..acddbd35 --- /dev/null +++ b/tools/types/datetime_test.go @@ -0,0 +1,198 @@ +package types_test + +import ( + "github.com/pocketbase/pocketbase/tools/types" + "strings" + "testing" + "time" +) + +func TestNowDateTime(t *testing.T) { + now := time.Now().UTC().Format("2006-01-02 15:04:05") // without ms part for test consistency + dt := types.NowDateTime() + + if !strings.Contains(dt.String(), now) { + t.Fatalf("Expected %q, got %q", now, dt.String()) + } +} + +func TestParseDateTime(t *testing.T) { + nowTime := time.Now().UTC() + nowDateTime, _ := types.ParseDateTime(nowTime) + nowStr := nowTime.Format(types.DefaultDateLayout) + + scenarios := []struct { + value any + expected string + }{ + {nil, ""}, + {"", ""}, + {"invalid", ""}, + {nowDateTime, nowStr}, + {nowTime, nowStr}, + {1641024040, "2022-01-01 08:00:40.000"}, + {"2022-01-01 11:23:45.678", "2022-01-01 11:23:45.678"}, + } + + for i, s := range scenarios { + dt, err := types.ParseDateTime(s.value) + if err != nil { + t.Errorf("(%d) Failed to parse %v: %v", i, s.value, err) + continue + } + + if dt.String() != s.expected { + t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String()) + } + } +} + +func TestDateTimeTime(t *testing.T) { + str := "2022-01-01 11:23:45.678" + + expected, err := time.Parse(types.DefaultDateLayout, str) + if err != nil { + t.Fatal(err) + } + + dt, err := types.ParseDateTime(str) + if err != nil { + t.Fatal(err) + } + + result := dt.Time() + + if !expected.Equal(result) { + t.Errorf("Expected time %v, got %v", expected, result) + } +} + +func TestDateTimeIsZero(t *testing.T) { + dt0 := types.DateTime{} + if !dt0.IsZero() { + t.Fatalf("Expected zero datatime, got %v", dt0) + } + + dt1 := types.NowDateTime() + if dt1.IsZero() { + t.Fatalf("Expected non-zero datatime, got %v", dt1) + } +} + +func TestDateTimeString(t *testing.T) { + dt0 := types.DateTime{} + if dt0.String() != "" { + t.Fatalf("Expected empty string for zer datetime, got %q", dt0.String()) + } + + expected := "2022-01-01 11:23:45.678" + dt1, _ := types.ParseDateTime(expected) + if dt1.String() != expected { + t.Fatalf("Expected %q, got %v", expected, dt1) + } +} + +func TestDateTimeMarshalJSON(t *testing.T) { + scenarios := []struct { + date string + expected string + }{ + {"", `""`}, + {"2022-01-01 11:23:45.678", `"2022-01-01 11:23:45.678"`}, + } + + for i, s := range scenarios { + dt, err := types.ParseDateTime(s.date) + if err != nil { + t.Errorf("(%d) %v", i, err) + } + + result, err := dt.MarshalJSON() + if err != nil { + t.Errorf("(%d) %v", i, err) + } + + if string(result) != s.expected { + t.Errorf("(%d) Expected %q, got %q", i, s.expected, string(result)) + } + } +} + +func TestDateTimeUnmarshalJSON(t *testing.T) { + scenarios := []struct { + date string + expected string + }{ + {"", ""}, + {"invalid_json", ""}, + {"'123'", ""}, + {"2022-01-01 11:23:45.678", ""}, + {`"2022-01-01 11:23:45.678"`, "2022-01-01 11:23:45.678"}, + } + + for i, s := range scenarios { + dt := types.DateTime{} + dt.UnmarshalJSON([]byte(s.date)) + + if dt.String() != s.expected { + t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String()) + } + } +} + +func TestDateTimeValue(t *testing.T) { + scenarios := []struct { + value any + expected string + }{ + {"", ""}, + {"invalid", ""}, + {1641024040, "2022-01-01 08:00:40.000"}, + {"2022-01-01 11:23:45.678", "2022-01-01 11:23:45.678"}, + {types.NowDateTime(), types.NowDateTime().String()}, + } + + for i, s := range scenarios { + dt, _ := types.ParseDateTime(s.value) + result, err := dt.Value() + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + + if result != s.expected { + t.Errorf("(%d) Expected %q, got %q", i, s.expected, result) + } + } +} + +func TestDateTimeScan(t *testing.T) { + now := time.Now().UTC().Format("2006-01-02 15:04:05") // without ms part for test consistency + + scenarios := []struct { + value any + expected string + }{ + {nil, ""}, + {"", ""}, + {"invalid", ""}, + {types.NowDateTime(), now}, + {time.Now(), now}, + {1641024040, "2022-01-01 08:00:40.000"}, + {"2022-01-01 11:23:45.678", "2022-01-01 11:23:45.678"}, + } + + for i, s := range scenarios { + dt := types.DateTime{} + + err := dt.Scan(s.value) + if err != nil { + t.Errorf("(%d) Failed to parse %v: %v", i, s.value, err) + continue + } + + if !strings.Contains(dt.String(), s.expected) { + t.Errorf("(%d) Expected %q, got %q", i, s.expected, dt.String()) + } + } +} diff --git a/tools/types/json_array.go b/tools/types/json_array.go new file mode 100644 index 00000000..5b24c067 --- /dev/null +++ b/tools/types/json_array.go @@ -0,0 +1,55 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// JsonArray defines a slice that is safe for json and db read/write. +type JsonArray []any + +// MarshalJSON implements the [json.Marshaler] interface. +func (m JsonArray) MarshalJSON() ([]byte, error) { + type alias JsonArray // prevent recursion + + // inialize an empty map to ensure that `[]` is returned as json + if m == nil { + m = JsonArray{} + } + + return json.Marshal(alias(m)) +} + +// Value implements the [driver.Valuer] interface. +func (m JsonArray) Value() (driver.Value, error) { + if m == nil { + return nil, nil + } + + data, err := json.Marshal(m) + + return string(data), err +} + +// Scan implements [sql.Scanner] interface to scan the provided value +// into the current `JsonArray` instance. +func (m *JsonArray) Scan(value any) error { + var data []byte + switch v := value.(type) { + case nil: + // no cast needed + case []byte: + data = v + case string: + data = []byte(v) + default: + return fmt.Errorf("Failed to unmarshal JsonArray value: %q.", value) + } + + if len(data) == 0 { + data = []byte("[]") + } + + return json.Unmarshal(data, m) +} diff --git a/tools/types/json_array_test.go b/tools/types/json_array_test.go new file mode 100644 index 00000000..cdf19c6f --- /dev/null +++ b/tools/types/json_array_test.go @@ -0,0 +1,95 @@ +package types_test + +import ( + "database/sql/driver" + "testing" + + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestJsonArrayMarshalJSON(t *testing.T) { + scenarios := []struct { + json types.JsonArray + expected string + }{ + {nil, "[]"}, + {types.JsonArray{}, `[]`}, + {types.JsonArray{1, 2, 3}, `[1,2,3]`}, + {types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`}, + {types.JsonArray{1, "test"}, `[1,"test"]`}, + } + + for i, s := range scenarios { + result, err := s.json.MarshalJSON() + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + if string(result) != s.expected { + t.Errorf("(%d) Expected %s, got %s", i, s.expected, string(result)) + } + } +} + +func TestJsonArrayValue(t *testing.T) { + scenarios := []struct { + json types.JsonArray + expected driver.Value + }{ + {nil, nil}, + {types.JsonArray{}, `[]`}, + {types.JsonArray{1, 2, 3}, `[1,2,3]`}, + {types.JsonArray{"test1", "test2", "test3"}, `["test1","test2","test3"]`}, + {types.JsonArray{1, "test"}, `[1,"test"]`}, + } + + for i, s := range scenarios { + result, err := s.json.Value() + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + if result != s.expected { + t.Errorf("(%d) Expected %s, got %v", i, s.expected, result) + } + } +} + +func TestJsonArrayScan(t *testing.T) { + scenarios := []struct { + value any + expectError bool + expectJson string + }{ + {``, false, `[]`}, + {[]byte{}, false, `[]`}, + {nil, false, `[]`}, + {123, true, `[]`}, + {`""`, true, `[]`}, + {`invalid_json`, true, `[]`}, + {`"test"`, true, `[]`}, + {`1,2,3`, true, `[]`}, + {`[1, 2, 3`, true, `[]`}, + {`[1, 2, 3]`, false, `[1,2,3]`}, + {[]byte(`[1, 2, 3]`), false, `[1,2,3]`}, + {`[1, "test"]`, false, `[1,"test"]`}, + {`[]`, false, `[]`}, + } + + for i, s := range scenarios { + arr := types.JsonArray{} + scanErr := arr.Scan(s.value) + + hasErr := scanErr != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr) + continue + } + + result, _ := arr.MarshalJSON() + + if string(result) != s.expectJson { + t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) + } + } +} diff --git a/tools/types/json_map.go b/tools/types/json_map.go new file mode 100644 index 00000000..f9c49db2 --- /dev/null +++ b/tools/types/json_map.go @@ -0,0 +1,55 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +// JsonMap defines a map that is safe for json and db read/write. +type JsonMap map[string]any + +// MarshalJSON implements the [json.Marshaler] interface. +func (m JsonMap) MarshalJSON() ([]byte, error) { + type alias JsonMap // prevent recursion + + // inialize an empty map to ensure that `{}` is returned as json + if m == nil { + m = JsonMap{} + } + + return json.Marshal(alias(m)) +} + +// Value implements the [driver.Valuer] interface. +func (m JsonMap) Value() (driver.Value, error) { + if m == nil { + return nil, nil + } + + data, err := json.Marshal(m) + + return string(data), err +} + +// Scan implements [sql.Scanner] interface to scan the provided value +// into the current `JsonMap` instance. +func (m *JsonMap) Scan(value any) error { + var data []byte + switch v := value.(type) { + case nil: + // no cast needed + case []byte: + data = v + case string: + data = []byte(v) + default: + return fmt.Errorf("Failed to unmarshal JsonMap value: %q.", value) + } + + if len(data) == 0 { + data = []byte("{}") + } + + return json.Unmarshal(data, m) +} diff --git a/tools/types/json_map_test.go b/tools/types/json_map_test.go new file mode 100644 index 00000000..59272cc7 --- /dev/null +++ b/tools/types/json_map_test.go @@ -0,0 +1,92 @@ +package types_test + +import ( + "database/sql/driver" + "testing" + + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestJsonMapMarshalJSON(t *testing.T) { + scenarios := []struct { + json types.JsonMap + expected string + }{ + {nil, "{}"}, + {types.JsonMap{}, `{}`}, + {types.JsonMap{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`}, + {types.JsonMap{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`}, + } + + for i, s := range scenarios { + result, err := s.json.MarshalJSON() + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + if string(result) != s.expected { + t.Errorf("(%d) Expected %s, got %s", i, s.expected, string(result)) + } + } +} + +func TestJsonMapValue(t *testing.T) { + scenarios := []struct { + json types.JsonMap + expected driver.Value + }{ + {nil, nil}, + {types.JsonMap{}, `{}`}, + {types.JsonMap{"test1": 123, "test2": "lorem"}, `{"test1":123,"test2":"lorem"}`}, + {types.JsonMap{"test": []int{1, 2, 3}}, `{"test":[1,2,3]}`}, + } + + for i, s := range scenarios { + result, err := s.json.Value() + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + if result != s.expected { + t.Errorf("(%d) Expected %s, got %v", i, s.expected, result) + } + } +} + +func TestJsonArrayMapScan(t *testing.T) { + scenarios := []struct { + value any + expectError bool + expectJson string + }{ + {``, false, `{}`}, + {nil, false, `{}`}, + {[]byte{}, false, `{}`}, + {`{}`, false, `{}`}, + {123, true, `{}`}, + {`""`, true, `{}`}, + {`invalid_json`, true, `{}`}, + {`"test"`, true, `{}`}, + {`1,2,3`, true, `{}`}, + {`{"test": 1`, true, `{}`}, + {`{"test": 1}`, false, `{"test":1}`}, + {[]byte(`{"test": 1}`), false, `{"test":1}`}, + } + + for i, s := range scenarios { + arr := types.JsonMap{} + scanErr := arr.Scan(s.value) + + hasErr := scanErr != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr) + continue + } + + result, _ := arr.MarshalJSON() + + if string(result) != s.expectJson { + t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) + } + } +} diff --git a/tools/types/json_raw.go b/tools/types/json_raw.go new file mode 100644 index 00000000..7f5defe5 --- /dev/null +++ b/tools/types/json_raw.go @@ -0,0 +1,83 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "errors" +) + +// JsonRaw defines a json value type that is safe for db read/write. +type JsonRaw []byte + +// ParseJsonRaw creates a new JsonRaw instance from the provided value +// (could be JsonRaw, int, float, string, []byte, etc.). +func ParseJsonRaw(value any) (JsonRaw, error) { + result := JsonRaw{} + err := result.Scan(value) + return result, err +} + +// String returns the current JsonRaw instance as a json encoded string. +func (j JsonRaw) String() string { + return string(j) +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (j JsonRaw) MarshalJSON() ([]byte, error) { + if len(j) == 0 { + return []byte("null"), nil + } + + return j, nil +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (j *JsonRaw) UnmarshalJSON(b []byte) error { + if j == nil { + return errors.New("JsonRaw: UnmarshalJSON on nil pointer") + } + + *j = append((*j)[0:0], b...) + + return nil +} + +// Value implements the [driver.Valuer] interface. +func (j JsonRaw) Value() (driver.Value, error) { + if len(j) == 0 { + return nil, nil + } + + return j.String(), nil +} + +// Scan implements [sql.Scanner] interface to scan the provided value +// into the current JsonRaw instance. +func (j *JsonRaw) Scan(value interface{}) error { + var data []byte + + switch v := value.(type) { + case nil: + // no cast is needed + case []byte: + if len(v) != 0 { + data = v + } + case string: + if v != "" { + data = []byte(v) + } + case JsonRaw: + if len(v) != 0 { + data = []byte(v) + } + default: + bytes, err := json.Marshal(v) + if err != nil { + return err + } + data = bytes + } + + return j.UnmarshalJSON(data) +} diff --git a/tools/types/json_raw_test.go b/tools/types/json_raw_test.go new file mode 100644 index 00000000..6683b3ff --- /dev/null +++ b/tools/types/json_raw_test.go @@ -0,0 +1,178 @@ +package types_test + +import ( + "database/sql/driver" + "testing" + + "github.com/pocketbase/pocketbase/tools/types" +) + +func TestParseJsonRaw(t *testing.T) { + scenarios := []struct { + value any + expectError bool + expectJson string + }{ + {nil, false, `null`}, + {``, false, `null`}, + {[]byte{}, false, `null`}, + {types.JsonRaw{}, false, `null`}, + {`{}`, false, `{}`}, + {`[]`, false, `[]`}, + {123, false, `123`}, + {`""`, false, `""`}, + {`test`, false, `test`}, + {`{"invalid"`, false, `{"invalid"`}, // treated as a byte casted string + {`{"test":1}`, false, `{"test":1}`}, + {[]byte(`[1,2,3]`), false, `[1,2,3]`}, + {[]int{1, 2, 3}, false, `[1,2,3]`}, + {map[string]int{"test": 1}, false, `{"test":1}`}, + } + + for i, s := range scenarios { + raw, parseErr := types.ParseJsonRaw(s.value) + hasErr := parseErr != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, parseErr) + continue + } + + result, _ := raw.MarshalJSON() + + if string(result) != s.expectJson { + t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) + } + } +} + +func TestJsonRawString(t *testing.T) { + scenarios := []struct { + json types.JsonRaw + expected string + }{ + {nil, ``}, + {types.JsonRaw{}, ``}, + {types.JsonRaw([]byte(`123`)), `123`}, + {types.JsonRaw(`{"demo":123}`), `{"demo":123}`}, + } + + for i, s := range scenarios { + result := s.json.String() + if result != s.expected { + t.Errorf("(%d) Expected %q, got %q", i, s.expected, result) + } + } +} + +func TestJsonRawMarshalJSON(t *testing.T) { + scenarios := []struct { + json types.JsonRaw + expected string + }{ + {nil, `null`}, + {types.JsonRaw{}, `null`}, + {types.JsonRaw([]byte(`123`)), `123`}, + {types.JsonRaw(`{"demo":123}`), `{"demo":123}`}, + } + + for i, s := range scenarios { + result, err := s.json.MarshalJSON() + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + + if string(result) != s.expected { + t.Errorf("(%d) Expected %q, got %q", i, s.expected, string(result)) + } + } +} + +func TestJsonRawUnmarshalJSON(t *testing.T) { + scenarios := []struct { + json []byte + expectString string + }{ + {nil, ""}, + {[]byte{0, 1, 2}, "\x00\x01\x02"}, + {[]byte("123"), "123"}, + {[]byte("test"), "test"}, + {[]byte(`{"test":123}`), `{"test":123}`}, + } + + for i, s := range scenarios { + raw := types.JsonRaw{} + err := raw.UnmarshalJSON(s.json) + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + + if raw.String() != s.expectString { + t.Errorf("(%d) Expected %q, got %q", i, s.expectString, raw.String()) + } + } +} + +func TestJsonRawValue(t *testing.T) { + scenarios := []struct { + json types.JsonRaw + expected driver.Value + }{ + {nil, nil}, + {types.JsonRaw{}, nil}, + {types.JsonRaw(``), nil}, + {types.JsonRaw(`test`), `test`}, + } + + for i, s := range scenarios { + result, err := s.json.Value() + if err != nil { + t.Errorf("(%d) %v", i, err) + continue + } + if result != s.expected { + t.Errorf("(%d) Expected %s, got %v", i, s.expected, result) + } + } +} + +func TestJsonRawScan(t *testing.T) { + scenarios := []struct { + value any + expectError bool + expectJson string + }{ + {nil, false, `null`}, + {``, false, `null`}, + {[]byte{}, false, `null`}, + {types.JsonRaw{}, false, `null`}, + {types.JsonRaw(`test`), false, `test`}, + {`{}`, false, `{}`}, + {`[]`, false, `[]`}, + {123, false, `123`}, + {`""`, false, `""`}, + {`test`, false, `test`}, + {`{"invalid"`, false, `{"invalid"`}, // treated as a byte casted string + {`{"test":1}`, false, `{"test":1}`}, + {[]byte(`[1,2,3]`), false, `[1,2,3]`}, + {[]int{1, 2, 3}, false, `[1,2,3]`}, + {map[string]int{"test": 1}, false, `{"test":1}`}, + } + + for i, s := range scenarios { + raw := types.JsonRaw{} + scanErr := raw.Scan(s.value) + hasErr := scanErr != nil + if hasErr != s.expectError { + t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, scanErr) + continue + } + + result, _ := raw.MarshalJSON() + + if string(result) != s.expectJson { + t.Errorf("(%d) Expected %s, got %v", i, s.expectJson, string(result)) + } + } +} diff --git a/ui/.env b/ui/.env new file mode 100644 index 00000000..35eb2889 --- /dev/null +++ b/ui/.env @@ -0,0 +1,4 @@ +# all environments should start with 'PB_' prefix +PB_BACKEND_URL = / +PB_PROFILE_COLLECTION = profiles +PB_RULES_SYNTAX_DOCS = https://pocketbase.io/docs/manage-collections#rules-filters-syntax diff --git a/ui/.env.development b/ui/.env.development new file mode 100644 index 00000000..657341af --- /dev/null +++ b/ui/.env.development @@ -0,0 +1 @@ +PB_BACKEND_URL = http://localhost:8090 diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 00000000..7c9b8944 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/.vscode/ +.DS_Store diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 00000000..a0e650ef --- /dev/null +++ b/ui/README.md @@ -0,0 +1,24 @@ +PocketBase Admin dashboard UI +====================================================================== + +This is the PocketBase Admin dashboard UI (built with Svelte and Vite). + +Although it could be used independently, it is mainly intended to be embedded +as part of a PocketBase app executable (hence the `embed.go` file). + +The used admins avatars are from https://boringavatars.com/. + +## Development + +Download the repo and run the appropriate console commands: + +```sh +# install dependencies +npm install + +# start a dev server with hot reload at localhost:3000 +npm run dev + +# or generate production ready bundle in dist/ directory +npm run build +``` diff --git a/ui/dist/assets/Elements.c2e07307.js b/ui/dist/assets/Elements.c2e07307.js new file mode 100644 index 00000000..dc2457c7 --- /dev/null +++ b/ui/dist/assets/Elements.c2e07307.js @@ -0,0 +1,68 @@ +import{S as ln,i as tn,s as sn,O as We,a as en,b as nn,c as fn,e,d as n,f as Xt,g as f,h as i,j as u,m as Dt,k as bn,t as un,l as on,n as zt,o as Ft,p as s,q as Gt,r as pn,u as Ye}from"./index.944ee0db.js";function mn(v){let b;return{c(){b=e("p"),b.textContent="Lorem ipsum dolor sit amet..."},m(o,p){i(o,b,p)},p:Ye,d(o){o&&s(b)}}}function vn(v){let b;return{c(){b=e("h4"),b.textContent="My title",f(b,"slot","header")},m(o,p){i(o,b,p)},p:Ye,d(o){o&&s(b)}}}function dn(v){let b,o,p;return{c(){b=e("button"),b.textContent="Cancel",o=n(),p=e("button"),p.textContent="Save",f(b,"class","btn btn-secondary"),f(p,"class","btn btn-expanded")},m(m,d){i(m,b,d),i(m,o,d),i(m,p,d)},p:Ye,d(m){m&&s(b),m&&s(o),m&&s(p)}}}function xn(v){let b,o,p,m,d,Jt,Kt,_,_t,Ze,y,Qt,Ut,Vt,L,Lt,re,B,Wt,Yt,Zt,r,rt,c,ct,a,at,h,ht,g,gt,li,ti,Ht,ii,Ct,si,Mt,ei,Tt,ni,yt,fi,Bt,bi,wt,ui,kt,oi,$t,pi,Ot,mi,It,vi,Et,di,xi,_i,ll,Li,Nt,Hi,Pt,Ci,jt,Mi,At,Ti,yi,Bi,tl,wi,ki,$i,il,Oi,sl,Ii,el,Ei,nl,Ni,fl,Pi,bl,ji,Ai,Si,ul,qi,ol,Ri,pl,Xi,ml,Di,vl,zi,dl,Fi,Gi,Ji,xl,Ki,_l,Qi,Ll,Ui,Hl,Vi,Cl,Wi,Ml,Yi,Zi,ri,w,ci,k,ai,$,hi,O,gi,I,ls,E,ts,N,is,P,ss,j,es,A,ns,S,fs,q,bs,us,os,Tl,ps,yl,ms,Bl,vs,wl,ds,kl,xs,_s,Ls,$l,Hs,Ol,Cs,Il,Ms,El,Ts,Nl,ys,Bs,ws,Pl,ks,jl,$s,Al,Os,Sl,Is,ql,Es,Ns,Ps,Rl,js,Xl,As,Dl,Ss,zl,qs,Fl,Rs,Gl,Xs,Ds,zs,Jl,Fs,Kl,Gs,Ql,Js,Ul,Ks,Vl,Qs,Wl,Us,Vs,Ws,R,Ys,X,Zs,D,rs,z,cs,F,as,G,hs,gs,le,St,te,ie,se,J,H,K,Q,ee,ne,fe,Yl,be,ue,oe,Zl,pe,rl,me,C,qt,ce,U,M,V,W,ve,de,xe,cl,_e,al,Le,hl,gl,T,Y,Z,He,Ce,Me,lt,Te,tt,ye,it,Be,st,we,ke,$e,et,Oe,nt,Ie,ft,Ee,bt,Ne,Pe,je,ut,Ae,ot,Se,pt,qe,mt,Re,Xe,De,vt,ze,dt,Fe,Ge,Je,xt,Ke,x,Qe,Rt,Ue,ae;m=new We({props:{multiple:!0,searchable:!0,items:["test1","test2"]}}),y=new We({props:{searchable:!0,items:["test1","test2"]}}),B=new We({props:{disabled:!0,searchable:!0,items:["test1","test2"]}});function ge(l){v[1](l)}let he={popup:!1,$$slots:{footer:[dn],header:[vn],default:[mn]},$$scope:{ctx:v}};return v[0]!==void 0&&(he.active=v[0]),x=new en({props:he}),nn.push(()=>fn(x,"active",ge)),{c(){b=e("div"),o=e("label"),o.textContent="EXAMPLE",p=n(),Xt(m.$$.fragment),d=n(),Jt=e("hr"),Kt=n(),_=e("div"),_t=e("label"),_t.textContent="EXAMPLE",Ze=n(),Xt(y.$$.fragment),Qt=n(),Ut=e("hr"),Vt=n(),L=e("div"),Lt=e("label"),Lt.textContent="EXAMPLE",re=n(),Xt(B.$$.fragment),Wt=n(),Yt=e("hr"),Zt=n(),r=e("div"),r.innerHTML=`
+
Hello world!
+
`,rt=n(),c=e("div"),c.innerHTML=`
+
Hello world!
+
`,ct=n(),a=e("div"),a.innerHTML=`
+
Hello world!
+
`,at=n(),h=e("div"),h.innerHTML=`
+
Hello world!
+
`,ht=n(),g=e("div"),g.innerHTML=`
+
Hello world!
+
`,gt=n(),li=e("hr"),ti=n(),Ht=e("h1"),Ht.textContent="H1 title",ii=n(),Ct=e("p"),Ct.textContent="Lorem Ipsum dolor sit amet...",si=n(),Mt=e("h2"),Mt.textContent="H2 title",ei=n(),Tt=e("p"),Tt.textContent="Lorem Ipsum dolor sit amet...",ni=n(),yt=e("h3"),yt.textContent="H3 title",fi=n(),Bt=e("p"),Bt.textContent="Lorem Ipsum dolor sit amet...",bi=n(),wt=e("h4"),wt.textContent="H4 title",ui=n(),kt=e("p"),kt.textContent="Lorem Ipsum dolor sit amet...",oi=n(),$t=e("h5"),$t.textContent="H5 title",pi=n(),Ot=e("p"),Ot.textContent="Lorem Ipsum dolor sit amet...",mi=n(),It=e("h6"),It.textContent="H6 title",vi=n(),Et=e("p"),Et.textContent="Lorem Ipsum dolor sit amet...",di=n(),xi=e("hr"),_i=n(),ll=e("div"),ll.innerHTML=`
COL1
+
COL2
`,Li=n(),Nt=e("p"),Nt.innerHTML=`Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has + been the industry's + standard dummy text ever since the 1500s, when an unknown printer took a galley of type + and scrambled it to make a type specimen book. It has survived not only five centuries, but also + the leap into electronic typesetting, remaining1 essentially2 unchanged.`,Hi=n(),Pt=e("p"),Pt.textContent=`It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and + more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum`,Ci=n(),jt=e("ul"),jt.innerHTML=`
  • Option 1
  • +
  • Option 2
  • +
  • Option 3
  • `,Mi=n(),At=e("ol"),At.innerHTML=`
  • Option 1
  • +
  • Option 2
  • +
  • Option 3
  • `,Ti=n(),yi=e("hr"),Bi=n(),tl=e("span"),tl.textContent="Lorem Ipsum",wi=n(),ki=e("hr"),$i=n(),il=e("button"),il.textContent="Button default",Oi=n(),sl=e("button"),sl.textContent="Button danger",Ii=n(),el=e("button"),el.textContent="Button warning",Ei=n(),nl=e("button"),nl.textContent="Button success",Ni=n(),fl=e("button"),fl.textContent="Button info",Pi=n(),bl=e("button"),bl.textContent="Button hint",ji=n(),Ai=e("hr"),Si=n(),ul=e("button"),ul.textContent="Button default",qi=n(),ol=e("button"),ol.textContent="Button danger",Ri=n(),pl=e("button"),pl.textContent="Button danger",Xi=n(),ml=e("button"),ml.textContent="Button success",Di=n(),vl=e("button"),vl.textContent="Button info",zi=n(),dl=e("button"),dl.textContent="Button hint",Fi=n(),Gi=e("hr"),Ji=n(),xl=e("button"),xl.textContent="Button default",Ki=n(),_l=e("button"),_l.textContent="Button danger",Qi=n(),Ll=e("button"),Ll.textContent="Button danger",Ui=n(),Hl=e("button"),Hl.textContent="Button success",Vi=n(),Cl=e("button"),Cl.textContent="Button info",Wi=n(),Ml=e("button"),Ml.textContent="Button hint",Yi=n(),Zi=e("hr"),ri=n(),w=e("button"),w.textContent="Button default",ci=n(),k=e("button"),k.textContent="Button danger",ai=n(),$=e("button"),$.textContent="Button warning",hi=n(),O=e("button"),O.textContent="Button success",gi=n(),I=e("button"),I.textContent="Button info",ls=n(),E=e("button"),E.textContent="Button hint",ts=n(),N=e("button"),N.textContent="Button default",is=n(),P=e("button"),P.textContent="Button danger",ss=n(),j=e("button"),j.textContent="Button danger",es=n(),A=e("button"),A.textContent="Button success",ns=n(),S=e("button"),S.textContent="Button info",fs=n(),q=e("button"),q.textContent="Button hint",bs=n(),us=e("hr"),os=n(),Tl=e("button"),Tl.innerHTML=` + Button default`,ps=n(),yl=e("button"),yl.innerHTML=` + Button danger`,ms=n(),Bl=e("button"),Bl.innerHTML=` + Button warning`,vs=n(),wl=e("button"),wl.innerHTML=` + Button success`,ds=n(),kl=e("button"),kl.innerHTML=` + Button hint`,xs=n(),_s=e("hr"),Ls=n(),$l=e("button"),$l.innerHTML=` + Button default`,Hs=n(),Ol=e("button"),Ol.innerHTML=` + Button danger`,Cs=n(),Il=e("button"),Il.innerHTML=` + Button warning`,Ms=n(),El=e("button"),El.innerHTML=` + Button success`,Ts=n(),Nl=e("button"),Nl.innerHTML=` + Button hint`,ys=n(),Bs=e("hr"),ws=n(),Pl=e("button"),Pl.innerHTML=` + Button default`,ks=n(),jl=e("button"),jl.innerHTML=` + Button danger`,$s=n(),Al=e("button"),Al.innerHTML=` + Button warning`,Os=n(),Sl=e("button"),Sl.innerHTML=` + Button success`,Is=n(),ql=e("button"),ql.innerHTML=` + Button hint`,Es=n(),Ns=e("hr"),Ps=n(),Rl=e("button"),Rl.innerHTML='',js=n(),Xl=e("button"),Xl.innerHTML='',As=n(),Dl=e("button"),Dl.innerHTML='',Ss=n(),zl=e("button"),zl.innerHTML='',qs=n(),Fl=e("button"),Fl.innerHTML='',Rs=n(),Gl=e("button"),Gl.innerHTML='',Xs=n(),Ds=e("hr"),zs=n(),Jl=e("button"),Jl.innerHTML=` + Button Loading`,Fs=n(),Kl=e("button"),Kl.innerHTML=` + Button Loading`,Gs=n(),Ql=e("button"),Ql.innerHTML=` + Button Loading`,Js=n(),Ul=e("button"),Ul.innerHTML='',Ks=n(),Vl=e("button"),Vl.innerHTML='',Qs=n(),Wl=e("button"),Wl.innerHTML='',Us=n(),Vs=e("hr"),Ws=n(),R=e("button"),R.innerHTML=` + Button Loading`,Ys=n(),X=e("button"),X.innerHTML=` + Button Loading`,Zs=n(),D=e("button"),D.innerHTML=` + Button Loading`,rs=n(),z=e("button"),z.innerHTML='',cs=n(),F=e("button"),F.innerHTML='',as=n(),G=e("button"),G.innerHTML='',hs=n(),gs=e("hr"),le=n(),St=e("input"),te=n(),ie=e("hr"),se=n(),J=e("select"),H=e("option"),H.textContent="Option 1",K=e("option"),K.textContent="Option 2",Q=e("option"),Q.textContent="Option 3",ee=n(),ne=e("hr"),fe=n(),Yl=e("textarea"),be=n(),ue=e("hr"),oe=n(),Zl=e("div"),Zl.innerHTML=` + `,pe=n(),rl=e("div"),rl.innerHTML=` + `,me=n(),C=e("div"),qt=e("label"),qt.textContent="Choose value",ce=n(),U=e("select"),M=e("option"),M.textContent="Option 1",V=e("option"),V.textContent="Option 2",W=e("option"),W.textContent="Option 3",ve=n(),de=e("hr"),xe=n(),cl=e("div"),cl.innerHTML='',_e=n(),al=e("div"),al.innerHTML="",Le=n(),hl=e("div"),gl=e("select"),T=e("option"),T.textContent="Option 1",Y=e("option"),Y.textContent="Option 2",Z=e("option"),Z.textContent="Option 3",He=n(),Ce=e("hr"),Me=n(),lt=e("div"),lt.innerHTML=` + `,Te=n(),tt=e("div"),tt.innerHTML=` + `,ye=n(),it=e("div"),it.innerHTML=` + `,Be=n(),st=e("div"),st.innerHTML=` + `,we=n(),ke=e("hr"),$e=n(),et=e("div"),et.innerHTML=` + +

    Something went wrong

    `,Oe=n(),nt=e("div"),nt.innerHTML=` + +

    Lorem ipsum dolor sit amet

    `,Ie=n(),ft=e("div"),ft.innerHTML=` + +
    Lorem ipsum
    `,Ee=n(),bt=e("div"),bt.innerHTML=` + +
    Lorem ipsum
    `,Ne=n(),Pe=e("hr"),je=n(),ut=e("div"),ut.innerHTML=` + `,Ae=n(),ot=e("div"),ot.innerHTML=` + `,Se=n(),pt=e("div"),pt.innerHTML=` + `,qe=n(),mt=e("div"),mt.innerHTML=` + `,Re=n(),Xe=e("hr"),De=n(),vt=e("div"),vt.innerHTML=` +
    + `,ze=n(),dt=e("div"),dt.innerHTML=`
    + `,Fe=n(),Ge=e("hr"),Je=n(),xt=e("div"),xt.innerHTML=`
    +
    +
    +
    +
    +
    `,Ke=n(),Xt(x.$$.fragment),f(o,"for",""),f(b,"class","form-field"),f(_t,"for",""),f(_,"class","form-field"),f(Lt,"for",""),f(L,"class","form-field disabled"),f(r,"class","alert"),f(c,"class","alert alert-info"),f(a,"class","alert alert-danger"),f(h,"class","alert alert-warning"),f(g,"class","alert alert-success"),f(ll,"class","grid"),f(il,"class","btn"),f(sl,"class","btn btn-danger"),f(el,"class","btn btn-warning"),f(nl,"class","btn btn-success"),f(fl,"class","btn btn-info"),f(bl,"class","btn btn-hint"),f(ul,"class","btn btn-secondary"),f(ol,"class","btn btn-secondary btn-danger"),f(pl,"class","btn btn-secondary btn-warning"),f(ml,"class","btn btn-secondary btn-success"),f(vl,"class","btn btn-secondary btn-info"),f(dl,"class","btn btn-secondary btn-hint"),f(xl,"class","btn btn-outline"),f(_l,"class","btn btn-outline btn-danger"),f(Ll,"class","btn btn-outline btn-warning"),f(Hl,"class","btn btn-outline btn-success"),f(Cl,"class","btn btn-outline btn-info"),f(Ml,"class","btn btn-outline btn-hint"),w.disabled=!0,f(w,"class","btn"),k.disabled=!0,f(k,"class","btn btn-danger"),$.disabled=!0,f($,"class","btn btn-warning"),O.disabled=!0,f(O,"class","btn btn-success"),I.disabled=!0,f(I,"class","btn btn-info"),E.disabled=!0,f(E,"class","btn btn-hint"),N.disabled=!0,f(N,"class","btn btn-secondary"),P.disabled=!0,f(P,"class","btn btn-secondary btn-danger"),j.disabled=!0,f(j,"class","btn btn-secondary btn-warning"),A.disabled=!0,f(A,"class","btn btn-secondary btn-success"),S.disabled=!0,f(S,"class","btn btn-secondary btn-info"),q.disabled=!0,f(q,"class","btn btn-secondary btn-hint"),f(Tl,"class","btn"),f(yl,"class","btn btn-danger"),f(Bl,"class","btn btn-warning"),f(wl,"class","btn btn-success"),f(kl,"class","btn btn-hint"),f($l,"class","btn btn-sm"),f(Ol,"class","btn btn-danger btn-sm"),f(Il,"class","btn btn-warning btn-sm"),f(El,"class","btn btn-success btn-sm"),f(Nl,"class","btn btn-hint btn-sm"),f(Pl,"class","btn btn-lg"),f(jl,"class","btn btn-danger btn-lg"),f(Al,"class","btn btn-warning btn-lg"),f(Sl,"class","btn btn-success btn-lg"),f(ql,"class","btn btn-hint btn-lg"),f(Rl,"class","btn btn-circle"),f(Xl,"class","btn btn-sm btn-circle"),f(Dl,"class","btn btn-lg btn-circle"),f(zl,"class","btn btn-secondary btn-circle"),f(Fl,"class","btn btn-secondary btn-sm btn-circle"),f(Gl,"class","btn btn-secondary btn-lg btn-circle"),f(Jl,"class","btn btn-loading"),f(Kl,"class","btn btn-loading btn-primary btn-sm"),f(Ql,"class","btn btn-loading btn-danger btn-lg"),f(Ul,"class","btn btn-loading btn-circle"),f(Vl,"class","btn btn-loading btn-primary btn-sm btn-circle"),f(Wl,"class","btn btn-loading btn-danger btn-lg btn-circle"),R.disabled=!0,f(R,"class","btn btn-loading"),X.disabled=!0,f(X,"class","btn btn-loading btn-primary btn-sm"),D.disabled=!0,f(D,"class","btn btn-loading btn-danger btn-lg"),z.disabled=!0,f(z,"class","btn btn-loading btn-circle"),F.disabled=!0,f(F,"class","btn btn-loading btn-primary btn-sm btn-circle"),G.disabled=!0,f(G,"class","btn btn-loading btn-danger btn-lg btn-circle"),f(St,"type","text"),H.__value="1",H.value=H.__value,H.selected=!0,K.__value="",K.value=K.__value,Q.__value="",Q.value=Q.__value,f(Yl,"cols","30"),f(Yl,"rows","10"),f(Zl,"class","form-field required"),f(rl,"class","form-field required"),f(qt,"for","field_3"),M.__value="1",M.value=M.__value,M.selected=!0,V.__value="",V.value=V.__value,W.__value="",W.value=W.__value,f(U,"id","field_3"),f(C,"class","form-field"),f(cl,"class","form-field"),f(al,"class","form-field"),T.__value="1",T.value=T.__value,T.selected=!0,Y.__value="",Y.value=Y.__value,Z.__value="",Z.value=Z.__value,f(hl,"class","form-field"),f(lt,"class","form-field"),f(tt,"class","form-field"),f(it,"class","form-field"),f(st,"class","form-field form-field-toggle"),f(et,"class","form-field error"),f(nt,"class","form-field"),f(ft,"class","form-field"),f(bt,"class","form-field has-error"),f(ut,"class","form-field disabled"),f(ot,"class","form-field"),f(pt,"class","form-field"),f(mt,"class","form-field form-field-toggle disabled"),f(vt,"class","form-field disabled"),f(dt,"class","form-field"),f(xt,"class","form-group")},m(l,t){i(l,b,t),u(b,o),u(b,p),Dt(m,b,null),i(l,d,t),i(l,Jt,t),i(l,Kt,t),i(l,_,t),u(_,_t),u(_,Ze),Dt(y,_,null),i(l,Qt,t),i(l,Ut,t),i(l,Vt,t),i(l,L,t),u(L,Lt),u(L,re),Dt(B,L,null),i(l,Wt,t),i(l,Yt,t),i(l,Zt,t),i(l,r,t),i(l,rt,t),i(l,c,t),i(l,ct,t),i(l,a,t),i(l,at,t),i(l,h,t),i(l,ht,t),i(l,g,t),i(l,gt,t),i(l,li,t),i(l,ti,t),i(l,Ht,t),i(l,ii,t),i(l,Ct,t),i(l,si,t),i(l,Mt,t),i(l,ei,t),i(l,Tt,t),i(l,ni,t),i(l,yt,t),i(l,fi,t),i(l,Bt,t),i(l,bi,t),i(l,wt,t),i(l,ui,t),i(l,kt,t),i(l,oi,t),i(l,$t,t),i(l,pi,t),i(l,Ot,t),i(l,mi,t),i(l,It,t),i(l,vi,t),i(l,Et,t),i(l,di,t),i(l,xi,t),i(l,_i,t),i(l,ll,t),i(l,Li,t),i(l,Nt,t),i(l,Hi,t),i(l,Pt,t),i(l,Ci,t),i(l,jt,t),i(l,Mi,t),i(l,At,t),i(l,Ti,t),i(l,yi,t),i(l,Bi,t),i(l,tl,t),i(l,wi,t),i(l,ki,t),i(l,$i,t),i(l,il,t),i(l,Oi,t),i(l,sl,t),i(l,Ii,t),i(l,el,t),i(l,Ei,t),i(l,nl,t),i(l,Ni,t),i(l,fl,t),i(l,Pi,t),i(l,bl,t),i(l,ji,t),i(l,Ai,t),i(l,Si,t),i(l,ul,t),i(l,qi,t),i(l,ol,t),i(l,Ri,t),i(l,pl,t),i(l,Xi,t),i(l,ml,t),i(l,Di,t),i(l,vl,t),i(l,zi,t),i(l,dl,t),i(l,Fi,t),i(l,Gi,t),i(l,Ji,t),i(l,xl,t),i(l,Ki,t),i(l,_l,t),i(l,Qi,t),i(l,Ll,t),i(l,Ui,t),i(l,Hl,t),i(l,Vi,t),i(l,Cl,t),i(l,Wi,t),i(l,Ml,t),i(l,Yi,t),i(l,Zi,t),i(l,ri,t),i(l,w,t),i(l,ci,t),i(l,k,t),i(l,ai,t),i(l,$,t),i(l,hi,t),i(l,O,t),i(l,gi,t),i(l,I,t),i(l,ls,t),i(l,E,t),i(l,ts,t),i(l,N,t),i(l,is,t),i(l,P,t),i(l,ss,t),i(l,j,t),i(l,es,t),i(l,A,t),i(l,ns,t),i(l,S,t),i(l,fs,t),i(l,q,t),i(l,bs,t),i(l,us,t),i(l,os,t),i(l,Tl,t),i(l,ps,t),i(l,yl,t),i(l,ms,t),i(l,Bl,t),i(l,vs,t),i(l,wl,t),i(l,ds,t),i(l,kl,t),i(l,xs,t),i(l,_s,t),i(l,Ls,t),i(l,$l,t),i(l,Hs,t),i(l,Ol,t),i(l,Cs,t),i(l,Il,t),i(l,Ms,t),i(l,El,t),i(l,Ts,t),i(l,Nl,t),i(l,ys,t),i(l,Bs,t),i(l,ws,t),i(l,Pl,t),i(l,ks,t),i(l,jl,t),i(l,$s,t),i(l,Al,t),i(l,Os,t),i(l,Sl,t),i(l,Is,t),i(l,ql,t),i(l,Es,t),i(l,Ns,t),i(l,Ps,t),i(l,Rl,t),i(l,js,t),i(l,Xl,t),i(l,As,t),i(l,Dl,t),i(l,Ss,t),i(l,zl,t),i(l,qs,t),i(l,Fl,t),i(l,Rs,t),i(l,Gl,t),i(l,Xs,t),i(l,Ds,t),i(l,zs,t),i(l,Jl,t),i(l,Fs,t),i(l,Kl,t),i(l,Gs,t),i(l,Ql,t),i(l,Js,t),i(l,Ul,t),i(l,Ks,t),i(l,Vl,t),i(l,Qs,t),i(l,Wl,t),i(l,Us,t),i(l,Vs,t),i(l,Ws,t),i(l,R,t),i(l,Ys,t),i(l,X,t),i(l,Zs,t),i(l,D,t),i(l,rs,t),i(l,z,t),i(l,cs,t),i(l,F,t),i(l,as,t),i(l,G,t),i(l,hs,t),i(l,gs,t),i(l,le,t),i(l,St,t),i(l,te,t),i(l,ie,t),i(l,se,t),i(l,J,t),u(J,H),u(J,K),u(J,Q),i(l,ee,t),i(l,ne,t),i(l,fe,t),i(l,Yl,t),i(l,be,t),i(l,ue,t),i(l,oe,t),i(l,Zl,t),i(l,pe,t),i(l,rl,t),i(l,me,t),i(l,C,t),u(C,qt),u(C,ce),u(C,U),u(U,M),u(U,V),u(U,W),i(l,ve,t),i(l,de,t),i(l,xe,t),i(l,cl,t),i(l,_e,t),i(l,al,t),i(l,Le,t),i(l,hl,t),u(hl,gl),u(gl,T),u(gl,Y),u(gl,Z),i(l,He,t),i(l,Ce,t),i(l,Me,t),i(l,lt,t),i(l,Te,t),i(l,tt,t),i(l,ye,t),i(l,it,t),i(l,Be,t),i(l,st,t),i(l,we,t),i(l,ke,t),i(l,$e,t),i(l,et,t),i(l,Oe,t),i(l,nt,t),i(l,Ie,t),i(l,ft,t),i(l,Ee,t),i(l,bt,t),i(l,Ne,t),i(l,Pe,t),i(l,je,t),i(l,ut,t),i(l,Ae,t),i(l,ot,t),i(l,Se,t),i(l,pt,t),i(l,qe,t),i(l,mt,t),i(l,Re,t),i(l,Xe,t),i(l,De,t),i(l,vt,t),i(l,ze,t),i(l,dt,t),i(l,Fe,t),i(l,Ge,t),i(l,Je,t),i(l,xt,t),i(l,Ke,t),Dt(x,l,t),Rt=!0,Ue||(ae=bn(un.call(null,tl,"My tooltip")),Ue=!0)},p(l,[t]){const Ve={};t&4&&(Ve.$$scope={dirty:t,ctx:l}),!Qe&&t&1&&(Qe=!0,Ve.active=l[0],on(()=>Qe=!1)),x.$set(Ve)},i(l){Rt||(zt(m.$$.fragment,l),zt(y.$$.fragment,l),zt(B.$$.fragment,l),zt(x.$$.fragment,l),Rt=!0)},o(l){Ft(m.$$.fragment,l),Ft(y.$$.fragment,l),Ft(B.$$.fragment,l),Ft(x.$$.fragment,l),Rt=!1},d(l){l&&s(b),Gt(m),l&&s(d),l&&s(Jt),l&&s(Kt),l&&s(_),Gt(y),l&&s(Qt),l&&s(Ut),l&&s(Vt),l&&s(L),Gt(B),l&&s(Wt),l&&s(Yt),l&&s(Zt),l&&s(r),l&&s(rt),l&&s(c),l&&s(ct),l&&s(a),l&&s(at),l&&s(h),l&&s(ht),l&&s(g),l&&s(gt),l&&s(li),l&&s(ti),l&&s(Ht),l&&s(ii),l&&s(Ct),l&&s(si),l&&s(Mt),l&&s(ei),l&&s(Tt),l&&s(ni),l&&s(yt),l&&s(fi),l&&s(Bt),l&&s(bi),l&&s(wt),l&&s(ui),l&&s(kt),l&&s(oi),l&&s($t),l&&s(pi),l&&s(Ot),l&&s(mi),l&&s(It),l&&s(vi),l&&s(Et),l&&s(di),l&&s(xi),l&&s(_i),l&&s(ll),l&&s(Li),l&&s(Nt),l&&s(Hi),l&&s(Pt),l&&s(Ci),l&&s(jt),l&&s(Mi),l&&s(At),l&&s(Ti),l&&s(yi),l&&s(Bi),l&&s(tl),l&&s(wi),l&&s(ki),l&&s($i),l&&s(il),l&&s(Oi),l&&s(sl),l&&s(Ii),l&&s(el),l&&s(Ei),l&&s(nl),l&&s(Ni),l&&s(fl),l&&s(Pi),l&&s(bl),l&&s(ji),l&&s(Ai),l&&s(Si),l&&s(ul),l&&s(qi),l&&s(ol),l&&s(Ri),l&&s(pl),l&&s(Xi),l&&s(ml),l&&s(Di),l&&s(vl),l&&s(zi),l&&s(dl),l&&s(Fi),l&&s(Gi),l&&s(Ji),l&&s(xl),l&&s(Ki),l&&s(_l),l&&s(Qi),l&&s(Ll),l&&s(Ui),l&&s(Hl),l&&s(Vi),l&&s(Cl),l&&s(Wi),l&&s(Ml),l&&s(Yi),l&&s(Zi),l&&s(ri),l&&s(w),l&&s(ci),l&&s(k),l&&s(ai),l&&s($),l&&s(hi),l&&s(O),l&&s(gi),l&&s(I),l&&s(ls),l&&s(E),l&&s(ts),l&&s(N),l&&s(is),l&&s(P),l&&s(ss),l&&s(j),l&&s(es),l&&s(A),l&&s(ns),l&&s(S),l&&s(fs),l&&s(q),l&&s(bs),l&&s(us),l&&s(os),l&&s(Tl),l&&s(ps),l&&s(yl),l&&s(ms),l&&s(Bl),l&&s(vs),l&&s(wl),l&&s(ds),l&&s(kl),l&&s(xs),l&&s(_s),l&&s(Ls),l&&s($l),l&&s(Hs),l&&s(Ol),l&&s(Cs),l&&s(Il),l&&s(Ms),l&&s(El),l&&s(Ts),l&&s(Nl),l&&s(ys),l&&s(Bs),l&&s(ws),l&&s(Pl),l&&s(ks),l&&s(jl),l&&s($s),l&&s(Al),l&&s(Os),l&&s(Sl),l&&s(Is),l&&s(ql),l&&s(Es),l&&s(Ns),l&&s(Ps),l&&s(Rl),l&&s(js),l&&s(Xl),l&&s(As),l&&s(Dl),l&&s(Ss),l&&s(zl),l&&s(qs),l&&s(Fl),l&&s(Rs),l&&s(Gl),l&&s(Xs),l&&s(Ds),l&&s(zs),l&&s(Jl),l&&s(Fs),l&&s(Kl),l&&s(Gs),l&&s(Ql),l&&s(Js),l&&s(Ul),l&&s(Ks),l&&s(Vl),l&&s(Qs),l&&s(Wl),l&&s(Us),l&&s(Vs),l&&s(Ws),l&&s(R),l&&s(Ys),l&&s(X),l&&s(Zs),l&&s(D),l&&s(rs),l&&s(z),l&&s(cs),l&&s(F),l&&s(as),l&&s(G),l&&s(hs),l&&s(gs),l&&s(le),l&&s(St),l&&s(te),l&&s(ie),l&&s(se),l&&s(J),l&&s(ee),l&&s(ne),l&&s(fe),l&&s(Yl),l&&s(be),l&&s(ue),l&&s(oe),l&&s(Zl),l&&s(pe),l&&s(rl),l&&s(me),l&&s(C),l&&s(ve),l&&s(de),l&&s(xe),l&&s(cl),l&&s(_e),l&&s(al),l&&s(Le),l&&s(hl),l&&s(He),l&&s(Ce),l&&s(Me),l&&s(lt),l&&s(Te),l&&s(tt),l&&s(ye),l&&s(it),l&&s(Be),l&&s(st),l&&s(we),l&&s(ke),l&&s($e),l&&s(et),l&&s(Oe),l&&s(nt),l&&s(Ie),l&&s(ft),l&&s(Ee),l&&s(bt),l&&s(Ne),l&&s(Pe),l&&s(je),l&&s(ut),l&&s(Ae),l&&s(ot),l&&s(Se),l&&s(pt),l&&s(qe),l&&s(mt),l&&s(Re),l&&s(Xe),l&&s(De),l&&s(vt),l&&s(ze),l&&s(dt),l&&s(Fe),l&&s(Ge),l&&s(Je),l&&s(xt),l&&s(Ke),Gt(x,l),Ue=!1,ae()}}}function _n(v,b,o){let p=!0;setTimeout(function(){pn("Hello world")},500);function m(d){p=d,o(0,p)}return[p,m]}class Hn extends ln{constructor(b){super(),tn(this,b,_n,xn,sn,{})}}export{Hn as default}; diff --git a/ui/dist/assets/FilterAutocompleteInput.15d21df7.js b/ui/dist/assets/FilterAutocompleteInput.15d21df7.js new file mode 100644 index 00000000..3102cdff --- /dev/null +++ b/ui/dist/assets/FilterAutocompleteInput.15d21df7.js @@ -0,0 +1,12 @@ +import{S as Sa,i as va,s as Ca,e as Aa,g as Ma,h as Da,u as xn,p as Oa,M as Ta,N as Ba,P as Pa,Q as Ra,R as La,H as kn,b as Ea}from"./index.944ee0db.js";class z{constructor(){}lineAt(e){if(e<0||e>this.length)throw new RangeError(`Invalid position ${e} in document of length ${this.length}`);return this.lineInner(e,!1,1,0)}line(e){if(e<1||e>this.lines)throw new RangeError(`Invalid line number ${e} in ${this.lines}-line document`);return this.lineInner(e,!0,1,0)}replace(e,t,i){let s=[];return this.decompose(0,e,s,2),i.length&&i.decompose(0,i.length,s,3),this.decompose(t,this.length,s,1),Ve.from(s,this.length-(t-e)+i.length)}append(e){return this.replace(this.length,this.length,e)}slice(e,t=this.length){let i=[];return this.decompose(e,t,i,0),Ve.from(i,t-e)}eq(e){if(e==this)return!0;if(e.length!=this.length||e.lines!=this.lines)return!1;let t=this.scanIdentical(e,1),i=this.length-this.scanIdentical(e,-1),s=new _t(this),r=new _t(e);for(let o=t,l=t;;){if(s.next(o),r.next(o),o=0,s.lineBreak!=r.lineBreak||s.done!=r.done||s.value!=r.value)return!1;if(l+=s.value.length,s.done||l>=i)return!0}}iter(e=1){return new _t(this,e)}iterRange(e,t=this.length){return new zo(this,e,t)}iterLines(e,t){let i;if(e==null)i=this.iter();else{t==null&&(t=this.lines+1);let s=this.line(e).from;i=this.iterRange(s,Math.max(s,t==this.lines+1?this.length:t<=1?0:this.line(t-1).to))}return new qo(i)}toString(){return this.sliceString(0)}toJSON(){let e=[];return this.flatten(e),e}static of(e){if(e.length==0)throw new RangeError("A document must have at least one line");return e.length==1&&!e[0]?z.empty:e.length<=32?new Q(e):Ve.from(Q.split(e,[]))}}class Q extends z{constructor(e,t=Ia(e)){super(),this.text=e,this.length=t}get lines(){return this.text.length}get children(){return null}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.text[r],l=s+o.length;if((t?i:l)>=e)return new Na(s,l,i,o);s=l+1,i++}}decompose(e,t,i,s){let r=e<=0&&t>=this.length?this:new Q(hr(this.text,e,t),Math.min(t,this.length)-Math.max(0,e));if(s&1){let o=i.pop(),l=Ri(r.text,o.text.slice(),0,r.length);if(l.length<=32)i.push(new Q(l,o.length+r.length));else{let h=l.length>>1;i.push(new Q(l.slice(0,h)),new Q(l.slice(h)))}}else i.push(r)}replace(e,t,i){if(!(i instanceof Q))return super.replace(e,t,i);let s=Ri(this.text,Ri(i.text,hr(this.text,0,e)),t),r=this.length+i.length-(t-e);return s.length<=32?new Q(s,r):Ve.from(Q.split(s,[]),r)}sliceString(e,t=this.length,i=` +`){let s="";for(let r=0,o=0;r<=t&&oe&&o&&(s+=i),er&&(s+=l.slice(Math.max(0,e-r),t-r)),r=h+1}return s}flatten(e){for(let t of this.text)e.push(t)}scanIdentical(){return 0}static split(e,t){let i=[],s=-1;for(let r of e)i.push(r),s+=r.length+1,i.length==32&&(t.push(new Q(i,s)),i=[],s=-1);return s>-1&&t.push(new Q(i,s)),t}}class Ve extends z{constructor(e,t){super(),this.children=e,this.length=t,this.lines=0;for(let i of e)this.lines+=i.lines}lineInner(e,t,i,s){for(let r=0;;r++){let o=this.children[r],l=s+o.length,h=i+o.lines-1;if((t?h:l)>=e)return o.lineInner(e,t,i,s);s=l+1,i=h+1}}decompose(e,t,i,s){for(let r=0,o=0;o<=t&&r=o){let a=s&((o<=e?1:0)|(h>=t?2:0));o>=e&&h<=t&&!a?i.push(l):l.decompose(e-o,t-o,i,a)}o=h+1}}replace(e,t,i){if(i.lines=r&&t<=l){let h=o.replace(e-r,t-r,i),a=this.lines-o.lines+h.lines;if(h.lines>5-1&&h.lines>a>>5+1){let c=this.children.slice();return c[s]=h,new Ve(c,this.length-(t-e)+i.length)}return super.replace(r,l,h)}r=l+1}return super.replace(e,t,i)}sliceString(e,t=this.length,i=` +`){let s="";for(let r=0,o=0;re&&r&&(s+=i),eo&&(s+=l.sliceString(e-o,t-o,i)),o=h+1}return s}flatten(e){for(let t of this.children)t.flatten(e)}scanIdentical(e,t){if(!(e instanceof Ve))return 0;let i=0,[s,r,o,l]=t>0?[0,0,this.children.length,e.children.length]:[this.children.length-1,e.children.length-1,-1,-1];for(;;s+=t,r+=t){if(s==o||r==l)return i;let h=this.children[s],a=e.children[r];if(h!=a)return i+h.scanIdentical(a,t);i+=h.length+1}}static from(e,t=e.reduce((i,s)=>i+s.length+1,-1)){let i=0;for(let d of e)i+=d.lines;if(i<32){let d=[];for(let p of e)p.flatten(d);return new Q(d,t)}let s=Math.max(32,i>>5),r=s<<1,o=s>>1,l=[],h=0,a=-1,c=[];function f(d){let p;if(d.lines>r&&d instanceof Ve)for(let g of d.children)f(g);else d.lines>o&&(h>o||!h)?(u(),l.push(d)):d instanceof Q&&h&&(p=c[c.length-1])instanceof Q&&d.lines+p.lines<=32?(h+=d.lines,a+=d.length+1,c[c.length-1]=new Q(p.text.concat(d.text),p.length+1+d.length)):(h+d.lines>s&&u(),h+=d.lines,a+=d.length+1,c.push(d))}function u(){h!=0&&(l.push(c.length==1?c[0]:Ve.from(c,a)),a=-1,h=c.length=0)}for(let d of e)f(d);return u(),l.length==1?l[0]:new Ve(l,t)}}z.empty=new Q([""],0);function Ia(n){let e=-1;for(let t of n)e+=t.length+1;return e}function Ri(n,e,t=0,i=1e9){for(let s=0,r=0,o=!0;r=t&&(h>i&&(l=l.slice(0,i-s)),s0?1:(e instanceof Q?e.text.length:e.children.length)<<1]}nextInner(e,t){for(this.done=this.lineBreak=!1;;){let i=this.nodes.length-1,s=this.nodes[i],r=this.offsets[i],o=r>>1,l=s instanceof Q?s.text.length:s.children.length;if(o==(t>0?l:0)){if(i==0)return this.done=!0,this.value="",this;t>0&&this.offsets[i-1]++,this.nodes.pop(),this.offsets.pop()}else if((r&1)==(t>0?0:1)){if(this.offsets[i]+=t,e==0)return this.lineBreak=!0,this.value=` +`,this;e--}else if(s instanceof Q){let h=s.text[o+(t<0?-1:0)];if(this.offsets[i]+=t,h.length>Math.max(0,e))return this.value=e==0?h:t>0?h.slice(e):h.slice(0,h.length-e),this;e-=h.length}else{let h=s.children[o+(t<0?-1:0)];e>h.length?(e-=h.length,this.offsets[i]+=t):(t<0&&this.offsets[i]--,this.nodes.push(h),this.offsets.push(t>0?1:(h instanceof Q?h.text.length:h.children.length)<<1))}}}next(e=0){return e<0&&(this.nextInner(-e,-this.dir),e=this.value.length),this.nextInner(e,this.dir)}}class zo{constructor(e,t,i){this.value="",this.done=!1,this.cursor=new _t(e,t>i?-1:1),this.pos=t>i?e.length:0,this.from=Math.min(t,i),this.to=Math.max(t,i)}nextInner(e,t){if(t<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;e+=Math.max(0,t<0?this.pos-this.to:this.from-this.pos);let i=t<0?this.pos-this.from:this.to-this.pos;e>i&&(e=i),i-=e;let{value:s}=this.cursor.next(e);return this.pos+=(s.length+e)*t,this.value=s.length<=i?s:t<0?s.slice(s.length-i):s.slice(0,i),this.done=!this.value,this}next(e=0){return e<0?e=Math.max(e,this.from-this.pos):e>0&&(e=Math.min(e,this.to-this.pos)),this.nextInner(e,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class qo{constructor(e){this.inner=e,this.afterBreak=!0,this.value="",this.done=!1}next(e=0){let{done:t,lineBreak:i,value:s}=this.inner.next(e);return t?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=s,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol!="undefined"&&(z.prototype[Symbol.iterator]=function(){return this.iter()},_t.prototype[Symbol.iterator]=zo.prototype[Symbol.iterator]=qo.prototype[Symbol.iterator]=function(){return this});class Na{constructor(e,t,i,s){this.from=e,this.to=t,this.number=i,this.text=s}get length(){return this.to-this.from}}let Dt="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(n=>n?parseInt(n,36):1);for(let n=1;nn)return Dt[e-1]<=n;return!1}function ar(n){return n>=127462&&n<=127487}const cr=8205;function Ae(n,e,t=!0,i=!0){return(t?Ko:Fa)(n,e,i)}function Ko(n,e,t){if(e==n.length)return e;e&&Uo(n.charCodeAt(e))&&jo(n.charCodeAt(e-1))&&e--;let i=re(n,e);for(e+=ve(i);e=0&&ar(re(n,o));)r++,o-=2;if(r%2==0)break;e+=2}else break}return e}function Fa(n,e,t){for(;e>0;){let i=Ko(n,e-2,t);if(i=56320&&n<57344}function jo(n){return n>=55296&&n<56320}function re(n,e){let t=n.charCodeAt(e);if(!jo(t)||e+1==n.length)return t;let i=n.charCodeAt(e+1);return Uo(i)?(t-55296<<10)+(i-56320)+65536:t}function Ps(n){return n<=65535?String.fromCharCode(n):(n-=65536,String.fromCharCode((n>>10)+55296,(n&1023)+56320))}function ve(n){return n<65536?1:2}const Kn=/\r\n?|\n/;var me=function(n){return n[n.Simple=0]="Simple",n[n.TrackDel=1]="TrackDel",n[n.TrackBefore=2]="TrackBefore",n[n.TrackAfter=3]="TrackAfter",n}(me||(me={}));class We{constructor(e){this.sections=e}get length(){let e=0;for(let t=0;te)return r+(e-s);r+=l}else{if(i!=me.Simple&&a>=e&&(i==me.TrackDel&&se||i==me.TrackBefore&&se))return null;if(a>e||a==e&&t<0&&!l)return e==s||t<0?r:r+h;r+=h}s=a}if(e>s)throw new RangeError(`Position ${e} is out of range for changeset of length ${s}`);return r}touchesRange(e,t=e){for(let i=0,s=0;i=0&&s<=t&&l>=e)return st?"cover":!0;s=l}return!1}toString(){let e="";for(let t=0;t=0?":"+s:"")}return e}toJSON(){return this.sections}static fromJSON(e){if(!Array.isArray(e)||e.length%2||e.some(t=>typeof t!="number"))throw new RangeError("Invalid JSON representation of ChangeDesc");return new We(e)}static create(e){return new We(e)}}class ee extends We{constructor(e,t){super(e),this.inserted=t}apply(e){if(this.length!=e.length)throw new RangeError("Applying change set to a document with the wrong length");return Un(this,(t,i,s,r,o)=>e=e.replace(s,s+(i-t),o),!1),e}mapDesc(e,t=!1){return jn(this,e,t,!0)}invert(e){let t=this.sections.slice(),i=[];for(let s=0,r=0;s=0){t[s]=l,t[s+1]=o;let h=s>>1;for(;i.length0&&Ye(i,t,r.text),r.forward(c),l+=c}let a=e[o++];for(;l>1].toJSON()))}return e}static of(e,t,i){let s=[],r=[],o=0,l=null;function h(c=!1){if(!c&&!s.length)return;ou||f<0||u>t)throw new RangeError(`Invalid change range ${f} to ${u} (in doc of length ${t})`);let p=d?typeof d=="string"?z.of(d.split(i||Kn)):d:z.empty,g=p.length;if(f==u&&g==0)return;fo&&he(s,f-o,-1),he(s,u-f,g),Ye(r,s,p),o=u}}return a(e),h(!l),l}static empty(e){return new ee(e?[e,-1]:[],[])}static fromJSON(e){if(!Array.isArray(e))throw new RangeError("Invalid JSON representation of ChangeSet");let t=[],i=[];for(let s=0;sl&&typeof o!="string"))throw new RangeError("Invalid JSON representation of ChangeSet");if(r.length==1)t.push(r[0],0);else{for(;i.length=0&&t<=0&&t==n[s+1]?n[s]+=e:e==0&&n[s]==0?n[s+1]+=t:i?(n[s]+=e,n[s+1]+=t):n.push(e,t)}function Ye(n,e,t){if(t.length==0)return;let i=e.length-2>>1;if(i>1])),!(t||o==n.sections.length||n.sections[o+1]<0);)l=n.sections[o++],h=n.sections[o++];e(s,a,r,c,f),s=a,r=c}}}function jn(n,e,t,i=!1){let s=[],r=i?[]:null,o=new ii(n),l=new ii(e);for(let h=-1;;)if(o.ins==-1&&l.ins==-1){let a=Math.min(o.len,l.len);he(s,a,-1),o.forward(a),l.forward(a)}else if(l.ins>=0&&(o.ins<0||h==o.i||o.off==0&&(l.len=0&&h=0){let a=0,c=o.len;for(;c;)if(l.ins==-1){let f=Math.min(c,l.len);a+=f,c-=f,l.forward(f)}else if(l.ins==0&&l.lenh||o.ins>=0&&o.len>h)&&(l||i.length>a),r.forward2(h),o.forward(h)}}}}class ii{constructor(e){this.set=e,this.i=0,this.next()}next(){let{sections:e}=this.set;this.i>1;return t>=e.length?z.empty:e[t]}textBit(e){let{inserted:t}=this.set,i=this.i-2>>1;return i>=t.length&&!e?z.empty:t[i].slice(this.off,e==null?void 0:this.off+e)}forward(e){e==this.len?this.next():(this.len-=e,this.off+=e)}forward2(e){this.ins==-1?this.forward(e):e==this.ins?this.next():(this.ins-=e,this.off+=e)}}class ct{constructor(e,t,i){this.from=e,this.to=t,this.flags=i}get anchor(){return this.flags&16?this.to:this.from}get head(){return this.flags&16?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return this.flags&4?-1:this.flags&8?1:0}get bidiLevel(){let e=this.flags&3;return e==3?null:e}get goalColumn(){let e=this.flags>>5;return e==33554431?void 0:e}map(e,t=-1){let i,s;return this.empty?i=s=e.mapPos(this.from,t):(i=e.mapPos(this.from,1),s=e.mapPos(this.to,-1)),i==this.from&&s==this.to?this:new ct(i,s,this.flags)}extend(e,t=e){if(e<=this.anchor&&t>=this.anchor)return m.range(e,t);let i=Math.abs(e-this.anchor)>Math.abs(t-this.anchor)?e:t;return m.range(this.anchor,i)}eq(e){return this.anchor==e.anchor&&this.head==e.head}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(e){if(!e||typeof e.anchor!="number"||typeof e.head!="number")throw new RangeError("Invalid JSON representation for SelectionRange");return m.range(e.anchor,e.head)}static create(e,t,i){return new ct(e,t,i)}}class m{constructor(e,t){this.ranges=e,this.mainIndex=t}map(e,t=-1){return e.empty?this:m.create(this.ranges.map(i=>i.map(e,t)),this.mainIndex)}eq(e){if(this.ranges.length!=e.ranges.length||this.mainIndex!=e.mainIndex)return!1;for(let t=0;te.toJSON()),main:this.mainIndex}}static fromJSON(e){if(!e||!Array.isArray(e.ranges)||typeof e.main!="number"||e.main>=e.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new m(e.ranges.map(t=>ct.fromJSON(t)),e.main)}static single(e,t=e){return new m([m.range(e,t)],0)}static create(e,t=0){if(e.length==0)throw new RangeError("A selection needs at least one range");for(let i=0,s=0;se?4:0))}static normalized(e,t=0){let i=e[t];e.sort((s,r)=>s.from-r.from),t=e.indexOf(i);for(let s=1;sr.head?m.range(h,l):m.range(l,h))}}return new m(e,t)}}function Jo(n,e){for(let t of n.ranges)if(t.to>e)throw new RangeError("Selection points outside of document")}let Rs=0;class T{constructor(e,t,i,s,r){this.combine=e,this.compareInput=t,this.compare=i,this.isStatic=s,this.id=Rs++,this.default=e([]),this.extensions=typeof r=="function"?r(this):r}static define(e={}){return new T(e.combine||(t=>t),e.compareInput||((t,i)=>t===i),e.compare||(e.combine?(t,i)=>t===i:Ls),!!e.static,e.enables)}of(e){return new Li([],this,0,e)}compute(e,t){if(this.isStatic)throw new Error("Can't compute a static facet");return new Li(e,this,1,t)}computeN(e,t){if(this.isStatic)throw new Error("Can't compute a static facet");return new Li(e,this,2,t)}from(e,t){return t||(t=i=>i),this.compute([e],i=>t(i.field(e)))}}function Ls(n,e){return n==e||n.length==e.length&&n.every((t,i)=>t===e[i])}class Li{constructor(e,t,i,s){this.dependencies=e,this.facet=t,this.type=i,this.value=s,this.id=Rs++}dynamicSlot(e){var t;let i=this.value,s=this.facet.compareInput,r=this.id,o=e[r]>>1,l=this.type==2,h=!1,a=!1,c=[];for(let f of this.dependencies)f=="doc"?h=!0:f=="selection"?a=!0:(((t=e[f.id])!==null&&t!==void 0?t:1)&1)==0&&c.push(e[f.id]);return{create(f){return f.values[o]=i(f),1},update(f,u){if(h&&u.docChanged||a&&(u.docChanged||u.selection)||Gn(f,c)){let d=i(f);if(l?!fr(d,f.values[o],s):!s(d,f.values[o]))return f.values[o]=d,1}return 0},reconfigure:(f,u)=>{let d=i(f),p=u.config.address[r];if(p!=null){let g=Fi(u,p);if(this.dependencies.every(y=>y instanceof T?u.facet(y)===f.facet(y):y instanceof ke?u.field(y,!1)==f.field(y,!1):!0)||(l?fr(d,g,s):s(d,g)))return f.values[o]=g,0}return f.values[o]=d,1}}}}function fr(n,e,t){if(n.length!=e.length)return!1;for(let i=0;in[h.id]),s=t.map(h=>h.type),r=i.filter(h=>!(h&1)),o=n[e.id]>>1;function l(h){let a=[];for(let c=0;ci===s),e);return e.provide&&(t.provides=e.provide(t)),t}create(e){let t=e.facet(ur).find(i=>i.field==this);return((t==null?void 0:t.create)||this.createF)(e)}slot(e){let t=e[this.id]>>1;return{create:i=>(i.values[t]=this.create(i),1),update:(i,s)=>{let r=i.values[t],o=this.updateF(r,s);return this.compareF(r,o)?0:(i.values[t]=o,1)},reconfigure:(i,s)=>s.config.address[this.id]!=null?(i.values[t]=s.field(this),0):(i.values[t]=this.create(i),1)}}init(e){return[this,ur.of({field:this,create:e})]}get extension(){return this}}const Ct={lowest:4,low:3,default:2,high:1,highest:0};function zt(n){return e=>new $o(e,n)}const fi={highest:zt(Ct.highest),high:zt(Ct.high),default:zt(Ct.default),low:zt(Ct.low),lowest:zt(Ct.lowest)};class $o{constructor(e,t){this.inner=e,this.prec=t}}class _e{of(e){return new Jn(this,e)}reconfigure(e){return _e.reconfigure.of({compartment:this,extension:e})}get(e){return e.config.compartments.get(this)}}class Jn{constructor(e,t){this.compartment=e,this.inner=t}}class Vi{constructor(e,t,i,s,r,o){for(this.base=e,this.compartments=t,this.dynamicSlots=i,this.address=s,this.staticValues=r,this.facets=o,this.statusTemplate=[];this.statusTemplate.length>1]}static resolve(e,t,i){let s=[],r=Object.create(null),o=new Map;for(let u of Wa(e,t,o))u instanceof ke?s.push(u):(r[u.facet.id]||(r[u.facet.id]=[])).push(u);let l=Object.create(null),h=[],a=[];for(let u of s)l[u.id]=a.length<<1,a.push(d=>u.slot(d));let c=i==null?void 0:i.config.facets;for(let u in r){let d=r[u],p=d[0].facet,g=c&&c[u]||[];if(d.every(y=>y.type==0))if(l[p.id]=h.length<<1|1,Ls(g,d))h.push(i.facet(p));else{let y=p.combine(d.map(b=>b.value));h.push(i&&p.compare(y,i.facet(p))?i.facet(p):y)}else{for(let y of d)y.type==0?(l[y.id]=h.length<<1|1,h.push(y.value)):(l[y.id]=a.length<<1,a.push(b=>y.dynamicSlot(b)));l[p.id]=a.length<<1,a.push(y=>Ha(y,p,d))}}let f=a.map(u=>u(l));return new Vi(e,o,f,l,h,r)}}function Wa(n,e,t){let i=[[],[],[],[],[]],s=new Map;function r(o,l){let h=s.get(o);if(h!=null){if(h<=l)return;let a=i[h].indexOf(o);a>-1&&i[h].splice(a,1),o instanceof Jn&&t.delete(o.compartment)}if(s.set(o,l),Array.isArray(o))for(let a of o)r(a,l);else if(o instanceof Jn){if(t.has(o.compartment))throw new RangeError("Duplicate use of compartment in extensions");let a=e.get(o.compartment)||o.inner;t.set(o.compartment,a),r(a,l)}else if(o instanceof $o)r(o.inner,o.prec);else if(o instanceof ke)i[l].push(o),o.provides&&r(o.provides,l);else if(o instanceof Li)i[l].push(o),o.facet.extensions&&r(o.facet.extensions,l);else{let a=o.extension;if(!a)throw new Error(`Unrecognized extension value in extension set (${o}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);r(a,l)}}return r(n,Ct.default),i.reduce((o,l)=>o.concat(l))}function Qt(n,e){if(e&1)return 2;let t=e>>1,i=n.status[t];if(i==4)throw new Error("Cyclic dependency between fields and/or facets");if(i&2)return i;n.status[t]=4;let s=n.computeSlot(n,n.config.dynamicSlots[t]);return n.status[t]=2|s}function Fi(n,e){return e&1?n.config.staticValues[e>>1]:n.values[e>>1]}const Xo=T.define(),Yo=T.define({combine:n=>n.some(e=>e),static:!0}),_o=T.define({combine:n=>n.length?n[0]:void 0,static:!0}),Qo=T.define(),Zo=T.define(),el=T.define(),tl=T.define({combine:n=>n.length?n[0]:!1});class wt{constructor(e,t){this.type=e,this.value=t}static define(){return new za}}class za{of(e){return new wt(this,e)}}class qa{constructor(e){this.map=e}of(e){return new H(this,e)}}class H{constructor(e,t){this.type=e,this.value=t}map(e){let t=this.type.map(this.value,e);return t===void 0?void 0:t==this.value?this:new H(this.type,t)}is(e){return this.type==e}static define(e={}){return new qa(e.map||(t=>t))}static mapEffects(e,t){if(!e.length)return e;let i=[];for(let s of e){let r=s.map(t);r&&i.push(r)}return i}}H.reconfigure=H.define();H.appendConfig=H.define();class te{constructor(e,t,i,s,r,o){this.startState=e,this.changes=t,this.selection=i,this.effects=s,this.annotations=r,this.scrollIntoView=o,this._doc=null,this._state=null,i&&Jo(i,t.newLength),r.some(l=>l.type==te.time)||(this.annotations=r.concat(te.time.of(Date.now())))}static create(e,t,i,s,r,o){return new te(e,t,i,s,r,o)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(e){for(let t of this.annotations)if(t.type==e)return t.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(e){let t=this.annotation(te.userEvent);return!!(t&&(t==e||t.length>e.length&&t.slice(0,e.length)==e&&t[e.length]=="."))}}te.time=wt.define();te.userEvent=wt.define();te.addToHistory=wt.define();te.remote=wt.define();function Ka(n,e){let t=[];for(let i=0,s=0;;){let r,o;if(i=n[i]))r=n[i++],o=n[i++];else if(s=0;s--){let r=i[s](n);r instanceof te?n=r:Array.isArray(r)&&r.length==1&&r[0]instanceof te?n=r[0]:n=nl(e,Ot(r),!1)}return n}function ja(n){let e=n.startState,t=e.facet(el),i=n;for(let s=t.length-1;s>=0;s--){let r=t[s](n);r&&Object.keys(r).length&&(i=il(n,$n(e,r,n.changes.newLength),!0))}return i==n?n:te.create(e,n.changes,n.selection,i.effects,i.annotations,i.scrollIntoView)}const Ga=[];function Ot(n){return n==null?Ga:Array.isArray(n)?n:[n]}var ce=function(n){return n[n.Word=0]="Word",n[n.Space=1]="Space",n[n.Other=2]="Other",n}(ce||(ce={}));const Ja=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let Xn;try{Xn=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch{}function $a(n){if(Xn)return Xn.test(n);for(let e=0;e"\x80"&&(t.toUpperCase()!=t.toLowerCase()||Ja.test(t)))return!0}return!1}function Xa(n){return e=>{if(!/\S/.test(e))return ce.Space;if($a(e))return ce.Word;for(let t=0;t-1)return ce.Word;return ce.Other}}class V{constructor(e,t,i,s,r,o){this.config=e,this.doc=t,this.selection=i,this.values=s,this.status=e.statusTemplate.slice(),this.computeSlot=r,o&&(o._state=this);for(let l=0;ls.set(h,l)),t=null),s.set(o.value.compartment,o.value.extension)):o.is(H.reconfigure)?(t=null,i=o.value):o.is(H.appendConfig)&&(t=null,i=Ot(i).concat(o.value));let r;t?r=e.startState.values.slice():(t=Vi.resolve(i,s,this),r=new V(t,this.doc,this.selection,t.dynamicSlots.map(()=>null),(l,h)=>h.reconfigure(l,this),null).values),new V(t,e.newDoc,e.newSelection,r,(o,l)=>l.update(o,e),e)}replaceSelection(e){return typeof e=="string"&&(e=this.toText(e)),this.changeByRange(t=>({changes:{from:t.from,to:t.to,insert:e},range:m.cursor(t.from+e.length)}))}changeByRange(e){let t=this.selection,i=e(t.ranges[0]),s=this.changes(i.changes),r=[i.range],o=Ot(i.effects);for(let l=1;lo.spec.fromJSON(l,h)))}}return V.create({doc:e.doc,selection:m.fromJSON(e.selection),extensions:t.extensions?s.concat([t.extensions]):s})}static create(e={}){let t=Vi.resolve(e.extensions||[],new Map),i=e.doc instanceof z?e.doc:z.of((e.doc||"").split(t.staticFacet(V.lineSeparator)||Kn)),s=e.selection?e.selection instanceof m?e.selection:m.single(e.selection.anchor,e.selection.head):m.single(0);return Jo(s,i.length),t.staticFacet(Yo)||(s=s.asSingle()),new V(t,i,s,t.dynamicSlots.map(()=>null),(r,o)=>o.create(r),null)}get tabSize(){return this.facet(V.tabSize)}get lineBreak(){return this.facet(V.lineSeparator)||` +`}get readOnly(){return this.facet(tl)}phrase(e,...t){for(let i of this.facet(V.phrases))if(Object.prototype.hasOwnProperty.call(i,e)){e=i[e];break}return t.length&&(e=e.replace(/\$(\$|\d*)/g,(i,s)=>{if(s=="$")return"$";let r=+(s||1);return!r||r>t.length?i:t[r-1]})),e}languageDataAt(e,t,i=-1){let s=[];for(let r of this.facet(Xo))for(let o of r(this,t,i))Object.prototype.hasOwnProperty.call(o,e)&&s.push(o[e]);return s}charCategorizer(e){return Xa(this.languageDataAt("wordChars",e).join(""))}wordAt(e){let{text:t,from:i,length:s}=this.doc.lineAt(e),r=this.charCategorizer(e),o=e-i,l=e-i;for(;o>0;){let h=Ae(t,o,!1);if(r(t.slice(h,o))!=ce.Word)break;o=h}for(;ln.length?n[0]:4});V.lineSeparator=_o;V.readOnly=tl;V.phrases=T.define({compare(n,e){let t=Object.keys(n),i=Object.keys(e);return t.length==i.length&&t.every(s=>n[s]==e[s])}});V.languageData=Xo;V.changeFilter=Qo;V.transactionFilter=Zo;V.transactionExtender=el;_e.reconfigure=H.define();function Ht(n,e,t={}){let i={};for(let s of n)for(let r of Object.keys(s)){let o=s[r],l=i[r];if(l===void 0)i[r]=o;else if(!(l===o||o===void 0))if(Object.hasOwnProperty.call(t,r))i[r]=t[r](l,o);else throw new Error("Config merge conflict for field "+r)}for(let s in e)i[s]===void 0&&(i[s]=e[s]);return i}class pt{eq(e){return this==e}range(e,t=e){return ni.create(e,t,this)}}pt.prototype.startSide=pt.prototype.endSide=0;pt.prototype.point=!1;pt.prototype.mapMode=me.TrackDel;class ni{constructor(e,t,i){this.from=e,this.to=t,this.value=i}static create(e,t,i){return new ni(e,t,i)}}function Yn(n,e){return n.from-e.from||n.value.startSide-e.value.startSide}class Es{constructor(e,t,i,s){this.from=e,this.to=t,this.value=i,this.maxPoint=s}get length(){return this.to[this.to.length-1]}findIndex(e,t,i,s=0){let r=i?this.to:this.from;for(let o=s,l=r.length;;){if(o==l)return o;let h=o+l>>1,a=r[h]-e||(i?this.value[h].endSide:this.value[h].startSide)-t;if(h==o)return a>=0?o:l;a>=0?l=h:o=h+1}}between(e,t,i,s){for(let r=this.findIndex(t,-1e9,!0),o=this.findIndex(i,1e9,!1,r);rd||u==d&&a.startSide>0&&a.endSide<=0)continue;(d-u||a.endSide-a.startSide)<0||(o<0&&(o=u),a.point&&(l=Math.max(l,d-u)),i.push(a),s.push(u-o),r.push(d-o))}return{mapped:i.length?new Es(s,r,i,l):null,pos:o}}}class Y{constructor(e,t,i,s){this.chunkPos=e,this.chunk=t,this.nextLayer=i,this.maxPoint=s}static create(e,t,i,s){return new Y(e,t,i,s)}get length(){let e=this.chunk.length-1;return e<0?0:Math.max(this.chunkEnd(e),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let e=this.nextLayer.size;for(let t of this.chunk)e+=t.value.length;return e}chunkEnd(e){return this.chunkPos[e]+this.chunk[e].length}update(e){let{add:t=[],sort:i=!1,filterFrom:s=0,filterTo:r=this.length}=e,o=e.filter;if(t.length==0&&!o)return this;if(i&&(t=t.slice().sort(Yn)),this.isEmpty)return t.length?Y.of(t):this;let l=new sl(this,null,-1).goto(0),h=0,a=[],c=new gt;for(;l.value||h=0){let f=t[h++];c.addInner(f.from,f.to,f.value)||a.push(f)}else l.rangeIndex==1&&l.chunkIndexthis.chunkEnd(l.chunkIndex)||rl.to||r=r&&e<=r+o.length&&o.between(r,e-r,t-r,i)===!1)return}this.nextLayer.between(e,t,i)}}iter(e=0){return si.from([this]).goto(e)}get isEmpty(){return this.nextLayer==this}static iter(e,t=0){return si.from(e).goto(t)}static compare(e,t,i,s,r=-1){let o=e.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),l=t.filter(f=>f.maxPoint>0||!f.isEmpty&&f.maxPoint>=r),h=dr(o,l,i),a=new qt(o,h,r),c=new qt(l,h,r);i.iterGaps((f,u,d)=>pr(a,f,c,u,d,s)),i.empty&&i.length==0&&pr(a,0,c,0,0,s)}static eq(e,t,i=0,s){s==null&&(s=1e9);let r=e.filter(c=>!c.isEmpty&&t.indexOf(c)<0),o=t.filter(c=>!c.isEmpty&&e.indexOf(c)<0);if(r.length!=o.length)return!1;if(!r.length)return!0;let l=dr(r,o),h=new qt(r,l,0).goto(i),a=new qt(o,l,0).goto(i);for(;;){if(h.to!=a.to||!_n(h.active,a.active)||h.point&&(!a.point||!h.point.eq(a.point)))return!1;if(h.to>s)return!0;h.next(),a.next()}}static spans(e,t,i,s,r=-1){let o=new qt(e,null,r).goto(t),l=t,h=o.openStart;for(;;){let a=Math.min(o.to,i);if(o.point?(s.point(l,a,o.point,o.activeForPoint(o.to),h,o.pointRank),h=o.openEnd(a)+(o.to>a?1:0)):a>l&&(s.span(l,a,o.active,h),h=o.openEnd(a)),o.to>i)break;l=o.to,o.next()}return h}static of(e,t=!1){let i=new gt;for(let s of e instanceof ni?[e]:t?Ya(e):e)i.add(s.from,s.to,s.value);return i.finish()}}Y.empty=new Y([],[],null,-1);function Ya(n){if(n.length>1)for(let e=n[0],t=1;t0)return n.slice().sort(Yn);e=i}return n}Y.empty.nextLayer=Y.empty;class gt{constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this.last=null,this.lastFrom=-1e9,this.lastTo=-1e9,this.from=[],this.to=[],this.value=[],this.maxPoint=-1,this.setMaxPoint=-1,this.nextLayer=null}finishChunk(e){this.chunks.push(new Es(this.from,this.to,this.value,this.maxPoint)),this.chunkPos.push(this.chunkStart),this.chunkStart=-1,this.setMaxPoint=Math.max(this.setMaxPoint,this.maxPoint),this.maxPoint=-1,e&&(this.from=[],this.to=[],this.value=[])}add(e,t,i){this.addInner(e,t,i)||(this.nextLayer||(this.nextLayer=new gt)).add(e,t,i)}addInner(e,t,i){let s=e-this.lastTo||i.startSide-this.last.endSide;if(s<=0&&(e-this.lastFrom||i.startSide-this.last.startSide)<0)throw new Error("Ranges must be added sorted by `from` position and `startSide`");return s<0?!1:(this.from.length==250&&this.finishChunk(!0),this.chunkStart<0&&(this.chunkStart=e),this.from.push(e-this.chunkStart),this.to.push(t-this.chunkStart),this.last=i,this.lastFrom=e,this.lastTo=t,this.value.push(i),i.point&&(this.maxPoint=Math.max(this.maxPoint,t-e)),!0)}addChunk(e,t){if((e-this.lastTo||t.value[0].startSide-this.last.endSide)<0)return!1;this.from.length&&this.finishChunk(!0),this.setMaxPoint=Math.max(this.setMaxPoint,t.maxPoint),this.chunks.push(t),this.chunkPos.push(e);let i=t.value.length-1;return this.last=t.value[i],this.lastFrom=t.from[i]+e,this.lastTo=t.to[i]+e,!0}finish(){return this.finishInner(Y.empty)}finishInner(e){if(this.from.length&&this.finishChunk(!1),this.chunks.length==0)return e;let t=Y.create(this.chunkPos,this.chunks,this.nextLayer?this.nextLayer.finishInner(e):e,this.setMaxPoint);return this.from=null,t}}function dr(n,e,t){let i=new Map;for(let r of n)for(let o=0;o=this.minPoint)break}}setRangeIndex(e){if(e==this.layer.chunk[this.chunkIndex].value.length){if(this.chunkIndex++,this.skip)for(;this.chunkIndex=i&&s.push(new sl(o,t,i,r));return s.length==1?s[0]:new si(s)}get startSide(){return this.value?this.value.startSide:0}goto(e,t=-1e9){for(let i of this.heap)i.goto(e,t);for(let i=this.heap.length>>1;i>=0;i--)Sn(this.heap,i);return this.next(),this}forward(e,t){for(let i of this.heap)i.forward(e,t);for(let i=this.heap.length>>1;i>=0;i--)Sn(this.heap,i);(this.to-e||this.value.endSide-t)<0&&this.next()}next(){if(this.heap.length==0)this.from=this.to=1e9,this.value=null,this.rank=-1;else{let e=this.heap[0];this.from=e.from,this.to=e.to,this.value=e.value,this.rank=e.rank,e.value&&e.next(),Sn(this.heap,0)}}}function Sn(n,e){for(let t=n[e];;){let i=(e<<1)+1;if(i>=n.length)break;let s=n[i];if(i+1=0&&(s=n[i+1],i++),t.compare(s)<0)break;n[i]=t,n[e]=s,e=i}}class qt{constructor(e,t,i){this.minPoint=i,this.active=[],this.activeTo=[],this.activeRank=[],this.minActive=-1,this.point=null,this.pointFrom=0,this.pointRank=0,this.to=-1e9,this.endSide=0,this.openStart=-1,this.cursor=si.from(e,t,i)}goto(e,t=-1e9){return this.cursor.goto(e,t),this.active.length=this.activeTo.length=this.activeRank.length=0,this.minActive=-1,this.to=e,this.endSide=t,this.openStart=-1,this.next(),this}forward(e,t){for(;this.minActive>-1&&(this.activeTo[this.minActive]-e||this.active[this.minActive].endSide-t)<0;)this.removeActive(this.minActive);this.cursor.forward(e,t)}removeActive(e){mi(this.active,e),mi(this.activeTo,e),mi(this.activeRank,e),this.minActive=gr(this.active,this.activeTo)}addActive(e){let t=0,{value:i,to:s,rank:r}=this.cursor;for(;t-1&&(this.activeTo[r]-this.cursor.from||this.active[r].endSide-this.cursor.startSide)<0){if(this.activeTo[r]>e){this.to=this.activeTo[r],this.endSide=this.active[r].endSide;break}this.removeActive(r),i&&mi(i,r)}else if(this.cursor.value)if(this.cursor.from>e){this.to=this.cursor.from,this.endSide=this.cursor.startSide;break}else{let o=this.cursor.value;if(!o.point)this.addActive(i),this.cursor.next();else if(t&&this.cursor.to==this.to&&this.cursor.from=0&&!(this.activeRank[i]e||this.activeTo[i]==e&&this.active[i].endSide>=this.point.endSide)&&t.push(this.active[i]);return t.reverse()}openEnd(e){let t=0;for(let i=this.activeTo.length-1;i>=0&&this.activeTo[i]>e;i--)t++;return t}}function pr(n,e,t,i,s,r){n.goto(e),t.goto(i);let o=i+s,l=i,h=i-e;for(;;){let a=n.to+h-t.to||n.endSide-t.endSide,c=a<0?n.to+h:t.to,f=Math.min(c,o);if(n.point||t.point?n.point&&t.point&&(n.point==t.point||n.point.eq(t.point))&&_n(n.activeForPoint(n.to+h),t.activeForPoint(t.to))||r.comparePoint(l,f,n.point,t.point):f>l&&!_n(n.active,t.active)&&r.compareRange(l,f,n.active,t.active),c>o)break;l=c,a<=0&&n.next(),a>=0&&t.next()}}function _n(n,e){if(n.length!=e.length)return!1;for(let t=0;t=e;i--)n[i+1]=n[i];n[e]=t}function gr(n,e){let t=-1,i=1e9;for(let s=0;s=e)return s;if(s==n.length)break;r+=n.charCodeAt(s)==9?t-r%t:1,s=Ae(n,s)}return i===!0?-1:n.length}const Zn="\u037C",mr=typeof Symbol=="undefined"?"__"+Zn:Symbol.for(Zn),es=typeof Symbol=="undefined"?"__styleSet"+Math.floor(Math.random()*1e8):Symbol("styleSet"),yr=typeof globalThis!="undefined"?globalThis:typeof window!="undefined"?window:{};class st{constructor(e,t){this.rules=[];let{finish:i}=t||{};function s(o){return/^@/.test(o)?[o]:o.split(/,\s*/)}function r(o,l,h,a){let c=[],f=/^@(\w+)\b/.exec(o[0]),u=f&&f[1]=="keyframes";if(f&&l==null)return h.push(o[0]+";");for(let d in l){let p=l[d];if(/&/.test(d))r(d.split(/,\s*/).map(g=>o.map(y=>g.replace(/&/,y))).reduce((g,y)=>g.concat(y)),p,h);else if(p&&typeof p=="object"){if(!f)throw new RangeError("The value of a property ("+d+") should be a primitive value.");r(s(d),p,c,u)}else p!=null&&c.push(d.replace(/_.*/,"").replace(/[A-Z]/g,g=>"-"+g.toLowerCase())+": "+p+";")}(c.length||u)&&h.push((i&&!f&&!a?o.map(i):o).join(", ")+" {"+c.join(" ")+"}")}for(let o in e)r(s(o),e[o],this.rules)}getRules(){return this.rules.join(` +`)}static newName(){let e=yr[mr]||1;return yr[mr]=e+1,Zn+e.toString(36)}static mount(e,t){(e[es]||new _a(e)).mount(Array.isArray(t)?t:[t])}}let bi=null;class _a{constructor(e){if(!e.head&&e.adoptedStyleSheets&&typeof CSSStyleSheet!="undefined"){if(bi)return e.adoptedStyleSheets=[bi.sheet].concat(e.adoptedStyleSheets),e[es]=bi;this.sheet=new CSSStyleSheet,e.adoptedStyleSheets=[this.sheet].concat(e.adoptedStyleSheets),bi=this}else{this.styleTag=(e.ownerDocument||e).createElement("style");let t=e.head||e;t.insertBefore(this.styleTag,t.firstChild)}this.modules=[],e[es]=this}mount(e){let t=this.sheet,i=0,s=0;for(let r=0;r-1&&(this.modules.splice(l,1),s--,l=-1),l==-1){if(this.modules.splice(s++,0,o),t)for(let h=0;h",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"',229:"Q"},br=typeof navigator!="undefined"&&/Chrome\/(\d+)/.exec(navigator.userAgent),Qa=typeof navigator!="undefined"&&/Apple Computer/.test(navigator.vendor),Za=typeof navigator!="undefined"&&/Gecko\/\d+/.test(navigator.userAgent),wr=typeof navigator!="undefined"&&/Mac/.test(navigator.platform),ec=typeof navigator!="undefined"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),tc=br&&(wr||+br[1]<57)||Za&≀for(var oe=0;oe<10;oe++)rt[48+oe]=rt[96+oe]=String(oe);for(var oe=1;oe<=24;oe++)rt[oe+111]="F"+oe;for(var oe=65;oe<=90;oe++)rt[oe]=String.fromCharCode(oe+32),Pt[oe]=String.fromCharCode(oe);for(var vn in rt)Pt.hasOwnProperty(vn)||(Pt[vn]=rt[vn]);function ic(n){var e=tc&&(n.ctrlKey||n.altKey||n.metaKey)||(Qa||ec)&&n.shiftKey&&n.key&&n.key.length==1,t=!e&&n.key||(n.shiftKey?Pt:rt)[n.keyCode]||n.key||"Unidentified";return t=="Esc"&&(t="Escape"),t=="Del"&&(t="Delete"),t=="Left"&&(t="ArrowLeft"),t=="Up"&&(t="ArrowUp"),t=="Right"&&(t="ArrowRight"),t=="Down"&&(t="ArrowDown"),t}function Hi(n){let e;return n.nodeType==11?e=n.getSelection?n:n.ownerDocument:e=n,e.getSelection()}function Rt(n,e){return e?n==e||n.contains(e.nodeType!=1?e.parentNode:e):!1}function nc(){let n=document.activeElement;for(;n&&n.shadowRoot;)n=n.shadowRoot.activeElement;return n}function ts(n,e){if(!e.anchorNode)return!1;try{return Rt(n,e.anchorNode)}catch{return!1}}function ri(n){return n.nodeType==3?oi(n,0,n.nodeValue.length).getClientRects():n.nodeType==1?n.getClientRects():[]}function Wi(n,e,t,i){return t?xr(n,e,t,i,-1)||xr(n,e,t,i,1):!1}function is(n){for(var e=0;;e++)if(n=n.previousSibling,!n)return e}function xr(n,e,t,i,s){for(;;){if(n==t&&e==i)return!0;if(e==(s<0?0:zi(n))){if(n.nodeName=="DIV")return!1;let r=n.parentNode;if(!r||r.nodeType!=1)return!1;e=is(n)+(s<0?0:1),n=r}else if(n.nodeType==1){if(n=n.childNodes[e+(s<0?-1:0)],n.nodeType==1&&n.contentEditable=="false")return!1;e=s<0?zi(n):0}else return!1}}function zi(n){return n.nodeType==3?n.nodeValue.length:n.childNodes.length}const rl={left:0,right:0,top:0,bottom:0};function on(n,e){let t=e?n.left:n.right;return{left:t,right:t,top:n.top,bottom:n.bottom}}function sc(n){return{left:0,right:n.innerWidth,top:0,bottom:n.innerHeight}}function rc(n,e,t,i,s,r,o,l){let h=n.ownerDocument,a=h.defaultView;for(let c=n;c;)if(c.nodeType==1){let f,u=c==h.body;if(u)f=sc(a);else{if(c.scrollHeight<=c.clientHeight&&c.scrollWidth<=c.clientWidth){c=c.parentNode;continue}let g=c.getBoundingClientRect();f={left:g.left,right:g.left+c.clientWidth,top:g.top,bottom:g.top+c.clientHeight}}let d=0,p=0;if(s=="nearest")e.top0&&e.bottom>f.bottom+p&&(p=e.bottom-f.bottom+p+o)):e.bottom>f.bottom&&(p=e.bottom-f.bottom+o,t<0&&e.top-p0&&e.right>f.right+d&&(d=e.right-f.right+d+r)):e.right>f.right&&(d=e.right-f.right+r,t<0&&e.leftt)return f.domBoundsAround(e,t,a);if(u>=e&&s==-1&&(s=h,r=a),a>t&&f.dom.parentNode==this.dom){o=h,l=c;break}c=u,a=u+f.breakAfter}return{from:r,to:l<0?i+this.length:l,startDOM:(s?this.children[s-1].dom.nextSibling:null)||this.dom.firstChild,endDOM:o=0?this.children[o].dom:null}}markDirty(e=!1){this.dirty|=2,this.markParentsDirty(e)}markParentsDirty(e){for(let t=this.parent;t;t=t.parent){if(e&&(t.dirty|=2),t.dirty&1)return;t.dirty|=1,e=!1}}setParent(e){this.parent!=e&&(this.parent=e,this.dirty&&this.markParentsDirty(!0))}setDOM(e){this.dom&&(this.dom.cmView=null),this.dom=e,e.cmView=this}get rootView(){for(let e=this;;){let t=e.parent;if(!t)return e;e=t}}replaceChildren(e,t,i=Is){this.markDirty();for(let s=e;sthis.pos||e==this.pos&&(t>0||this.i==0||this.children[this.i-1].breakAfter))return this.off=e-this.pos,this;let i=this.children[--this.i];this.pos-=i.length+i.breakAfter}}}function al(n,e,t,i,s,r,o,l,h){let{children:a}=n,c=a.length?a[e]:null,f=r.length?r[r.length-1]:null,u=f?f.breakAfter:o;if(!(e==i&&c&&!o&&!u&&r.length<2&&c.merge(t,s,r.length?f:null,t==0,l,h))){if(i0&&(!o&&r.length&&c.merge(t,c.length,r[0],!1,l,0)?c.breakAfter=r.shift().breakAfter:(t2);var M={mac:Ar||/Mac/.test(Ce.platform),windows:/Win/.test(Ce.platform),linux:/Linux|X11/.test(Ce.platform),ie:ln,ie_version:fl?ns.documentMode||6:rs?+rs[1]:ss?+ss[1]:0,gecko:vr,gecko_version:vr?+(/Firefox\/(\d+)/.exec(Ce.userAgent)||[0,0])[1]:0,chrome:!!Cn,chrome_version:Cn?+Cn[1]:0,ios:Ar,android:/Android\b/.test(Ce.userAgent),webkit:Cr,safari:ul,webkit_version:Cr?+(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent)||[0,0])[1]:0,tabSize:ns.documentElement.style.tabSize!=null?"tab-size":"-moz-tab-size"};const hc=256;class ot extends ${constructor(e){super(),this.text=e}get length(){return this.text.length}createDOM(e){this.setDOM(e||document.createTextNode(this.text))}sync(e){this.dom||this.createDOM(),this.dom.nodeValue!=this.text&&(e&&e.node==this.dom&&(e.written=!0),this.dom.nodeValue=this.text)}reuseDOM(e){e.nodeType==3&&this.createDOM(e)}merge(e,t,i){return i&&(!(i instanceof ot)||this.length-(t-e)+i.length>hc)?!1:(this.text=this.text.slice(0,e)+(i?i.text:"")+this.text.slice(t),this.markDirty(),!0)}split(e){let t=new ot(this.text.slice(e));return this.text=this.text.slice(0,e),this.markDirty(),t}localPosFromDOM(e,t){return e==this.dom?t:t?this.text.length:0}domAtPos(e){return new le(this.dom,e)}domBoundsAround(e,t,i){return{from:i,to:i+this.length,startDOM:this.dom,endDOM:this.dom.nextSibling}}coordsAt(e,t){return os(this.dom,e,t)}}class ze extends ${constructor(e,t=[],i=0){super(),this.mark=e,this.children=t,this.length=i;for(let s of t)s.setParent(this)}setAttrs(e){if(ll(e),this.mark.class&&(e.className=this.mark.class),this.mark.attrs)for(let t in this.mark.attrs)e.setAttribute(t,this.mark.attrs[t]);return e}reuseDOM(e){e.nodeName==this.mark.tagName.toUpperCase()&&(this.setDOM(e),this.dirty|=6)}sync(e){this.dom?this.dirty&4&&this.setAttrs(this.dom):this.setDOM(this.setAttrs(document.createElement(this.mark.tagName))),super.sync(e)}merge(e,t,i,s,r,o){return i&&(!(i instanceof ze&&i.mark.eq(this.mark))||e&&r<=0||te&&t.push(i=e&&(s=r),i=h,r++}let o=this.length-e;return this.length=e,s>-1&&(this.children.length=s,this.markDirty()),new ze(this.mark,t,o)}domAtPos(e){return gl(this.dom,this.children,e)}coordsAt(e,t){return yl(this,e,t)}}function os(n,e,t){let i=n.nodeValue.length;e>i&&(e=i);let s=e,r=e,o=0;e==0&&t<0||e==i&&t>=0?M.chrome||M.gecko||(e?(s--,o=1):r=0)?0:l.length-1];return M.safari&&!o&&h.width==0&&(h=Array.prototype.find.call(l,a=>a.width)||h),o?on(h,o<0):h||null}class Qe extends ${constructor(e,t,i){super(),this.widget=e,this.length=t,this.side=i,this.prevWidget=null}static create(e,t,i){return new(e.customView||Qe)(e,t,i)}split(e){let t=Qe.create(this.widget,this.length-e,this.side);return this.length-=e,t}sync(){(!this.dom||!this.widget.updateDOM(this.dom))&&(this.dom&&this.prevWidget&&this.prevWidget.destroy(this.dom),this.prevWidget=null,this.setDOM(this.widget.toDOM(this.editorView)),this.dom.contentEditable="false")}getSide(){return this.side}merge(e,t,i,s,r,o){return i&&(!(i instanceof Qe)||!this.widget.compare(i.widget)||e>0&&r<=0||t0?i.length-1:0;s=i[r],!(e>0?r==0:r==i.length-1||s.top0?-1:1);return e==0&&t>0||e==this.length&&t<=0?s:on(s,e==0)}get isEditable(){return!1}destroy(){super.destroy(),this.dom&&this.widget.destroy(this.dom)}}class dl extends Qe{domAtPos(e){let{topView:t,text:i}=this.widget;return t?ls(e,0,t,i,(s,r)=>s.domAtPos(r),s=>new le(i,Math.min(s,i.nodeValue.length))):new le(i,Math.min(e,i.nodeValue.length))}sync(){this.setDOM(this.widget.toDOM())}localPosFromDOM(e,t){let{topView:i,text:s}=this.widget;return i?pl(e,t,i,s):Math.min(t,this.length)}ignoreMutation(){return!1}get overrideDOMText(){return null}coordsAt(e,t){let{topView:i,text:s}=this.widget;return i?ls(e,t,i,s,(r,o,l)=>r.coordsAt(o,l),(r,o)=>os(s,r,o)):os(s,e,t)}destroy(){var e;super.destroy(),(e=this.widget.topView)===null||e===void 0||e.destroy()}get isEditable(){return!0}}function ls(n,e,t,i,s,r){if(t instanceof ze){for(let o of t.children){let l=Rt(o.dom,i),h=l?i.nodeValue.length:o.length;if(n0?-1:1);return i&&i.topt.top?{left:t.left,right:t.right,top:i.top,bottom:i.bottom}:t}get overrideDOMText(){return z.empty}}ot.prototype.children=Qe.prototype.children=Lt.prototype.children=Is;function ac(n,e){let t=n.parent,i=t?t.children.indexOf(n):-1;for(;t&&i>=0;)if(e<0?i>0:is&&t0;i--){let s=e[i-1].dom;if(s.parentNode==n)return le.after(s)}return new le(n,0)}function ml(n,e,t){let i,{children:s}=n;t>0&&e instanceof ze&&s.length&&(i=s[s.length-1])instanceof ze&&i.mark.eq(e.mark)?ml(i,e.children[0],t-1):(s.push(e),e.setParent(n)),n.length+=e.length}function yl(n,e,t){for(let r=0,o=0;o0?h>=e:h>e)&&(e0)){let c=0;if(h==r){if(l.getSide()<=0)continue;c=t=-l.getSide()}let f=l.coordsAt(Math.max(0,e-r),t);return c&&f?on(f,t<0):f}r=h}let i=n.dom.lastChild;if(!i)return n.dom.getBoundingClientRect();let s=ri(i);return s[s.length-1]||null}function hs(n,e){for(let t in n)t=="class"&&e.class?e.class+=" "+n.class:t=="style"&&e.style?e.style+=";"+n.style:e[t]=n[t];return e}function Ns(n,e){if(n==e)return!0;if(!n||!e)return!1;let t=Object.keys(n),i=Object.keys(e);if(t.length!=i.length)return!1;for(let s of t)if(i.indexOf(s)==-1||n[s]!==e[s])return!1;return!0}function as(n,e,t){let i=null;if(e)for(let s in e)t&&s in t||n.removeAttribute(i=s);if(t)for(let s in t)e&&e[s]==t[s]||n.setAttribute(i=s,t[s]);return!!i}class xt{eq(e){return!1}updateDOM(e){return!1}compare(e){return this==e||this.constructor==e.constructor&&this.eq(e)}get estimatedHeight(){return-1}ignoreEvent(e){return!0}get customView(){return null}destroy(e){}}var J=function(n){return n[n.Text=0]="Text",n[n.WidgetBefore=1]="WidgetBefore",n[n.WidgetAfter=2]="WidgetAfter",n[n.WidgetRange=3]="WidgetRange",n}(J||(J={}));class P extends pt{constructor(e,t,i,s){super(),this.startSide=e,this.endSide=t,this.widget=i,this.spec=s}get heightRelevant(){return!1}static mark(e){return new hn(e)}static widget(e){let t=e.side||0,i=!!e.block;return t+=i?t>0?3e8:-4e8:t>0?1e8:-1e8,new mt(e,t,t,i,e.widget||null,!1)}static replace(e){let t=!!e.block,i,s;if(e.isBlockGap)i=-5e8,s=4e8;else{let{start:r,end:o}=bl(e,t);i=(r?t?-3e8:-1:5e8)-1,s=(o?t?2e8:1:-6e8)+1}return new mt(e,i,s,t,e.widget||null,!0)}static line(e){return new di(e)}static set(e,t=!1){return Y.of(e,t)}hasHeight(){return this.widget?this.widget.estimatedHeight>-1:!1}}P.none=Y.empty;class hn extends P{constructor(e){let{start:t,end:i}=bl(e);super(t?-1:5e8,i?1:-6e8,null,e),this.tagName=e.tagName||"span",this.class=e.class||"",this.attrs=e.attributes||null}eq(e){return this==e||e instanceof hn&&this.tagName==e.tagName&&this.class==e.class&&Ns(this.attrs,e.attrs)}range(e,t=e){if(e>=t)throw new RangeError("Mark decorations may not be empty");return super.range(e,t)}}hn.prototype.point=!1;class di extends P{constructor(e){super(-2e8,-2e8,null,e)}eq(e){return e instanceof di&&Ns(this.spec.attributes,e.spec.attributes)}range(e,t=e){if(t!=e)throw new RangeError("Line decoration ranges must be zero-length");return super.range(e,t)}}di.prototype.mapMode=me.TrackBefore;di.prototype.point=!0;class mt extends P{constructor(e,t,i,s,r,o){super(t,i,r,e),this.block=s,this.isReplace=o,this.mapMode=s?t<=0?me.TrackBefore:me.TrackAfter:me.TrackDel}get type(){return this.startSide=5}eq(e){return e instanceof mt&&cc(this.widget,e.widget)&&this.block==e.block&&this.startSide==e.startSide&&this.endSide==e.endSide}range(e,t=e){if(this.isReplace&&(e>t||e==t&&this.startSide>0&&this.endSide<=0))throw new RangeError("Invalid range for replacement decoration");if(!this.isReplace&&t!=e)throw new RangeError("Widget decorations can only have zero-length ranges");return super.range(e,t)}}mt.prototype.point=!0;function bl(n,e=!1){let{inclusiveStart:t,inclusiveEnd:i}=n;return t==null&&(t=n.inclusive),i==null&&(i=n.inclusive),{start:t!=null?t:e,end:i!=null?i:e}}function cc(n,e){return n==e||!!(n&&e&&n.compare(e))}function cs(n,e,t,i=0){let s=t.length-1;s>=0&&t[s]+i>=n?t[s]=Math.max(t[s],e):t.push(n,e)}class fe extends ${constructor(){super(...arguments),this.children=[],this.length=0,this.prevAttrs=void 0,this.attrs=null,this.breakAfter=0}merge(e,t,i,s,r,o){if(i){if(!(i instanceof fe))return!1;this.dom||i.transferDOM(this)}return s&&this.setDeco(i?i.attrs:null),cl(this,e,t,i?i.children:[],r,o),!0}split(e){let t=new fe;if(t.breakAfter=this.breakAfter,this.length==0)return t;let{i,off:s}=this.childPos(e);s&&(t.append(this.children[i].split(s),0),this.children[i].merge(s,this.children[i].length,null,!1,0,0),i++);for(let r=i;r0&&this.children[i-1].length==0;)this.children[--i].destroy();return this.children.length=i,this.markDirty(),this.length=e,t}transferDOM(e){!this.dom||(this.markDirty(),e.setDOM(this.dom),e.prevAttrs=this.prevAttrs===void 0?this.attrs:this.prevAttrs,this.prevAttrs=void 0,this.dom=null)}setDeco(e){Ns(this.attrs,e)||(this.dom&&(this.prevAttrs=this.attrs,this.markDirty()),this.attrs=e)}append(e,t){ml(this,e,t)}addLineDeco(e){let t=e.spec.attributes,i=e.spec.class;t&&(this.attrs=hs(t,this.attrs||{})),i&&(this.attrs=hs({class:i},this.attrs||{}))}domAtPos(e){return gl(this.dom,this.children,e)}reuseDOM(e){e.nodeName=="DIV"&&(this.setDOM(e),this.dirty|=6)}sync(e){var t;this.dom?this.dirty&4&&(ll(this.dom),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0):(this.setDOM(document.createElement("div")),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0),this.prevAttrs!==void 0&&(as(this.dom,this.prevAttrs,this.attrs),this.dom.classList.add("cm-line"),this.prevAttrs=void 0),super.sync(e);let i=this.dom.lastChild;for(;i&&$.get(i)instanceof ze;)i=i.lastChild;if(!i||!this.length||i.nodeName!="BR"&&((t=$.get(i))===null||t===void 0?void 0:t.isEditable)==!1&&(!M.ios||!this.children.some(s=>s instanceof ot))){let s=document.createElement("BR");s.cmIgnore=!0,this.dom.appendChild(s)}}measureTextSize(){if(this.children.length==0||this.length>20)return null;let e=0;for(let t of this.children){if(!(t instanceof ot))return null;let i=ri(t.dom);if(i.length!=1)return null;e+=i[0].width}return{lineHeight:this.dom.getBoundingClientRect().height,charWidth:e/this.length}}coordsAt(e,t){return yl(this,e,t)}become(e){return!1}get type(){return J.Text}static find(e,t){for(let i=0,s=0;i=t){if(r instanceof fe)return r;if(o>t)break}s=o+r.breakAfter}return null}}class ut extends ${constructor(e,t,i){super(),this.widget=e,this.length=t,this.type=i,this.breakAfter=0,this.prevWidget=null}merge(e,t,i,s,r,o){return i&&(!(i instanceof ut)||!this.widget.compare(i.widget)||e>0&&r<=0||t0;){if(this.textOff==this.text.length){let{value:r,lineBreak:o,done:l}=this.cursor.next(this.skip);if(this.skip=0,l)throw new Error("Ran out of text content when drawing inline views");if(o){this.posCovered()||this.getLine(),this.content.length?this.content[this.content.length-1].breakAfter=1:this.breakAtStart=1,this.flushBuffer([]),this.curLine=null,e--;continue}else this.text=r,this.textOff=0}let s=Math.min(this.text.length-this.textOff,e,512);this.flushBuffer(t.slice(0,i)),this.getLine().append(wi(new ot(this.text.slice(this.textOff,this.textOff+s)),t),i),this.atCursorPos=!0,this.textOff+=s,e-=s,i=0}}span(e,t,i,s){this.buildText(t-e,i,s),this.pos=t,this.openStart<0&&(this.openStart=s)}point(e,t,i,s,r,o){if(this.disallowBlockEffectsFor[o]&&i instanceof mt){if(i.block)throw new RangeError("Block decorations may not be specified via plugins");if(t>this.doc.lineAt(this.pos).to)throw new RangeError("Decorations that replace line breaks may not be specified via plugins")}let l=t-e;if(i instanceof mt)if(i.block){let{type:h}=i;h==J.WidgetAfter&&!this.posCovered()&&this.getLine(),this.addBlockWidget(new ut(i.widget||new Mr("div"),l,h))}else{let h=Qe.create(i.widget||new Mr("span"),l,i.startSide),a=this.atCursorPos&&!h.isEditable&&r<=s.length&&(e0),c=!h.isEditable&&(en.some(e=>e)});class qi{constructor(e,t="nearest",i="nearest",s=5,r=5){this.range=e,this.y=t,this.x=i,this.yMargin=s,this.xMargin=r}map(e){return e.empty?this:new qi(this.range.map(e),this.y,this.x,this.yMargin,this.xMargin)}}const Dr=H.define({map:(n,e)=>n.map(e)});function Pe(n,e,t){let i=n.facet(Sl);i.length?i[0](e):window.onerror?window.onerror(String(e),t,void 0,void 0,e):t?console.error(t+":",e):console.error(e)}const an=T.define({combine:n=>n.length?n[0]:!0});let fc=0;const Gt=T.define();class ue{constructor(e,t,i,s){this.id=e,this.create=t,this.domEventHandlers=i,this.extension=s(this)}static define(e,t){const{eventHandlers:i,provide:s,decorations:r}=t||{};return new ue(fc++,e,i,o=>{let l=[Gt.of(o)];return r&&l.push(li.of(h=>{let a=h.plugin(o);return a?r(a):P.none})),s&&l.push(s(o)),l})}static fromClass(e,t){return ue.define(i=>new e(i),t)}}class An{constructor(e){this.spec=e,this.mustUpdate=null,this.value=null}update(e){if(this.value){if(this.mustUpdate){let t=this.mustUpdate;if(this.mustUpdate=null,this.value.update)try{this.value.update(t)}catch(i){if(Pe(t.state,i,"CodeMirror plugin crashed"),this.value.destroy)try{this.value.destroy()}catch{}this.deactivate()}}}else if(this.spec)try{this.value=this.spec.create(e)}catch(t){Pe(e.state,t,"CodeMirror plugin crashed"),this.deactivate()}return this}destroy(e){var t;if(!((t=this.value)===null||t===void 0)&&t.destroy)try{this.value.destroy()}catch(i){Pe(e.state,i,"CodeMirror plugin crashed")}}deactivate(){this.spec=this.value=null}}const Al=T.define(),Ml=T.define(),li=T.define(),Dl=T.define(),Ol=T.define(),Jt=T.define();class Ue{constructor(e,t,i,s){this.fromA=e,this.toA=t,this.fromB=i,this.toB=s}join(e){return new Ue(Math.min(this.fromA,e.fromA),Math.max(this.toA,e.toA),Math.min(this.fromB,e.fromB),Math.max(this.toB,e.toB))}addToSet(e){let t=e.length,i=this;for(;t>0;t--){let s=e[t-1];if(!(s.fromA>i.toA)){if(s.toAc)break;r+=2}if(!h)return i;new Ue(h.fromA,h.toA,h.fromB,h.toB).addToSet(i),o=h.toA,l=h.toB}}}class Ki{constructor(e,t,i){this.view=e,this.state=t,this.transactions=i,this.flags=0,this.startState=e.state,this.changes=ee.empty(this.startState.doc.length);for(let o of i)this.changes=this.changes.compose(o.changes);let s=[];this.changes.iterChangedRanges((o,l,h,a)=>s.push(new Ue(o,l,h,a))),this.changedRanges=s;let r=e.hasFocus;r!=e.inputState.notifiedFocused&&(e.inputState.notifiedFocused=r,this.flags|=1)}static create(e,t,i){return new Ki(e,t,i)}get viewportChanged(){return(this.flags&4)>0}get heightChanged(){return(this.flags&2)>0}get geometryChanged(){return this.docChanged||(this.flags&10)>0}get focusChanged(){return(this.flags&1)>0}get docChanged(){return!this.changes.empty}get selectionSet(){return this.transactions.some(e=>e.selection)}get empty(){return this.flags==0&&this.transactions.length==0}}var Z=function(n){return n[n.LTR=0]="LTR",n[n.RTL=1]="RTL",n}(Z||(Z={}));const us=Z.LTR,uc=Z.RTL;function Tl(n){let e=[];for(let t=0;t=t){if(l.level==i)return o;(r<0||(s!=0?s<0?l.fromt:e[r].level>l.level))&&(r=o)}}if(r<0)throw new RangeError("Index out of range");return r}}const X=[];function yc(n,e){let t=n.length,i=e==us?1:2,s=e==us?2:1;if(!n||i==1&&!mc.test(n))return Bl(t);for(let o=0,l=i,h=i;o=0;u-=3)if(Re[u+1]==-c){let d=Re[u+2],p=d&2?i:d&4?d&1?s:i:0;p&&(X[o]=X[Re[u]]=p),l=u;break}}else{if(Re.length==189)break;Re[l++]=o,Re[l++]=a,Re[l++]=h}else if((f=X[o])==2||f==1){let u=f==i;h=u?0:1;for(let d=l-3;d>=0;d-=3){let p=Re[d+2];if(p&2)break;if(u)Re[d+2]|=2;else{if(p&4)break;Re[d+2]|=4}}}for(let o=0;ol;){let c=a,f=X[--a]!=2;for(;a>l&&f==(X[a-1]!=2);)a--;r.push(new Tt(a,c,f?2:1))}else r.push(new Tt(l,o,0))}else for(let o=0;o1)for(let h of this.points)h.node==e&&h.pos>this.text.length&&(h.pos-=o-1);i=r+o}}readNode(e){if(e.cmIgnore)return;let t=$.get(e),i=t&&t.overrideDOMText;if(i!=null){this.findPointInside(e,i.length);for(let s=i.iter();!s.next().done;)s.lineBreak?this.lineBreak():this.append(s.value)}else e.nodeType==3?this.readTextNode(e):e.nodeName=="BR"?e.nextSibling&&this.lineBreak():e.nodeType==1&&this.readRange(e.firstChild,null)}findPointBefore(e,t){for(let i of this.points)i.node==e&&e.childNodes[i.offset]==t&&(i.pos=this.text.length)}findPointInside(e,t){for(let i of this.points)(e.nodeType==3?i.node==e:e.contains(i.node))&&(i.pos=this.text.length+Math.min(t,i.offset))}}function Or(n){return n.nodeType==1&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(n.nodeName)}class Tr{constructor(e,t){this.node=e,this.offset=t,this.pos=-1}}class Br extends ${constructor(e){super(),this.view=e,this.compositionDeco=P.none,this.decorations=[],this.dynamicDecorationMap=[],this.minWidth=0,this.minWidthFrom=0,this.minWidthTo=0,this.impreciseAnchor=null,this.impreciseHead=null,this.forceSelection=!1,this.lastUpdate=Date.now(),this.setDOM(e.contentDOM),this.children=[new fe],this.children[0].setParent(this),this.updateDeco(),this.updateInner([new Ue(0,0,0,e.state.doc.length)],0)}get root(){return this.view.root}get editorView(){return this.view}get length(){return this.view.state.doc.length}update(e){let t=e.changedRanges;this.minWidth>0&&t.length&&(t.every(({fromA:o,toA:l})=>lthis.minWidthTo)?(this.minWidthFrom=e.changes.mapPos(this.minWidthFrom,1),this.minWidthTo=e.changes.mapPos(this.minWidthTo,1)):this.minWidth=this.minWidthFrom=this.minWidthTo=0),this.view.inputState.composing<0?this.compositionDeco=P.none:(e.transactions.length||this.dirty)&&(this.compositionDeco=xc(this.view,e.changes)),(M.ie||M.chrome)&&!this.compositionDeco.size&&e&&e.state.doc.lines!=e.startState.doc.lines&&(this.forceSelection=!0);let i=this.decorations,s=this.updateDeco(),r=Cc(i,s,e.changes);return t=Ue.extendWithRanges(t,r),this.dirty==0&&t.length==0?!1:(this.updateInner(t,e.startState.doc.length),e.transactions.length&&(this.lastUpdate=Date.now()),!0)}updateInner(e,t){this.view.viewState.mustMeasureContent=!0,this.updateChildren(e,t);let{observer:i}=this.view;i.ignore(()=>{this.dom.style.height=this.view.viewState.contentHeight+"px",this.dom.style.flexBasis=this.minWidth?this.minWidth+"px":"";let r=M.chrome||M.ios?{node:i.selectionRange.focusNode,written:!1}:void 0;this.sync(r),this.dirty=0,r&&(r.written||i.selectionRange.focusNode!=r.node)&&(this.forceSelection=!0),this.dom.style.height=""});let s=[];if(this.view.viewport.from||this.view.viewport.to=0?e[s]:null;if(!r)break;let{fromA:o,toA:l,fromB:h,toB:a}=r,{content:c,breakAtStart:f,openStart:u,openEnd:d}=Vs.build(this.view.state.doc,h,a,this.decorations,this.dynamicDecorationMap),{i:p,off:g}=i.findPos(l,1),{i:y,off:b}=i.findPos(o,-1);al(this,y,b,p,g,c,f,u,d)}}updateSelection(e=!1,t=!1){if((e||!this.view.observer.selectionRange.focusNode)&&this.view.observer.readSelectionRange(),!(t||this.mayControlSelection())||M.ios&&this.view.inputState.rapidCompositionStart)return;let i=this.forceSelection;this.forceSelection=!1;let s=this.view.state.selection.main,r=this.domAtPos(s.anchor),o=s.empty?r:this.domAtPos(s.head);if(M.gecko&&s.empty&&wc(r)){let h=document.createTextNode("");this.view.observer.ignore(()=>r.node.insertBefore(h,r.node.childNodes[r.offset]||null)),r=o=new le(h,0),i=!0}let l=this.view.observer.selectionRange;(i||!l.focusNode||!Wi(r.node,r.offset,l.anchorNode,l.anchorOffset)||!Wi(o.node,o.offset,l.focusNode,l.focusOffset))&&(this.view.observer.ignore(()=>{M.android&&M.chrome&&this.dom.contains(l.focusNode)&&Ac(l.focusNode,this.dom)&&(this.dom.blur(),this.dom.focus({preventScroll:!0}));let h=Hi(this.root);if(h)if(s.empty){if(M.gecko){let a=Sc(r.node,r.offset);if(a&&a!=3){let c=El(r.node,r.offset,a==1?1:-1);c&&(r=new le(c,a==1?0:c.nodeValue.length))}}h.collapse(r.node,r.offset),s.bidiLevel!=null&&l.cursorBidiLevel!=null&&(l.cursorBidiLevel=s.bidiLevel)}else if(h.extend)h.collapse(r.node,r.offset),h.extend(o.node,o.offset);else{let a=document.createRange();s.anchor>s.head&&([r,o]=[o,r]),a.setEnd(o.node,o.offset),a.setStart(r.node,r.offset),h.removeAllRanges(),h.addRange(a)}}),this.view.observer.setSelectionRange(r,o)),this.impreciseAnchor=r.precise?null:new le(l.anchorNode,l.anchorOffset),this.impreciseHead=o.precise?null:new le(l.focusNode,l.focusOffset)}enforceCursorAssoc(){if(this.compositionDeco.size)return;let e=this.view.state.selection.main,t=Hi(this.root);if(!t||!e.empty||!e.assoc||!t.modify)return;let i=fe.find(this,e.head);if(!i)return;let s=i.posAtStart;if(e.head==s||e.head==s+i.length)return;let r=this.coordsAt(e.head,-1),o=this.coordsAt(e.head,1);if(!r||!o||r.bottom>o.top)return;let l=this.domAtPos(e.head+e.assoc);t.collapse(l.node,l.offset),t.modify("move",e.assoc<0?"forward":"backward","lineboundary")}mayControlSelection(){let e=this.root.activeElement;return e==this.dom||ts(this.dom,this.view.observer.selectionRange)&&!(e&&this.dom.contains(e))}nearest(e){for(let t=e;t;){let i=$.get(t);if(i&&i.rootView==this)return i;t=t.parentNode}return null}posFromDOM(e,t){let i=this.nearest(e);if(!i)throw new RangeError("Trying to find position for a DOM position outside of the document");return i.localPosFromDOM(e,t)+i.posAtStart}domAtPos(e){let{i:t,off:i}=this.childCursor().findPos(e,-1);for(;to||e==o&&r.type!=J.WidgetBefore&&r.type!=J.WidgetAfter&&(!s||t==2||this.children[s-1].breakAfter||this.children[s-1].type==J.WidgetBefore&&t>-2))return r.coordsAt(e-o,t);i=o}}measureVisibleLineHeights(e){let t=[],{from:i,to:s}=e,r=this.view.contentDOM.clientWidth,o=r>Math.max(this.view.scrollDOM.clientWidth,this.minWidth)+1,l=-1,h=this.view.textDirection==Z.LTR;for(let a=0,c=0;cs)break;if(a>=i){let d=f.dom.getBoundingClientRect();if(t.push(d.height),o){let p=f.dom.lastChild,g=p?ri(p):[];if(g.length){let y=g[g.length-1],b=h?y.right-d.left:d.right-y.left;b>l&&(l=b,this.minWidth=r,this.minWidthFrom=a,this.minWidthTo=u)}}}a=u+f.breakAfter}return t}textDirectionAt(e){let{i:t}=this.childPos(e,1);return getComputedStyle(this.children[t].dom).direction=="rtl"?Z.RTL:Z.LTR}measureTextSize(){for(let s of this.children)if(s instanceof fe){let r=s.measureTextSize();if(r)return r}let e=document.createElement("div"),t,i;return e.className="cm-line",e.style.width="99999px",e.textContent="abc def ghi jkl mno pqr stu",this.view.observer.ignore(()=>{this.dom.appendChild(e);let s=ri(e.firstChild)[0];t=e.getBoundingClientRect().height,i=s?s.width/27:7,e.remove()}),{lineHeight:t,charWidth:i}}childCursor(e=this.length){let t=this.children.length;return t&&(e-=this.children[--t].length),new hl(this.children,e,t)}computeBlockGapDeco(){let e=[],t=this.view.viewState;for(let i=0,s=0;;s++){let r=s==t.viewports.length?null:t.viewports[s],o=r?r.from-1:this.length;if(o>i){let l=t.lineBlockAt(o).bottom-t.lineBlockAt(i).top;e.push(P.replace({widget:new Pr(l),block:!0,inclusive:!0,isBlockGap:!0}).range(i,o))}if(!r)break;i=r.to+1}return P.set(e)}updateDeco(){let e=this.view.state.facet(li).map((t,i)=>(this.dynamicDecorationMap[i]=typeof t=="function")?t(this.view):t);for(let t=e.length;tt.anchor?-1:1),s;if(!i)return;!t.empty&&(s=this.coordsAt(t.anchor,t.anchor>t.head?-1:1))&&(i={left:Math.min(i.left,s.left),top:Math.min(i.top,s.top),right:Math.max(i.right,s.right),bottom:Math.max(i.bottom,s.bottom)});let r=0,o=0,l=0,h=0;for(let c of this.view.state.facet(Ol).map(f=>f(this.view)))if(c){let{left:f,right:u,top:d,bottom:p}=c;f!=null&&(r=Math.max(r,f)),u!=null&&(o=Math.max(o,u)),d!=null&&(l=Math.max(l,d)),p!=null&&(h=Math.max(h,p))}let a={left:i.left-r,top:i.top-l,right:i.right+o,bottom:i.bottom+h};rc(this.view.scrollDOM,a,t.head0&&t<=0)n=n.childNodes[e-1],e=zi(n);else if(n.nodeType==1&&e=0)n=n.childNodes[e],e=0;else return null}}function Sc(n,e){return n.nodeType!=1?0:(e&&n.childNodes[e-1].contentEditable=="false"?1:0)|(e0;){let a=Ae(s.text,o,!1);if(i(s.text.slice(a,o))!=h)break;o=a}for(;ln?e.left-n:Math.max(0,n-e.right)}function Oc(n,e){return e.top>n?e.top-n:Math.max(0,n-e.bottom)}function Mn(n,e){return n.tope.top+1}function Rr(n,e){return en.bottom?{top:n.top,left:n.left,right:n.right,bottom:e}:n}function ps(n,e,t){let i,s,r,o,l,h,a,c;for(let d=n.firstChild;d;d=d.nextSibling){let p=ri(d);for(let g=0;gv||o==v&&r>b)&&(i=d,s=y,r=b,o=v),b==0?t>y.bottom&&(!a||a.bottomy.top)&&(h=d,c=y):a&&Mn(a,y)?a=Lr(a,y.bottom):c&&Mn(c,y)&&(c=Rr(c,y.top))}}if(a&&a.bottom>=t?(i=l,s=a):c&&c.top<=t&&(i=h,s=c),!i)return{node:n,offset:0};let f=Math.max(s.left,Math.min(s.right,e));if(i.nodeType==3)return Er(i,f,t);if(!r&&i.contentEditable=="true")return ps(i,f,t);let u=Array.prototype.indexOf.call(n.childNodes,i)+(e>=(s.left+s.right)/2?1:0);return{node:n,offset:u}}function Er(n,e,t){let i=n.nodeValue.length,s=-1,r=1e9,o=0;for(let l=0;lt?c.top-t:t-c.bottom)-1;if(c.left-1<=e&&c.right+1>=e&&f=(c.left+c.right)/2,d=u;if((M.chrome||M.gecko)&&oi(n,l).getBoundingClientRect().left==c.right&&(d=!u),f<=0)return{node:n,offset:l+(d?1:0)};s=l+(d?1:0),r=f}}}return{node:n,offset:s>-1?s:o>0?n.nodeValue.length:0}}function Il(n,{x:e,y:t},i,s=-1){var r;let o=n.contentDOM.getBoundingClientRect(),l=o.top+n.viewState.paddingTop,h,{docHeight:a}=n.viewState,c=t-l;if(c<0)return 0;if(c>a)return n.state.doc.length;for(let b=n.defaultLineHeight/2,v=!1;h=n.elementAtHeight(c),h.type!=J.Text;)for(;c=s>0?h.bottom+b:h.top-b,!(c>=0&&c<=a);){if(v)return i?null:0;v=!0,s=-s}t=l+c;let f=h.from;if(fn.viewport.to)return n.viewport.to==n.state.doc.length?n.state.doc.length:i?null:Ir(n,o,h,e,t);let u=n.dom.ownerDocument,d=n.root.elementFromPoint?n.root:u,p=d.elementFromPoint(e,t);p&&!n.contentDOM.contains(p)&&(p=null),p||(e=Math.max(o.left+1,Math.min(o.right-1,e)),p=d.elementFromPoint(e,t),p&&!n.contentDOM.contains(p)&&(p=null));let g,y=-1;if(p&&((r=n.docView.nearest(p))===null||r===void 0?void 0:r.isEditable)!=!1){if(u.caretPositionFromPoint){let b=u.caretPositionFromPoint(e,t);b&&({offsetNode:g,offset:y}=b)}else if(u.caretRangeFromPoint){let b=u.caretRangeFromPoint(e,t);b&&({startContainer:g,startOffset:y}=b,M.safari&&Tc(g,y,e)&&(g=void 0))}}if(!g||!n.docView.dom.contains(g)){let b=fe.find(n.docView,f);if(!b)return c>h.top+h.height/2?h.to:h.from;({node:g,offset:y}=ps(b.dom,e,t))}return n.docView.posFromDOM(g,y)}function Ir(n,e,t,i,s){let r=Math.round((i-e.left)*n.defaultCharacterWidth);n.lineWrapping&&t.height>n.defaultLineHeight*1.5&&(r+=Math.floor((s-t.top)/n.defaultLineHeight)*n.viewState.heightOracle.lineLength);let o=n.state.sliceDoc(t.from,t.to);return t.from+Qn(o,r,n.state.tabSize)}function Tc(n,e,t){let i;if(n.nodeType!=3||e!=(i=n.nodeValue.length))return!1;for(let s=n.nextSibling;s;s=s.nextSibling)if(s.nodeType!=1||s.nodeName!="BR")return!1;return oi(n,i-1,i).getBoundingClientRect().left>t}function Bc(n,e,t,i){let s=n.state.doc.lineAt(e.head),r=!i||!n.lineWrapping?null:n.coordsAtPos(e.assoc<0&&e.head>s.from?e.head-1:e.head);if(r){let h=n.dom.getBoundingClientRect(),a=n.textDirectionAt(s.from),c=n.posAtCoords({x:t==(a==Z.LTR)?h.right-1:h.left+1,y:(r.top+r.bottom)/2});if(c!=null)return m.cursor(c,t?-1:1)}let o=fe.find(n.docView,e.head),l=o?t?o.posAtEnd:o.posAtStart:t?s.to:s.from;return m.cursor(l,t?-1:1)}function Nr(n,e,t,i){let s=n.state.doc.lineAt(e.head),r=n.bidiSpans(s),o=n.textDirectionAt(s.from);for(let l=e,h=null;;){let a=bc(s,r,o,l,t),c=Pl;if(!a){if(s.number==(t?n.state.doc.lines:1))return l;c=` +`,s=n.state.doc.line(s.number+(t?1:-1)),r=n.bidiSpans(s),a=m.cursor(t?s.from:s.to)}if(h){if(!h(c))return l}else{if(!i)return a;h=i(c)}l=a}}function Pc(n,e,t){let i=n.state.charCategorizer(e),s=i(t);return r=>{let o=i(r);return s==ce.Space&&(s=o),s==o}}function Rc(n,e,t,i){let s=e.head,r=t?1:-1;if(s==(t?n.state.doc.length:0))return m.cursor(s,e.assoc);let o=e.goalColumn,l,h=n.contentDOM.getBoundingClientRect(),a=n.coordsAtPos(s),c=n.documentTop;if(a)o==null&&(o=a.left-h.left),l=r<0?a.top:a.bottom;else{let d=n.viewState.lineBlockAt(s);o==null&&(o=Math.min(h.right-h.left,n.defaultCharacterWidth*(s-d.from))),l=(r<0?d.top:d.bottom)+c}let f=h.left+o,u=i!=null?i:n.defaultLineHeight>>1;for(let d=0;;d+=10){let p=l+(u+d)*r,g=Il(n,{x:f,y:p},!1,r);if(ph.bottom||(r<0?gs))return m.cursor(g,e.assoc,void 0,o)}}function Dn(n,e,t){let i=n.state.facet(Dl).map(s=>s(n));for(;;){let s=!1;for(let r of i)r.between(t.from-1,t.from+1,(o,l,h)=>{t.from>o&&t.fromt.from?m.cursor(o,1):m.cursor(l,-1),s=!0)});if(!s)return t}}class Lc{constructor(e){this.lastKeyCode=0,this.lastKeyTime=0,this.chromeScrollHack=-1,this.pendingIOSKey=void 0,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastEscPress=0,this.lastContextMenu=0,this.scrollHandlers=[],this.registeredEvents=[],this.customHandlers=[],this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.rapidCompositionStart=!1,this.mouseSelection=null;for(let t in ie){let i=ie[t];e.contentDOM.addEventListener(t,s=>{!Vr(e,s)||this.ignoreDuringComposition(s)||t=="keydown"&&this.keydown(e,s)||(this.mustFlushObserver(s)&&e.observer.forceFlush(),this.runCustomHandlers(t,e,s)?s.preventDefault():i(e,s))}),this.registeredEvents.push(t)}M.chrome&&M.chrome_version>=102&&e.scrollDOM.addEventListener("wheel",()=>{this.chromeScrollHack<0?e.contentDOM.style.pointerEvents="none":window.clearTimeout(this.chromeScrollHack),this.chromeScrollHack=setTimeout(()=>{this.chromeScrollHack=-1,e.contentDOM.style.pointerEvents=""},100)},{passive:!0}),this.notifiedFocused=e.hasFocus,M.safari&&e.contentDOM.addEventListener("input",()=>null)}setSelectionOrigin(e){this.lastSelectionOrigin=e,this.lastSelectionTime=Date.now()}ensureHandlers(e,t){var i;let s;this.customHandlers=[];for(let r of t)if(s=(i=r.update(e).spec)===null||i===void 0?void 0:i.domEventHandlers){this.customHandlers.push({plugin:r.value,handlers:s});for(let o in s)this.registeredEvents.indexOf(o)<0&&o!="scroll"&&(this.registeredEvents.push(o),e.contentDOM.addEventListener(o,l=>{!Vr(e,l)||this.runCustomHandlers(o,e,l)&&l.preventDefault()}))}}runCustomHandlers(e,t,i){for(let s of this.customHandlers){let r=s.handlers[e];if(r)try{if(r.call(s.plugin,i,t)||i.defaultPrevented)return!0}catch(o){Pe(t.state,o)}}return!1}runScrollHandlers(e,t){for(let i of this.customHandlers){let s=i.handlers.scroll;if(s)try{s.call(i.plugin,t,e)}catch(r){Pe(e.state,r)}}}keydown(e,t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),t.keyCode==9&&Date.now()s.keyCode==t.keyCode))&&!(t.ctrlKey||t.altKey||t.metaKey)&&!t.synthetic?(this.pendingIOSKey=i,setTimeout(()=>this.flushIOSKey(e),250),!0):!1}flushIOSKey(e){let t=this.pendingIOSKey;return t?(this.pendingIOSKey=void 0,Zt(e.contentDOM,t.key,t.keyCode)):!1}ignoreDuringComposition(e){return/^key/.test(e.type)?this.composing>0?!0:M.safari&&Date.now()-this.compositionEndedAt<100?(this.compositionEndedAt=0,!0):!1:!1}mustFlushObserver(e){return e.type=="keydown"&&e.keyCode!=229||e.type=="compositionend"&&!M.ios}startMouseSelection(e){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=e}update(e){this.mouseSelection&&this.mouseSelection.update(e),e.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}const Nl=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],Vl=[16,17,18,20,91,92,224,225];class Ec{constructor(e,t,i,s){this.view=e,this.style=i,this.mustSelect=s,this.lastEvent=t;let r=e.contentDOM.ownerDocument;r.addEventListener("mousemove",this.move=this.move.bind(this)),r.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=t.shiftKey,this.multiple=e.state.facet(V.allowMultipleSelections)&&Ic(e,t),this.dragMove=Nc(e,t),this.dragging=Vc(e,t)&&Fs(t)==1?null:!1,this.dragging===!1&&(t.preventDefault(),this.select(t))}move(e){if(e.buttons==0)return this.destroy();this.dragging===!1&&this.select(this.lastEvent=e)}up(e){this.dragging==null&&this.select(this.lastEvent),this.dragging||e.preventDefault(),this.destroy()}destroy(){let e=this.view.contentDOM.ownerDocument;e.removeEventListener("mousemove",this.move),e.removeEventListener("mouseup",this.up),this.view.inputState.mouseSelection=null}select(e){let t=this.style.get(e,this.extend,this.multiple);(this.mustSelect||!t.eq(this.view.state.selection)||t.main.assoc!=this.view.state.selection.main.assoc)&&this.view.dispatch({selection:t,userEvent:"select.pointer",scrollIntoView:!0}),this.mustSelect=!1}update(e){e.docChanged&&this.dragging&&(this.dragging=this.dragging.map(e.changes)),this.style.update(e)&&setTimeout(()=>this.select(this.lastEvent),20)}}function Ic(n,e){let t=n.state.facet(wl);return t.length?t[0](e):M.mac?e.metaKey:e.ctrlKey}function Nc(n,e){let t=n.state.facet(xl);return t.length?t[0](e):M.mac?!e.altKey:!e.ctrlKey}function Vc(n,e){let{main:t}=n.state.selection;if(t.empty)return!1;let i=Hi(n.root);if(!i||i.rangeCount==0)return!0;let s=i.getRangeAt(0).getClientRects();for(let r=0;r=e.clientX&&o.top<=e.clientY&&o.bottom>=e.clientY)return!0}return!1}function Vr(n,e){if(!e.bubbles)return!0;if(e.defaultPrevented)return!1;for(let t=e.target,i;t!=n.contentDOM;t=t.parentNode)if(!t||t.nodeType==11||(i=$.get(t))&&i.ignoreEvent(e))return!1;return!0}const ie=Object.create(null),Fl=M.ie&&M.ie_version<15||M.ios&&M.webkit_version<604;function Fc(n){let e=n.dom.parentNode;if(!e)return;let t=e.appendChild(document.createElement("textarea"));t.style.cssText="position: fixed; left: -10000px; top: 10px",t.focus(),setTimeout(()=>{n.focus(),t.remove(),Hl(n,t.value)},50)}function Hl(n,e){let{state:t}=n,i,s=1,r=t.toText(e),o=r.lines==t.selection.ranges.length;if(gs!=null&&t.selection.ranges.every(h=>h.empty)&&gs==r.toString()){let h=-1;i=t.changeByRange(a=>{let c=t.doc.lineAt(a.from);if(c.from==h)return{range:a};h=c.from;let f=t.toText((o?r.line(s++).text:e)+t.lineBreak);return{changes:{from:c.from,insert:f},range:m.cursor(a.from+f.length)}})}else o?i=t.changeByRange(h=>{let a=r.line(s++);return{changes:{from:h.from,to:h.to,insert:a.text},range:m.cursor(h.from+a.length)}}):i=t.replaceSelection(r);n.dispatch(i,{userEvent:"input.paste",scrollIntoView:!0})}ie.keydown=(n,e)=>{n.inputState.setSelectionOrigin("select"),e.keyCode==27?n.inputState.lastEscPress=Date.now():Vl.indexOf(e.keyCode)<0&&(n.inputState.lastEscPress=0)};let Wl=0;ie.touchstart=(n,e)=>{Wl=Date.now(),n.inputState.setSelectionOrigin("select.pointer")};ie.touchmove=n=>{n.inputState.setSelectionOrigin("select.pointer")};ie.mousedown=(n,e)=>{if(n.observer.flush(),Wl>Date.now()-2e3&&Fs(e)==1)return;let t=null;for(let i of n.state.facet(kl))if(t=i(n,e),t)break;if(!t&&e.button==0&&(t=zc(n,e)),t){let i=n.root.activeElement!=n.contentDOM;i&&n.observer.ignore(()=>ol(n.contentDOM)),n.inputState.startMouseSelection(new Ec(n,e,t,i))}};function Fr(n,e,t,i){if(i==1)return m.cursor(e,t);if(i==2)return Mc(n.state,e,t);{let s=fe.find(n.docView,e),r=n.state.doc.lineAt(s?s.posAtEnd:e),o=s?s.posAtStart:r.from,l=s?s.posAtEnd:r.to;return ln>=e.top&&n<=e.bottom,Hr=(n,e,t)=>zl(e,t)&&n>=t.left&&n<=t.right;function Hc(n,e,t,i){let s=fe.find(n.docView,e);if(!s)return 1;let r=e-s.posAtStart;if(r==0)return 1;if(r==s.length)return-1;let o=s.coordsAt(r,-1);if(o&&Hr(t,i,o))return-1;let l=s.coordsAt(r,1);return l&&Hr(t,i,l)?1:o&&zl(i,o)?-1:1}function Wr(n,e){let t=n.posAtCoords({x:e.clientX,y:e.clientY},!1);return{pos:t,bias:Hc(n,t,e.clientX,e.clientY)}}const Wc=M.ie&&M.ie_version<=11;let zr=null,qr=0,Kr=0;function Fs(n){if(!Wc)return n.detail;let e=zr,t=Kr;return zr=n,Kr=Date.now(),qr=!e||t>Date.now()-400&&Math.abs(e.clientX-n.clientX)<2&&Math.abs(e.clientY-n.clientY)<2?(qr+1)%3:1}function zc(n,e){let t=Wr(n,e),i=Fs(e),s=n.state.selection,r=t,o=e;return{update(l){l.docChanged&&(t&&(t.pos=l.changes.mapPos(t.pos)),s=s.map(l.changes),o=null)},get(l,h,a){let c;if(o&&l.clientX==o.clientX&&l.clientY==o.clientY?c=r:(c=r=Wr(n,l),o=l),!c||!t)return s;let f=Fr(n,c.pos,c.bias,i);if(t.pos!=c.pos&&!h){let u=Fr(n,t.pos,t.bias,i),d=Math.min(u.from,f.from),p=Math.max(u.to,f.to);f=d{let{selection:{main:t}}=n.state,{mouseSelection:i}=n.inputState;i&&(i.dragging=t),e.dataTransfer&&(e.dataTransfer.setData("Text",n.state.sliceDoc(t.from,t.to)),e.dataTransfer.effectAllowed="copyMove")};function Ur(n,e,t,i){if(!t)return;let s=n.posAtCoords({x:e.clientX,y:e.clientY},!1);e.preventDefault();let{mouseSelection:r}=n.inputState,o=i&&r&&r.dragging&&r.dragMove?{from:r.dragging.from,to:r.dragging.to}:null,l={from:s,insert:t},h=n.state.changes(o?[o,l]:l);n.focus(),n.dispatch({changes:h,selection:{anchor:h.mapPos(s,-1),head:h.mapPos(s,1)},userEvent:o?"move.drop":"input.drop"})}ie.drop=(n,e)=>{if(!e.dataTransfer)return;if(n.state.readOnly)return e.preventDefault();let t=e.dataTransfer.files;if(t&&t.length){e.preventDefault();let i=Array(t.length),s=0,r=()=>{++s==t.length&&Ur(n,e,i.filter(o=>o!=null).join(n.state.lineBreak),!1)};for(let o=0;o{/[\x00-\x08\x0e-\x1f]{2}/.test(l.result)||(i[o]=l.result),r()},l.readAsText(t[o])}}else Ur(n,e,e.dataTransfer.getData("Text"),!0)};ie.paste=(n,e)=>{if(n.state.readOnly)return e.preventDefault();n.observer.flush();let t=Fl?null:e.clipboardData;t?(Hl(n,t.getData("text/plain")),e.preventDefault()):Fc(n)};function qc(n,e){let t=n.dom.parentNode;if(!t)return;let i=t.appendChild(document.createElement("textarea"));i.style.cssText="position: fixed; left: -10000px; top: 10px",i.value=e,i.focus(),i.selectionEnd=e.length,i.selectionStart=0,setTimeout(()=>{i.remove(),n.focus()},50)}function Kc(n){let e=[],t=[],i=!1;for(let s of n.selection.ranges)s.empty||(e.push(n.sliceDoc(s.from,s.to)),t.push(s));if(!e.length){let s=-1;for(let{from:r}of n.selection.ranges){let o=n.doc.lineAt(r);o.number>s&&(e.push(o.text),t.push({from:o.from,to:Math.min(n.doc.length,o.to+1)})),s=o.number}i=!0}return{text:e.join(n.lineBreak),ranges:t,linewise:i}}let gs=null;ie.copy=ie.cut=(n,e)=>{let{text:t,ranges:i,linewise:s}=Kc(n.state);if(!t&&!s)return;gs=s?t:null;let r=Fl?null:e.clipboardData;r?(e.preventDefault(),r.clearData(),r.setData("text/plain",t)):qc(n,t),e.type=="cut"&&!n.state.readOnly&&n.dispatch({changes:i,scrollIntoView:!0,userEvent:"delete.cut"})};function ql(n){setTimeout(()=>{n.hasFocus!=n.inputState.notifiedFocused&&n.update([])},10)}ie.focus=ql;ie.blur=n=>{n.observer.clearSelectionRange(),ql(n)};function Kl(n,e){if(n.docView.compositionDeco.size){n.inputState.rapidCompositionStart=e;try{n.update([])}finally{n.inputState.rapidCompositionStart=!1}}}ie.compositionstart=ie.compositionupdate=n=>{n.inputState.compositionFirstChange==null&&(n.inputState.compositionFirstChange=!0),n.inputState.composing<0&&(n.inputState.composing=0,n.docView.compositionDeco.size&&(n.observer.flush(),Kl(n,!0)))};ie.compositionend=n=>{n.inputState.composing=-1,n.inputState.compositionEndedAt=Date.now(),n.inputState.compositionFirstChange=null,setTimeout(()=>{n.inputState.composing<0&&Kl(n,!1)},50)};ie.contextmenu=n=>{n.inputState.lastContextMenu=Date.now()};ie.beforeinput=(n,e)=>{var t;let i;if(M.chrome&&M.android&&(i=Nl.find(s=>s.inputType==e.inputType))&&(n.observer.delayAndroidKey(i.key,i.keyCode),i.key=="Backspace"||i.key=="Delete")){let s=((t=window.visualViewport)===null||t===void 0?void 0:t.height)||0;setTimeout(()=>{var r;(((r=window.visualViewport)===null||r===void 0?void 0:r.height)||0)>s+10&&n.hasFocus&&(n.contentDOM.blur(),n.focus())},100)}};const jr=["pre-wrap","normal","pre-line","break-spaces"];class Uc{constructor(){this.doc=z.empty,this.lineWrapping=!1,this.heightSamples={},this.lineHeight=14,this.charWidth=7,this.lineLength=30,this.heightChanged=!1}heightForGap(e,t){let i=this.doc.lineAt(t).number-this.doc.lineAt(e).number+1;return this.lineWrapping&&(i+=Math.ceil((t-e-i*this.lineLength*.5)/this.lineLength)),this.lineHeight*i}heightForLine(e){return this.lineWrapping?(1+Math.max(0,Math.ceil((e-this.lineLength)/(this.lineLength-5))))*this.lineHeight:this.lineHeight}setDoc(e){return this.doc=e,this}mustRefreshForWrapping(e){return jr.indexOf(e)>-1!=this.lineWrapping}mustRefreshForHeights(e){let t=!1;for(let i=0;i-1,l=Math.round(t)!=Math.round(this.lineHeight)||this.lineWrapping!=o;if(this.lineWrapping=o,this.lineHeight=t,this.charWidth=i,this.lineLength=s,l){this.heightSamples={};for(let h=0;h0}set outdated(e){this.flags=(e?2:0)|this.flags&-3}setHeight(e,t){this.height!=t&&(Math.abs(this.height-t)>Ei&&(e.heightChanged=!0),this.height=t)}replace(e,t,i){return be.of(i)}decomposeLeft(e,t){t.push(this)}decomposeRight(e,t){t.push(this)}applyChanges(e,t,i,s){let r=this;for(let o=s.length-1;o>=0;o--){let{fromA:l,toA:h,fromB:a,toB:c}=s[o],f=r.lineAt(l,j.ByPosNoHeight,t,0,0),u=f.to>=h?f:r.lineAt(h,j.ByPosNoHeight,t,0,0);for(c+=u.to-h,h=u.to;o>0&&f.from<=s[o-1].toA;)l=s[o-1].fromA,a=s[o-1].fromB,o--,lr*2){let l=e[t-1];l.break?e.splice(--t,1,l.left,null,l.right):e.splice(--t,1,l.left,l.right),i+=1+l.break,s-=l.size}else if(r>s*2){let l=e[i];l.break?e.splice(i,1,l.left,null,l.right):e.splice(i,1,l.left,l.right),i+=2+l.break,r-=l.size}else break;else if(s=r&&o(this.blockAt(0,i,s,r))}updateHeight(e,t=0,i=!1,s){return s&&s.from<=t&&s.more&&this.setHeight(e,s.heights[s.index++]),this.outdated=!1,this}toString(){return`block(${this.length})`}}class Se extends Ul{constructor(e,t){super(e,t,J.Text),this.collapsed=0,this.widgetHeight=0}replace(e,t,i){let s=i[0];return i.length==1&&(s instanceof Se||s instanceof se&&s.flags&4)&&Math.abs(this.length-s.length)<10?(s instanceof se?s=new Se(s.length,this.height):s.height=this.height,this.outdated||(s.outdated=!1),s):be.of(i)}updateHeight(e,t=0,i=!1,s){return s&&s.from<=t&&s.more?this.setHeight(e,s.heights[s.index++]):(i||this.outdated)&&this.setHeight(e,Math.max(this.widgetHeight,e.heightForLine(this.length-this.collapsed))),this.outdated=!1,this}toString(){return`line(${this.length}${this.collapsed?-this.collapsed:""}${this.widgetHeight?":"+this.widgetHeight:""})`}}class se extends be{constructor(e){super(e,0)}lines(e,t){let i=e.lineAt(t).number,s=e.lineAt(t+this.length).number;return{firstLine:i,lastLine:s,lineHeight:this.height/(s-i+1)}}blockAt(e,t,i,s){let{firstLine:r,lastLine:o,lineHeight:l}=this.lines(t,s),h=Math.max(0,Math.min(o-r,Math.floor((e-i)/l))),{from:a,length:c}=t.line(r+h);return new et(a,c,i+l*h,l,J.Text)}lineAt(e,t,i,s,r){if(t==j.ByHeight)return this.blockAt(e,i,s,r);if(t==j.ByPosNoHeight){let{from:f,to:u}=i.lineAt(e);return new et(f,u-f,0,0,J.Text)}let{firstLine:o,lineHeight:l}=this.lines(i,r),{from:h,length:a,number:c}=i.lineAt(e);return new et(h,a,s+l*(c-o),l,J.Text)}forEachLine(e,t,i,s,r,o){let{firstLine:l,lineHeight:h}=this.lines(i,r);for(let a=Math.max(e,r),c=Math.min(r+this.length,t);a<=c;){let f=i.lineAt(a);a==e&&(s+=h*(f.number-l)),o(new et(f.from,f.length,s,h,J.Text)),s+=h,a=f.to+1}}replace(e,t,i){let s=this.length-t;if(s>0){let r=i[i.length-1];r instanceof se?i[i.length-1]=new se(r.length+s):i.push(null,new se(s-1))}if(e>0){let r=i[0];r instanceof se?i[0]=new se(e+r.length):i.unshift(new se(e-1),null)}return be.of(i)}decomposeLeft(e,t){t.push(new se(e-1),null)}decomposeRight(e,t){t.push(null,new se(this.length-e-1))}updateHeight(e,t=0,i=!1,s){let r=t+this.length;if(s&&s.from<=t+this.length&&s.more){let o=[],l=Math.max(t,s.from),h=-1,a=e.heightChanged;for(s.from>t&&o.push(new se(s.from-t-1).updateHeight(e,t));l<=r&&s.more;){let f=e.doc.lineAt(l).length;o.length&&o.push(null);let u=s.heights[s.index++];h==-1?h=u:Math.abs(u-h)>=Ei&&(h=-2);let d=new Se(f,u);d.outdated=!1,o.push(d),l+=f+1}l<=r&&o.push(null,new se(r-l).updateHeight(e,l));let c=be.of(o);return e.heightChanged=a||h<0||Math.abs(c.height-this.height)>=Ei||Math.abs(h-this.lines(e.doc,t).lineHeight)>=Ei,c}else(i||this.outdated)&&(this.setHeight(e,e.heightForGap(t,t+this.length)),this.outdated=!1);return this}toString(){return`gap(${this.length})`}}class Gc extends be{constructor(e,t,i){super(e.length+t+i.length,e.height+i.height,t|(e.outdated||i.outdated?2:0)),this.left=e,this.right=i,this.size=e.size+i.size}get break(){return this.flags&1}blockAt(e,t,i,s){let r=i+this.left.height;return el))return a;let c=t==j.ByPosNoHeight?j.ByPosNoHeight:j.ByPos;return h?a.join(this.right.lineAt(l,c,i,o,l)):this.left.lineAt(l,c,i,s,r).join(a)}forEachLine(e,t,i,s,r,o){let l=s+this.left.height,h=r+this.left.length+this.break;if(this.break)e=h&&this.right.forEachLine(e,t,i,l,h,o);else{let a=this.lineAt(h,j.ByPos,i,s,r);e=e&&a.from<=t&&o(a),t>a.to&&this.right.forEachLine(a.to+1,t,i,l,h,o)}}replace(e,t,i){let s=this.left.length+this.break;if(tthis.left.length)return this.balanced(this.left,this.right.replace(e-s,t-s,i));let r=[];e>0&&this.decomposeLeft(e,r);let o=r.length;for(let l of i)r.push(l);if(e>0&&Gr(r,o-1),t=i&&t.push(null)),e>i&&this.right.decomposeLeft(e-i,t)}decomposeRight(e,t){let i=this.left.length,s=i+this.break;if(e>=s)return this.right.decomposeRight(e-s,t);e2*t.size||t.size>2*e.size?be.of(this.break?[e,null,t]:[e,t]):(this.left=e,this.right=t,this.height=e.height+t.height,this.outdated=e.outdated||t.outdated,this.size=e.size+t.size,this.length=e.length+this.break+t.length,this)}updateHeight(e,t=0,i=!1,s){let{left:r,right:o}=this,l=t+r.length+this.break,h=null;return s&&s.from<=t+r.length&&s.more?h=r=r.updateHeight(e,t,i,s):r.updateHeight(e,t,i),s&&s.from<=l+o.length&&s.more?h=o=o.updateHeight(e,l,i,s):o.updateHeight(e,l,i),h?this.balanced(r,o):(this.height=this.left.height+this.right.height,this.outdated=!1,this)}toString(){return this.left+(this.break?" ":"-")+this.right}}function Gr(n,e){let t,i;n[e]==null&&(t=n[e-1])instanceof se&&(i=n[e+1])instanceof se&&n.splice(e-1,3,new se(t.length+1+i.length))}const Jc=5;class Hs{constructor(e,t){this.pos=e,this.oracle=t,this.nodes=[],this.lineStart=-1,this.lineEnd=-1,this.covering=null,this.writtenTo=e}get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]==this.covering}span(e,t){if(this.lineStart>-1){let i=Math.min(t,this.lineEnd),s=this.nodes[this.nodes.length-1];s instanceof Se?s.length+=i-this.pos:(i>this.pos||!this.isCovered)&&this.nodes.push(new Se(i-this.pos,-1)),this.writtenTo=i,t>i&&(this.nodes.push(null),this.writtenTo++,this.lineStart=-1)}this.pos=t}point(e,t,i){if(e=Jc)&&this.addLineDeco(s,r)}else t>e&&this.span(e,t);this.lineEnd>-1&&this.lineEnd-1)return;let{from:e,to:t}=this.oracle.doc.lineAt(this.pos);this.lineStart=e,this.lineEnd=t,this.writtenToe&&this.nodes.push(new Se(this.pos-e,-1)),this.writtenTo=this.pos}blankContent(e,t){let i=new se(t-e);return this.oracle.doc.lineAt(e).to==t&&(i.flags|=4),i}ensureLine(){this.enterLine();let e=this.nodes.length?this.nodes[this.nodes.length-1]:null;if(e instanceof Se)return e;let t=new Se(0,-1);return this.nodes.push(t),t}addBlock(e){this.enterLine(),e.type==J.WidgetAfter&&!this.isCovered&&this.ensureLine(),this.nodes.push(e),this.writtenTo=this.pos=this.pos+e.length,e.type!=J.WidgetBefore&&(this.covering=e)}addLineDeco(e,t){let i=this.ensureLine();i.length+=t,i.collapsed+=t,i.widgetHeight=Math.max(i.widgetHeight,e),this.writtenTo=this.pos=this.pos+t}finish(e){let t=this.nodes.length==0?null:this.nodes[this.nodes.length-1];this.lineStart>-1&&!(t instanceof Se)&&!this.isCovered?this.nodes.push(new Se(0,-1)):(this.writtenToa.clientHeight||a.scrollWidth>a.clientWidth)&&c.overflow!="visible"){let f=a.getBoundingClientRect();i=Math.max(i,f.left),s=Math.min(s,f.right),r=Math.max(r,f.top),o=Math.min(o,f.bottom)}h=c.position=="absolute"||c.position=="fixed"?a.offsetParent:a.parentNode}else if(h.nodeType==11)h=h.host;else break;return{left:i-t.left,right:Math.max(i,s)-t.left,top:r-(t.top+e),bottom:Math.max(r,o)-(t.top+e)}}function _c(n,e){let t=n.getBoundingClientRect();return{left:0,right:t.right-t.left,top:e,bottom:t.bottom-(t.top+e)}}class On{constructor(e,t,i){this.from=e,this.to=t,this.size=i}static same(e,t){if(e.length!=t.length)return!1;for(let i=0;itypeof t!="function"),this.heightMap=be.empty().applyChanges(this.stateDeco,z.empty,this.heightOracle.setDoc(e.doc),[new Ue(0,0,0,e.doc.length)]),this.viewport=this.getViewport(0,null),this.updateViewportLines(),this.updateForViewport(),this.lineGaps=this.ensureLineGaps([]),this.lineGapDeco=P.set(this.lineGaps.map(t=>t.draw(!1))),this.computeVisibleRanges()}updateForViewport(){let e=[this.viewport],{main:t}=this.state.selection;for(let i=0;i<=1;i++){let s=i?t.head:t.anchor;if(!e.some(({from:r,to:o})=>s>=r&&s<=o)){let{from:r,to:o}=this.lineBlockAt(s);e.push(new xi(r,o))}}this.viewports=e.sort((i,s)=>i.from-s.from),this.scaler=this.heightMap.height<=7e6?Yr:new tf(this.heightOracle.doc,this.heightMap,this.viewports)}updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine(this.viewport.from,this.viewport.to,this.state.doc,0,0,e=>{this.viewportLines.push(this.scaler.scale==1?e:$t(e,this.scaler))})}update(e,t=null){this.state=e.state;let i=this.stateDeco;this.stateDeco=this.state.facet(li).filter(a=>typeof a!="function");let s=e.changedRanges,r=Ue.extendWithRanges(s,$c(i,this.stateDeco,e?e.changes:ee.empty(this.state.doc.length))),o=this.heightMap.height;this.heightMap=this.heightMap.applyChanges(this.stateDeco,e.startState.doc,this.heightOracle.setDoc(this.state.doc),r),this.heightMap.height!=o&&(e.flags|=2);let l=r.length?this.mapViewport(this.viewport,e.changes):this.viewport;(t&&(t.range.headl.to)||!this.viewportIsAppropriate(l))&&(l=this.getViewport(0,t));let h=!e.changes.empty||e.flags&2||l.from!=this.viewport.from||l.to!=this.viewport.to;this.viewport=l,this.updateForViewport(),h&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps,e.changes))),e.flags|=this.computeVisibleRanges(),t&&(this.scrollTarget=t),!this.mustEnforceCursorAssoc&&e.selectionSet&&e.view.lineWrapping&&e.state.selection.main.empty&&e.state.selection.main.assoc&&(this.mustEnforceCursorAssoc=!0)}measure(e){let t=e.contentDOM,i=window.getComputedStyle(t),s=this.heightOracle,r=i.whiteSpace;this.defaultTextDirection=i.direction=="rtl"?Z.RTL:Z.LTR;let o=this.heightOracle.mustRefreshForWrapping(r),l=o||this.mustMeasureContent||this.contentDOMHeight!=t.clientHeight;this.contentDOMHeight=t.clientHeight,this.mustMeasureContent=!1;let h=0,a=0,c=parseInt(i.paddingTop)||0,f=parseInt(i.paddingBottom)||0;(this.paddingTop!=c||this.paddingBottom!=f)&&(this.paddingTop=c,this.paddingBottom=f,h|=10),this.editorWidth!=e.scrollDOM.clientWidth&&(s.lineWrapping&&(l=!0),this.editorWidth=e.scrollDOM.clientWidth,h|=8);let u=(this.printing?_c:Yc)(t,this.paddingTop),d=u.top-this.pixelViewport.top,p=u.bottom-this.pixelViewport.bottom;this.pixelViewport=u;let g=this.pixelViewport.bottom>this.pixelViewport.top&&this.pixelViewport.right>this.pixelViewport.left;if(g!=this.inView&&(this.inView=g,g&&(l=!0)),!this.inView)return 0;let y=t.clientWidth;if((this.contentDOMWidth!=y||this.editorHeight!=e.scrollDOM.clientHeight)&&(this.contentDOMWidth=y,this.editorHeight=e.scrollDOM.clientHeight,h|=8),l){let v=e.docView.measureVisibleLineHeights(this.viewport);if(s.mustRefreshForHeights(v)&&(o=!0),o||s.lineWrapping&&Math.abs(y-this.contentDOMWidth)>s.charWidth){let{lineHeight:A,charWidth:x}=e.docView.measureTextSize();o=s.refresh(r,A,x,y/x,v),o&&(e.docView.minWidth=0,h|=8)}d>0&&p>0?a=Math.max(d,p):d<0&&p<0&&(a=Math.min(d,p)),s.heightChanged=!1;for(let A of this.viewports){let x=A.from==this.viewport.from?v:e.docView.measureVisibleLineHeights(A);this.heightMap=this.heightMap.updateHeight(s,0,o,new jc(A.from,x))}s.heightChanged&&(h|=2)}let b=!this.viewportIsAppropriate(this.viewport,a)||this.scrollTarget&&(this.scrollTarget.range.headthis.viewport.to);return b&&(this.viewport=this.getViewport(a,this.scrollTarget)),this.updateForViewport(),(h&2||b)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(o?[]:this.lineGaps)),h|=this.computeVisibleRanges(),this.mustEnforceCursorAssoc&&(this.mustEnforceCursorAssoc=!1,e.docView.enforceCursorAssoc()),h}get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bottom)}getViewport(e,t){let i=.5-Math.max(-.5,Math.min(.5,e/1e3/2)),s=this.heightMap,r=this.state.doc,{visibleTop:o,visibleBottom:l}=this,h=new xi(s.lineAt(o-i*1e3,j.ByHeight,r,0,0).from,s.lineAt(l+(1-i)*1e3,j.ByHeight,r,0,0).to);if(t){let{head:a}=t.range;if(ah.to){let c=Math.min(this.editorHeight,this.pixelViewport.bottom-this.pixelViewport.top),f=s.lineAt(a,j.ByPos,r,0,0),u;t.y=="center"?u=(f.top+f.bottom)/2-c/2:t.y=="start"||t.y=="nearest"&&a=l+Math.max(10,Math.min(i,250)))&&s>o-2*1e3&&ri.from&&l.push({from:i.from,to:r}),o=i.from&&h.from<=i.to&&Xr(l,h.from-10,h.from+10),!h.empty&&h.to>=i.from&&h.to<=i.to&&Xr(l,h.to-10,h.to+10);for(let{from:a,to:c}of l)c-a>1e3&&t.push(ef(e,f=>f.from>=i.from&&f.to<=i.to&&Math.abs(f.from-a)<1e3&&Math.abs(f.to-c)<1e3)||new On(a,c,this.gapSize(i,a,c,s)))}return t}gapSize(e,t,i,s){let r=$r(s,i)-$r(s,t);return this.heightOracle.lineWrapping?e.height*r:s.total*this.heightOracle.charWidth*r}updateLineGaps(e){On.same(e,this.lineGaps)||(this.lineGaps=e,this.lineGapDeco=P.set(e.map(t=>t.draw(this.heightOracle.lineWrapping))))}computeVisibleRanges(){let e=this.stateDeco;this.lineGaps.length&&(e=e.concat(this.lineGapDeco));let t=[];Y.spans(e,this.viewport.from,this.viewport.to,{span(s,r){t.push({from:s,to:r})},point(){}},20);let i=t.length!=this.visibleRanges.length||this.visibleRanges.some((s,r)=>s.from!=t[r].from||s.to!=t[r].to);return this.visibleRanges=t,i?4:0}lineBlockAt(e){return e>=this.viewport.from&&e<=this.viewport.to&&this.viewportLines.find(t=>t.from<=e&&t.to>=e)||$t(this.heightMap.lineAt(e,j.ByPos,this.state.doc,0,0),this.scaler)}lineBlockAtHeight(e){return $t(this.heightMap.lineAt(this.scaler.fromDOM(e),j.ByHeight,this.state.doc,0,0),this.scaler)}elementAtHeight(e){return $t(this.heightMap.blockAt(this.scaler.fromDOM(e),this.state.doc,0,0),this.scaler)}get docHeight(){return this.scaler.toDOM(this.heightMap.height)}get contentHeight(){return this.docHeight+this.paddingTop+this.paddingBottom}}class xi{constructor(e,t){this.from=e,this.to=t}}function Zc(n,e,t){let i=[],s=n,r=0;return Y.spans(t,n,e,{span(){},point(o,l){o>s&&(i.push({from:s,to:o}),r+=o-s),s=l}},20),s=1)return e[e.length-1].to;let i=Math.floor(n*t);for(let s=0;;s++){let{from:r,to:o}=e[s],l=o-r;if(i<=l)return r+i;i-=l}}function $r(n,e){let t=0;for(let{from:i,to:s}of n.ranges){if(e<=s){t+=e-i;break}t+=s-i}return t/n.total}function Xr(n,e,t){for(let i=0;ie){let r=[];s.fromt&&r.push({from:t,to:s.to}),n.splice(i,1,...r),i+=r.length-1}}}function ef(n,e){for(let t of n)if(e(t))return t}const Yr={toDOM(n){return n},fromDOM(n){return n},scale:1};class tf{constructor(e,t,i){let s=0,r=0,o=0;this.viewports=i.map(({from:l,to:h})=>{let a=t.lineAt(l,j.ByPos,e,0,0).top,c=t.lineAt(h,j.ByPos,e,0,0).bottom;return s+=c-a,{from:l,to:h,top:a,bottom:c,domTop:0,domBottom:0}}),this.scale=(7e6-s)/(t.height-s);for(let l of this.viewports)l.domTop=o+(l.top-r)*this.scale,o=l.domBottom=l.domTop+(l.bottom-l.top),r=l.bottom}toDOM(e){for(let t=0,i=0,s=0;;t++){let r=t$t(s,e)):n.type)}const Si=T.define({combine:n=>n.join(" ")}),ms=T.define({combine:n=>n.indexOf(!0)>-1}),ys=st.newName(),jl=st.newName(),Gl=st.newName(),Jl={"&light":"."+jl,"&dark":"."+Gl};function bs(n,e,t){return new st(e,{finish(i){return/&/.test(i)?i.replace(/&\w*/,s=>{if(s=="&")return n;if(!t||!t[s])throw new RangeError(`Unsupported selector: ${s}`);return t[s]}):n+" "+i}})}const nf=bs("."+ys,{"&.cm-editor":{position:"relative !important",boxSizing:"border-box","&.cm-focused":{outline:"1px dotted #212121"},display:"flex !important",flexDirection:"column"},".cm-scroller":{display:"flex !important",alignItems:"flex-start !important",fontFamily:"monospace",lineHeight:1.4,height:"100%",overflowX:"auto",position:"relative",zIndex:0},".cm-content":{margin:0,flexGrow:2,flexShrink:0,minHeight:"100%",display:"block",whiteSpace:"pre",wordWrap:"normal",boxSizing:"border-box",padding:"4px 0",outline:"none","&[contenteditable=true]":{WebkitUserModify:"read-write-plaintext-only"}},".cm-lineWrapping":{whiteSpace_fallback:"pre-wrap",whiteSpace:"break-spaces",wordBreak:"break-word",overflowWrap:"anywhere",flexShrink:1},"&light .cm-content":{caretColor:"black"},"&dark .cm-content":{caretColor:"white"},".cm-line":{display:"block",padding:"0 2px 0 4px"},".cm-selectionLayer":{zIndex:-1,contain:"size style"},".cm-selectionBackground":{position:"absolute"},"&light .cm-selectionBackground":{background:"#d9d9d9"},"&dark .cm-selectionBackground":{background:"#222"},"&light.cm-focused .cm-selectionBackground":{background:"#d7d4f0"},"&dark.cm-focused .cm-selectionBackground":{background:"#233"},".cm-cursorLayer":{zIndex:100,contain:"size style",pointerEvents:"none"},"&.cm-focused .cm-cursorLayer":{animation:"steps(1) cm-blink 1.2s infinite"},"@keyframes cm-blink":{"0%":{},"50%":{visibility:"hidden"},"100%":{}},"@keyframes cm-blink2":{"0%":{},"50%":{visibility:"hidden"},"100%":{}},".cm-cursor, .cm-dropCursor":{position:"absolute",borderLeft:"1.2px solid black",marginLeft:"-0.6px",pointerEvents:"none"},".cm-cursor":{display:"none"},"&dark .cm-cursor":{borderLeftColor:"#444"},"&.cm-focused .cm-cursor":{display:"block"},"&light .cm-activeLine":{backgroundColor:"#f3f9ff"},"&dark .cm-activeLine":{backgroundColor:"#223039"},"&light .cm-specialChar":{color:"red"},"&dark .cm-specialChar":{color:"#f78"},".cm-gutters":{display:"flex",height:"100%",boxSizing:"border-box",left:0,zIndex:200},"&light .cm-gutters":{backgroundColor:"#f5f5f5",color:"#6c6c6c",borderRight:"1px solid #ddd"},"&dark .cm-gutters":{backgroundColor:"#333338",color:"#ccc"},".cm-gutter":{display:"flex !important",flexDirection:"column",flexShrink:0,boxSizing:"border-box",minHeight:"100%",overflow:"hidden"},".cm-gutterElement":{boxSizing:"border-box"},".cm-lineNumbers .cm-gutterElement":{padding:"0 3px 0 5px",minWidth:"20px",textAlign:"right",whiteSpace:"nowrap"},"&light .cm-activeLineGutter":{backgroundColor:"#e2f2ff"},"&dark .cm-activeLineGutter":{backgroundColor:"#222227"},".cm-panels":{boxSizing:"border-box",position:"sticky",left:0,right:0},"&light .cm-panels":{backgroundColor:"#f5f5f5",color:"black"},"&light .cm-panels-top":{borderBottom:"1px solid #ddd"},"&light .cm-panels-bottom":{borderTop:"1px solid #ddd"},"&dark .cm-panels":{backgroundColor:"#333338",color:"white"},".cm-tab":{display:"inline-block",overflow:"hidden",verticalAlign:"bottom"},".cm-widgetBuffer":{verticalAlign:"text-top",height:"1em",display:"inline"},".cm-placeholder":{color:"#888",display:"inline-block",verticalAlign:"top"},".cm-button":{verticalAlign:"middle",color:"inherit",fontSize:"70%",padding:".2em 1em",borderRadius:"1px"},"&light .cm-button":{backgroundImage:"linear-gradient(#eff1f5, #d9d9df)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#b4b4b4, #d0d3d6)"}},"&dark .cm-button":{backgroundImage:"linear-gradient(#393939, #111)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#111, #333)"}},".cm-textfield":{verticalAlign:"middle",color:"inherit",fontSize:"70%",border:"1px solid silver",padding:".2em .5em"},"&light .cm-textfield":{backgroundColor:"white"},"&dark .cm-textfield":{border:"1px solid #555",backgroundColor:"inherit"}},Jl),sf={childList:!0,characterData:!0,subtree:!0,attributes:!0,characterDataOldValue:!0},Tn=M.ie&&M.ie_version<=11;class rf{constructor(e,t,i){this.view=e,this.onChange=t,this.onScrollChanged=i,this.active=!1,this.selectionRange=new oc,this.selectionChanged=!1,this.delayedFlush=-1,this.resizeTimeout=-1,this.queue=[],this.delayedAndroidKey=null,this.scrollTargets=[],this.intersection=null,this.resize=null,this.intersecting=!1,this.gapIntersection=null,this.gaps=[],this.parentCheck=-1,this.dom=e.contentDOM,this.observer=new MutationObserver(s=>{for(let r of s)this.queue.push(r);(M.ie&&M.ie_version<=11||M.ios&&e.composing)&&s.some(r=>r.type=="childList"&&r.removedNodes.length||r.type=="characterData"&&r.oldValue.length>r.target.nodeValue.length)?this.flushSoon():this.flush()}),Tn&&(this.onCharData=s=>{this.queue.push({target:s.target,type:"characterData",oldValue:s.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),window.addEventListener("resize",this.onResize=this.onResize.bind(this)),typeof ResizeObserver=="function"&&(this.resize=new ResizeObserver(()=>{this.view.docView.lastUpdate{this.parentCheck<0&&(this.parentCheck=setTimeout(this.listenForScroll.bind(this),1e3)),s.length>0&&s[s.length-1].intersectionRatio>0!=this.intersecting&&(this.intersecting=!this.intersecting,this.intersecting!=this.view.inView&&this.onScrollChanged(document.createEvent("Event")))},{}),this.intersection.observe(this.dom),this.gapIntersection=new IntersectionObserver(s=>{s.length>0&&s[s.length-1].intersectionRatio>0&&this.onScrollChanged(document.createEvent("Event"))},{})),this.listenForScroll(),this.readSelectionRange(),this.dom.ownerDocument.addEventListener("selectionchange",this.onSelectionChange)}onScroll(e){this.intersecting&&this.flush(!1),this.onScrollChanged(e)}onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout(()=>{this.resizeTimeout=-1,this.view.requestMeasure()},50))}onPrint(){this.view.viewState.printing=!0,this.view.measure(),setTimeout(()=>{this.view.viewState.printing=!1,this.view.requestMeasure()},500)}updateGaps(e){if(this.gapIntersection&&(e.length!=this.gaps.length||this.gaps.some((t,i)=>t!=e[i]))){this.gapIntersection.disconnect();for(let t of e)this.gapIntersection.observe(t);this.gaps=e}}onSelectionChange(e){if(!this.readSelectionRange()||this.delayedAndroidKey)return;let{view:t}=this,i=this.selectionRange;if(t.state.facet(an)?t.root.activeElement!=this.dom:!ts(t.dom,i))return;let s=i.anchorNode&&t.docView.nearest(i.anchorNode);s&&s.ignoreEvent(e)||((M.ie&&M.ie_version<=11||M.android&&M.chrome)&&!t.state.selection.main.empty&&i.focusNode&&Wi(i.focusNode,i.focusOffset,i.anchorNode,i.anchorOffset)?this.flushSoon():this.flush(!1))}readSelectionRange(){let{root:e}=this.view,t=M.safari&&e.nodeType==11&&nc()==this.view.contentDOM&&of(this.view)||Hi(e);return!t||this.selectionRange.eq(t)?!1:(this.selectionRange.setRange(t),this.selectionChanged=!0)}setSelectionRange(e,t){this.selectionRange.set(e.node,e.offset,t.node,t.offset),this.selectionChanged=!1}clearSelectionRange(){this.selectionRange.set(null,0,null,0)}listenForScroll(){this.parentCheck=-1;let e=0,t=null;for(let i=this.dom;i;)if(i.nodeType==1)!t&&e{let i=this.delayedAndroidKey;this.delayedAndroidKey=null,this.delayedFlush=-1,this.flush()||Zt(this.view.contentDOM,i.key,i.keyCode)}),(!this.delayedAndroidKey||e=="Enter")&&(this.delayedAndroidKey={key:e,keyCode:t})}flushSoon(){this.delayedFlush<0&&(this.delayedFlush=window.setTimeout(()=>{this.delayedFlush=-1,this.flush()},20))}forceFlush(){this.delayedFlush>=0&&(window.clearTimeout(this.delayedFlush),this.delayedFlush=-1,this.flush())}processRecords(){let e=this.queue;for(let r of this.observer.takeRecords())e.push(r);e.length&&(this.queue=[]);let t=-1,i=-1,s=!1;for(let r of e){let o=this.readMutation(r);!o||(o.typeOver&&(s=!0),t==-1?{from:t,to:i}=o:(t=Math.min(o.from,t),i=Math.max(o.to,i)))}return{from:t,to:i,typeOver:s}}flush(e=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return;e&&this.readSelectionRange();let{from:t,to:i,typeOver:s}=this.processRecords(),r=this.selectionChanged&&ts(this.dom,this.selectionRange);if(t<0&&!r)return;this.selectionChanged=!1;let o=this.view.state,l=this.onChange(t,i,s);return this.view.state==o&&this.view.update([]),l}readMutation(e){let t=this.view.docView.nearest(e.target);if(!t||t.ignoreMutation(e))return null;if(t.markDirty(e.type=="attributes"),e.type=="attributes"&&(t.dirty|=4),e.type=="childList"){let i=_r(t,e.previousSibling||e.target.previousSibling,-1),s=_r(t,e.nextSibling||e.target.nextSibling,1);return{from:i?t.posAfter(i):t.posAtStart,to:s?t.posBefore(s):t.posAtEnd,typeOver:!1}}else return e.type=="characterData"?{from:t.posAtStart,to:t.posAtEnd,typeOver:e.target.nodeValue==e.oldValue}:null}destroy(){var e,t,i;this.stop(),(e=this.intersection)===null||e===void 0||e.disconnect(),(t=this.gapIntersection)===null||t===void 0||t.disconnect(),(i=this.resize)===null||i===void 0||i.disconnect();for(let s of this.scrollTargets)s.removeEventListener("scroll",this.onScroll);window.removeEventListener("scroll",this.onScroll),window.removeEventListener("resize",this.onResize),window.removeEventListener("beforeprint",this.onPrint),this.dom.ownerDocument.removeEventListener("selectionchange",this.onSelectionChange),clearTimeout(this.parentCheck),clearTimeout(this.resizeTimeout)}}function _r(n,e,t){for(;e;){let i=$.get(e);if(i&&i.parent==n)return i;let s=e.parentNode;e=s!=n.dom?s:t>0?e.nextSibling:e.previousSibling}return null}function of(n){let e=null;function t(h){h.preventDefault(),h.stopImmediatePropagation(),e=h.getTargetRanges()[0]}if(n.contentDOM.addEventListener("beforeinput",t,!0),document.execCommand("indent"),n.contentDOM.removeEventListener("beforeinput",t,!0),!e)return null;let i=e.startContainer,s=e.startOffset,r=e.endContainer,o=e.endOffset,l=n.docView.domAtPos(n.state.selection.main.anchor);return Wi(l.node,l.offset,r,o)&&([i,s,r,o]=[r,o,i,s]),{anchorNode:i,anchorOffset:s,focusNode:r,focusOffset:o}}function lf(n,e,t,i){let s,r,o=n.state.selection.main;if(e>-1){let l=n.docView.domBoundsAround(e,t,0);if(!l||n.state.readOnly)return!1;let{from:h,to:a}=l,c=n.docView.impreciseHead||n.docView.impreciseAnchor?[]:af(n),f=new Rl(c,n.state);f.readRange(l.startDOM,l.endDOM);let u=o.from,d=null;(n.inputState.lastKeyCode===8&&n.inputState.lastKeyTime>Date.now()-100||M.android&&f.text.length=o.from&&s.to<=o.to&&(s.from!=o.from||s.to!=o.to)&&o.to-o.from-(s.to-s.from)<=4?s={from:o.from,to:o.to,insert:n.state.doc.slice(o.from,s.from).append(s.insert).append(n.state.doc.slice(s.to,o.to))}:(M.mac||M.android)&&s&&s.from==s.to&&s.from==o.head-1&&s.insert.toString()=="."&&(s={from:o.from,to:o.to,insert:z.of([" "])}),s){let l=n.state;if(M.ios&&n.inputState.flushIOSKey(n)||M.android&&(s.from==o.from&&s.to==o.to&&s.insert.length==1&&s.insert.lines==2&&Zt(n.contentDOM,"Enter",13)||s.from==o.from-1&&s.to==o.to&&s.insert.length==0&&Zt(n.contentDOM,"Backspace",8)||s.from==o.from&&s.to==o.to+1&&s.insert.length==0&&Zt(n.contentDOM,"Delete",46)))return!0;let h=s.insert.toString();if(n.state.facet(vl).some(f=>f(n,s.from,s.to,h)))return!0;n.inputState.composing>=0&&n.inputState.composing++;let a;if(s.from>=o.from&&s.to<=o.to&&s.to-s.from>=(o.to-o.from)/3&&(!r||r.main.empty&&r.main.from==s.from+s.insert.length)&&n.inputState.composing<0){let f=o.froms.to?l.sliceDoc(s.to,o.to):"";a=l.replaceSelection(n.state.toText(f+s.insert.sliceString(0,void 0,n.state.lineBreak)+u))}else{let f=l.changes(s),u=r&&!l.selection.main.eq(r.main)&&r.main.to<=f.newLength?r.main:void 0;if(l.selection.ranges.length>1&&n.inputState.composing>=0&&s.to<=o.to&&s.to>=o.to-10){let d=n.state.sliceDoc(s.from,s.to),p=Ll(n)||n.state.doc.lineAt(o.head),g=o.to-s.to,y=o.to-o.from;a=l.changeByRange(b=>{if(b.from==o.from&&b.to==o.to)return{changes:f,range:u||b.map(f)};let v=b.to-g,A=v-d.length;if(b.to-b.from!=y||n.state.sliceDoc(A,v)!=d||p&&b.to>=p.from&&b.from<=p.to)return{range:b};let x=l.changes({from:A,to:v,insert:s.insert}),S=b.to-o.to;return{changes:x,range:u?m.range(Math.max(0,u.anchor+S),Math.max(0,u.head+S)):b.map(x)}})}else a={changes:f,selection:u&&l.selection.replaceRange(u)}}let c="input.type";return n.composing&&(c+=".compose",n.inputState.compositionFirstChange&&(c+=".start",n.inputState.compositionFirstChange=!1)),n.dispatch(a,{scrollIntoView:!0,userEvent:c}),!0}else if(r&&!r.main.eq(o)){let l=!1,h="select";return n.inputState.lastSelectionTime>Date.now()-50&&(n.inputState.lastSelectionOrigin=="select"&&(l=!0),h=n.inputState.lastSelectionOrigin),n.dispatch({selection:r,scrollIntoView:l,userEvent:h}),!0}else return!1}function hf(n,e,t,i){let s=Math.min(n.length,e.length),r=0;for(;r0&&l>0&&n.charCodeAt(o-1)==e.charCodeAt(l-1);)o--,l--;if(i=="end"){let h=Math.max(0,r-Math.min(o,l));t-=o+h-r}return o=o?r-t:0,l=r+(l-o),o=r):l=l?r-t:0,o=r+(o-l),l=r),{from:r,toA:o,toB:l}}function af(n){let e=[];if(n.root.activeElement!=n.contentDOM)return e;let{anchorNode:t,anchorOffset:i,focusNode:s,focusOffset:r}=n.observer.selectionRange;return t&&(e.push(new Tr(t,i)),(s!=t||r!=i)&&e.push(new Tr(s,r))),e}function cf(n,e){if(n.length==0)return null;let t=n[0].pos,i=n.length==2?n[1].pos:t;return t>-1&&i>-1?m.single(t+e,i+e):null}class O{constructor(e={}){this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.contentDOM=document.createElement("div"),this.scrollDOM=document.createElement("div"),this.scrollDOM.tabIndex=-1,this.scrollDOM.className="cm-scroller",this.scrollDOM.appendChild(this.contentDOM),this.announceDOM=document.createElement("div"),this.announceDOM.style.cssText="position: absolute; top: -10000px",this.announceDOM.setAttribute("aria-live","polite"),this.dom=document.createElement("div"),this.dom.appendChild(this.announceDOM),this.dom.appendChild(this.scrollDOM),this._dispatch=e.dispatch||(t=>this.update([t])),this.dispatch=this.dispatch.bind(this),this.root=e.root||lc(e.parent)||document,this.viewState=new Jr(e.state||V.create(e)),this.plugins=this.state.facet(Gt).map(t=>new An(t));for(let t of this.plugins)t.update(this);this.observer=new rf(this,(t,i,s)=>lf(this,t,i,s),t=>{this.inputState.runScrollHandlers(this,t),this.observer.intersecting&&this.measure()}),this.inputState=new Lc(this),this.inputState.ensureHandlers(this,this.plugins),this.docView=new Br(this),this.mountStyles(),this.updateAttrs(),this.updateState=0,this.requestMeasure(),e.parent&&e.parent.appendChild(this.dom)}get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return this.inputState.composing>0}get compositionStarted(){return this.inputState.composing>=0}dispatch(...e){this._dispatch(e.length==1&&e[0]instanceof te?e[0]:this.state.update(...e))}update(e){if(this.updateState!=0)throw new Error("Calls to EditorView.update are not allowed while an update is in progress");let t=!1,i=!1,s,r=this.state;for(let l of e){if(l.startState!=r)throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");r=l.state}if(this.destroyed){this.viewState.state=r;return}if(this.observer.clear(),r.facet(V.phrases)!=this.state.facet(V.phrases))return this.setState(r);s=Ki.create(this,r,e);let o=this.viewState.scrollTarget;try{this.updateState=2;for(let l of e){if(o&&(o=o.map(l.changes)),l.scrollIntoView){let{main:h}=l.state.selection;o=new qi(h.empty?h:m.cursor(h.head,h.head>h.anchor?-1:1))}for(let h of l.effects)h.is(Dr)&&(o=h.value)}this.viewState.update(s,o),this.bidiCache=Ui.update(this.bidiCache,s.changes),s.empty||(this.updatePlugins(s),this.inputState.update(s)),t=this.docView.update(s),this.state.facet(Jt)!=this.styleModules&&this.mountStyles(),i=this.updateAttrs(),this.showAnnouncements(e),this.docView.updateSelection(t,e.some(l=>l.isUserEvent("select.pointer")))}finally{this.updateState=0}if(s.startState.facet(Si)!=s.state.facet(Si)&&(this.viewState.mustMeasureContent=!0),(t||i||o||this.viewState.mustEnforceCursorAssoc||this.viewState.mustMeasureContent)&&this.requestMeasure(),!s.empty)for(let l of this.state.facet(fs))l(s)}setState(e){if(this.updateState!=0)throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");if(this.destroyed){this.viewState.state=e;return}this.updateState=2;let t=this.hasFocus;try{for(let i of this.plugins)i.destroy(this);this.viewState=new Jr(e),this.plugins=e.facet(Gt).map(i=>new An(i)),this.pluginMap.clear();for(let i of this.plugins)i.update(this);this.docView=new Br(this),this.inputState.ensureHandlers(this,this.plugins),this.mountStyles(),this.updateAttrs(),this.bidiCache=[]}finally{this.updateState=0}t&&this.focus(),this.requestMeasure()}updatePlugins(e){let t=e.startState.facet(Gt),i=e.state.facet(Gt);if(t!=i){let s=[];for(let r of i){let o=t.indexOf(r);if(o<0)s.push(new An(r));else{let l=this.plugins[o];l.mustUpdate=e,s.push(l)}}for(let r of this.plugins)r.mustUpdate!=e&&r.destroy(this);this.plugins=s,this.pluginMap.clear(),this.inputState.ensureHandlers(this,this.plugins)}else for(let s of this.plugins)s.mustUpdate=e;for(let s=0;s-1&&cancelAnimationFrame(this.measureScheduled),this.measureScheduled=0,e&&this.observer.flush();let t=null;try{for(let i=0;;i++){this.updateState=1;let s=this.viewport,r=this.viewState.measure(this);if(!r&&!this.measureRequests.length&&this.viewState.scrollTarget==null)break;if(i>5){console.warn(this.measureRequests.length?"Measure loop restarted more than 5 times":"Viewport failed to stabilize");break}let o=[];r&4||([this.measureRequests,o]=[o,this.measureRequests]);let l=o.map(f=>{try{return f.read(this)}catch(u){return Pe(this.state,u),Qr}}),h=Ki.create(this,this.state,[]),a=!1,c=!1;h.flags|=r,t?t.flags|=r:t=h,this.updateState=2,h.empty||(this.updatePlugins(h),this.inputState.update(h),this.updateAttrs(),a=this.docView.update(h));for(let f=0;f{let s=as(this.contentDOM,this.contentAttrs,t),r=as(this.dom,this.editorAttrs,e);return s||r});return this.editorAttrs=e,this.contentAttrs=t,i}showAnnouncements(e){let t=!0;for(let i of e)for(let s of i.effects)if(s.is(O.announce)){t&&(this.announceDOM.textContent=""),t=!1;let r=this.announceDOM.appendChild(document.createElement("div"));r.textContent=s.value}}mountStyles(){this.styleModules=this.state.facet(Jt),st.mount(this.root,this.styleModules.concat(nf).reverse())}readMeasured(){if(this.updateState==2)throw new Error("Reading the editor layout isn't allowed during an update");this.updateState==0&&this.measureScheduled>-1&&this.measure(!1)}requestMeasure(e){if(this.measureScheduled<0&&(this.measureScheduled=requestAnimationFrame(()=>this.measure())),e){if(e.key!=null){for(let t=0;ti.spec==e)||null),t&&t.update(this).value}get documentTop(){return this.contentDOM.getBoundingClientRect().top+this.viewState.paddingTop}get documentPadding(){return{top:this.viewState.paddingTop,bottom:this.viewState.paddingBottom}}elementAtHeight(e){return this.readMeasured(),this.viewState.elementAtHeight(e)}lineBlockAtHeight(e){return this.readMeasured(),this.viewState.lineBlockAtHeight(e)}get viewportLineBlocks(){return this.viewState.viewportLines}lineBlockAt(e){return this.viewState.lineBlockAt(e)}get contentHeight(){return this.viewState.contentHeight}moveByChar(e,t,i){return Dn(this,e,Nr(this,e,t,i))}moveByGroup(e,t){return Dn(this,e,Nr(this,e,t,i=>Pc(this,e.head,i)))}moveToLineBoundary(e,t,i=!0){return Bc(this,e,t,i)}moveVertically(e,t,i){return Dn(this,e,Rc(this,e,t,i))}domAtPos(e){return this.docView.domAtPos(e)}posAtDOM(e,t=0){return this.docView.posFromDOM(e,t)}posAtCoords(e,t=!0){return this.readMeasured(),Il(this,e,t)}coordsAtPos(e,t=1){this.readMeasured();let i=this.docView.coordsAt(e,t);if(!i||i.left==i.right)return i;let s=this.state.doc.lineAt(e),r=this.bidiSpans(s),o=r[Tt.find(r,e-s.from,-1,t)];return on(i,o.dir==Z.LTR==t>0)}get defaultCharacterWidth(){return this.viewState.heightOracle.charWidth}get defaultLineHeight(){return this.viewState.heightOracle.lineHeight}get textDirection(){return this.viewState.defaultTextDirection}textDirectionAt(e){return!this.state.facet(Cl)||ethis.viewport.to?this.textDirection:(this.readMeasured(),this.docView.textDirectionAt(e))}get lineWrapping(){return this.viewState.heightOracle.lineWrapping}bidiSpans(e){if(e.length>ff)return Bl(e.length);let t=this.textDirectionAt(e.from);for(let s of this.bidiCache)if(s.from==e.from&&s.dir==t)return s.order;let i=yc(e.text,t);return this.bidiCache.push(new Ui(e.from,e.to,t,i)),i}get hasFocus(){var e;return(document.hasFocus()||M.safari&&((e=this.inputState)===null||e===void 0?void 0:e.lastContextMenu)>Date.now()-3e4)&&this.root.activeElement==this.contentDOM}focus(){this.observer.ignore(()=>{ol(this.contentDOM),this.docView.updateSelection()})}destroy(){for(let e of this.plugins)e.destroy(this);this.plugins=[],this.inputState.destroy(),this.dom.remove(),this.observer.destroy(),this.measureScheduled>-1&&cancelAnimationFrame(this.measureScheduled),this.destroyed=!0}static scrollIntoView(e,t={}){return Dr.of(new qi(typeof e=="number"?m.cursor(e):e,t.y,t.x,t.yMargin,t.xMargin))}static domEventHandlers(e){return ue.define(()=>({}),{eventHandlers:e})}static theme(e,t){let i=st.newName(),s=[Si.of(i),Jt.of(bs(`.${i}`,e))];return t&&t.dark&&s.push(ms.of(!0)),s}static baseTheme(e){return fi.lowest(Jt.of(bs("."+ys,e,Jl)))}static findFromDOM(e){var t;let i=e.querySelector(".cm-content"),s=i&&$.get(i)||$.get(e);return((t=s==null?void 0:s.rootView)===null||t===void 0?void 0:t.view)||null}}O.styleModule=Jt;O.inputHandler=vl;O.perLineTextDirection=Cl;O.exceptionSink=Sl;O.updateListener=fs;O.editable=an;O.mouseSelectionStyle=kl;O.dragMovesSelection=xl;O.clickAddsSelectionRange=wl;O.decorations=li;O.atomicRanges=Dl;O.scrollMargins=Ol;O.darkTheme=ms;O.contentAttributes=Ml;O.editorAttributes=Al;O.lineWrapping=O.contentAttributes.of({class:"cm-lineWrapping"});O.announce=H.define();const ff=4096,Qr={};class Ui{constructor(e,t,i,s){this.from=e,this.to=t,this.dir=i,this.order=s}static update(e,t){if(t.empty)return e;let i=[],s=e.length?e[e.length-1].dir:Z.LTR;for(let r=Math.max(0,e.length-10);r=0;s--){let r=i[s],o=typeof r=="function"?r(n):r;o&&hs(o,t)}return t}const uf=M.mac?"mac":M.windows?"win":M.linux?"linux":"key";function df(n,e){const t=n.split(/-(?!$)/);let i=t[t.length-1];i=="Space"&&(i=" ");let s,r,o,l;for(let h=0;hi.concat(s),[]))),t}function gf(n,e,t){return Xl($l(n.state),e,n,t)}let Xe=null;const mf=4e3;function yf(n,e=uf){let t=Object.create(null),i=Object.create(null),s=(o,l)=>{let h=i[o];if(h==null)i[o]=l;else if(h!=l)throw new Error("Key binding "+o+" is used both as a regular binding and as a multi-stroke prefix")},r=(o,l,h,a)=>{let c=t[o]||(t[o]=Object.create(null)),f=l.split(/ (?!$)/).map(p=>df(p,e));for(let p=1;p{let b=Xe={view:y,prefix:g,scope:o};return setTimeout(()=>{Xe==b&&(Xe=null)},mf),!0}]})}let u=f.join(" ");s(u,!1);let d=c[u]||(c[u]={preventDefault:!1,commands:[]});d.commands.push(h),a&&(d.preventDefault=!0)};for(let o of n){let l=o[e]||o.key;if(!!l)for(let h of o.scope?o.scope.split(" "):["editor"])r(h,l,o.run,o.preventDefault),o.shift&&r(h,"Shift-"+l,o.shift,o.preventDefault)}return t}function Xl(n,e,t,i){let s=ic(e),r=re(s,0),o=ve(r)==s.length&&s!=" ",l="",h=!1;Xe&&Xe.view==t&&Xe.scope==i&&(l=Xe.prefix+" ",(h=Vl.indexOf(e.keyCode)<0)&&(Xe=null));let a=u=>{if(u){for(let d of u.commands)if(d(t))return!0;u.preventDefault&&(h=!0)}return!1},c=n[i],f;if(c){if(a(c[l+vi(s,e,!o)]))return!0;if(o&&(e.shiftKey||e.altKey||e.metaKey||r>127)&&(f=rt[e.keyCode])&&f!=s){if(a(c[l+vi(f,e,!0)]))return!0;if(e.shiftKey&&Pt[e.keyCode]!=f&&a(c[l+vi(Pt[e.keyCode],e,!1)]))return!0}else if(o&&e.shiftKey&&a(c[l+vi(s,e,!0)]))return!0}return h}const Yl=!M.ios,Xt=T.define({combine(n){return Ht(n,{cursorBlinkRate:1200,drawRangeCursor:!0},{cursorBlinkRate:(e,t)=>Math.min(e,t),drawRangeCursor:(e,t)=>e||t})}});function bf(n={}){return[Xt.of(n),wf,xf]}class _l{constructor(e,t,i,s,r){this.left=e,this.top=t,this.width=i,this.height=s,this.className=r}draw(){let e=document.createElement("div");return e.className=this.className,this.adjust(e),e}adjust(e){e.style.left=this.left+"px",e.style.top=this.top+"px",this.width>=0&&(e.style.width=this.width+"px"),e.style.height=this.height+"px"}eq(e){return this.left==e.left&&this.top==e.top&&this.width==e.width&&this.height==e.height&&this.className==e.className}}const wf=ue.fromClass(class{constructor(n){this.view=n,this.rangePieces=[],this.cursors=[],this.measureReq={read:this.readPos.bind(this),write:this.drawSel.bind(this)},this.selectionLayer=n.scrollDOM.appendChild(document.createElement("div")),this.selectionLayer.className="cm-selectionLayer",this.selectionLayer.setAttribute("aria-hidden","true"),this.cursorLayer=n.scrollDOM.appendChild(document.createElement("div")),this.cursorLayer.className="cm-cursorLayer",this.cursorLayer.setAttribute("aria-hidden","true"),n.requestMeasure(this.measureReq),this.setBlinkRate()}setBlinkRate(){this.cursorLayer.style.animationDuration=this.view.state.facet(Xt).cursorBlinkRate+"ms"}update(n){let e=n.startState.facet(Xt)!=n.state.facet(Xt);(e||n.selectionSet||n.geometryChanged||n.viewportChanged)&&this.view.requestMeasure(this.measureReq),n.transactions.some(t=>t.scrollIntoView)&&(this.cursorLayer.style.animationName=this.cursorLayer.style.animationName=="cm-blink"?"cm-blink2":"cm-blink"),e&&this.setBlinkRate()}readPos(){let{state:n}=this.view,e=n.facet(Xt),t=n.selection.ranges.map(s=>s.empty?[]:kf(this.view,s)).reduce((s,r)=>s.concat(r)),i=[];for(let s of n.selection.ranges){let r=s==n.selection.main;if(s.empty?!r||Yl:e.drawRangeCursor){let o=Sf(this.view,s,r);o&&i.push(o)}}return{rangePieces:t,cursors:i}}drawSel({rangePieces:n,cursors:e}){if(n.length!=this.rangePieces.length||n.some((t,i)=>!t.eq(this.rangePieces[i]))){this.selectionLayer.textContent="";for(let t of n)this.selectionLayer.appendChild(t.draw());this.rangePieces=n}if(e.length!=this.cursors.length||e.some((t,i)=>!t.eq(this.cursors[i]))){let t=this.cursorLayer.children;if(t.length!==e.length){this.cursorLayer.textContent="";for(const i of e)this.cursorLayer.appendChild(i.draw())}else e.forEach((i,s)=>i.adjust(t[s]));this.cursors=e}}destroy(){this.selectionLayer.remove(),this.cursorLayer.remove()}}),Ql={".cm-line":{"& ::selection":{backgroundColor:"transparent !important"},"&::selection":{backgroundColor:"transparent !important"}}};Yl&&(Ql[".cm-line"].caretColor="transparent !important");const xf=fi.highest(O.theme(Ql));function Zl(n){let e=n.scrollDOM.getBoundingClientRect();return{left:(n.textDirection==Z.LTR?e.left:e.right-n.scrollDOM.clientWidth)-n.scrollDOM.scrollLeft,top:e.top-n.scrollDOM.scrollTop}}function to(n,e,t){let i=m.cursor(e);return{from:Math.max(t.from,n.moveToLineBoundary(i,!1,!0).from),to:Math.min(t.to,n.moveToLineBoundary(i,!0,!0).from),type:J.Text}}function io(n,e){let t=n.lineBlockAt(e);if(Array.isArray(t.type)){for(let i of t.type)if(i.to>e||i.to==e&&(i.to==t.to||i.type==J.Text))return i}return t}function kf(n,e){if(e.to<=n.viewport.from||e.from>=n.viewport.to)return[];let t=Math.max(e.from,n.viewport.from),i=Math.min(e.to,n.viewport.to),s=n.textDirection==Z.LTR,r=n.contentDOM,o=r.getBoundingClientRect(),l=Zl(n),h=window.getComputedStyle(r.firstChild),a=o.left+parseInt(h.paddingLeft)+Math.min(0,parseInt(h.textIndent)),c=o.right-parseInt(h.paddingRight),f=io(n,t),u=io(n,i),d=f.type==J.Text?f:null,p=u.type==J.Text?u:null;if(n.lineWrapping&&(d&&(d=to(n,t,d)),p&&(p=to(n,i,p))),d&&p&&d.from==p.from)return y(b(e.from,e.to,d));{let A=d?b(e.from,null,d):v(f,!1),x=p?b(null,e.to,p):v(u,!0),S=[];return(d||f).to<(p||u).from-1?S.push(g(a,A.bottom,c,x.top)):A.bottomL&&G.from=E)break;I>C&&N(Math.max(q,C),A==null&&q<=L,Math.min(I,E),x==null&&I>=W,K.dir)}if(C=R.to+1,C>=E)break}return U.length==0&&N(L,A==null,W,x==null,n.textDirection),{top:D,bottom:B,horizontal:U}}function v(A,x){let S=o.top+(x?A.top:A.bottom);return{top:S,bottom:S,horizontal:[]}}}function Sf(n,e,t){let i=n.coordsAtPos(e.head,e.assoc||1);if(!i)return null;let s=Zl(n);return new _l(i.left-s.left,i.top-s.top,-1,i.bottom-i.top,t?"cm-cursor cm-cursor-primary":"cm-cursor cm-cursor-secondary")}const eh=H.define({map(n,e){return n==null?null:e.mapPos(n)}}),Yt=ke.define({create(){return null},update(n,e){return n!=null&&(n=e.changes.mapPos(n)),e.effects.reduce((t,i)=>i.is(eh)?i.value:t,n)}}),vf=ue.fromClass(class{constructor(n){this.view=n,this.cursor=null,this.measureReq={read:this.readPos.bind(this),write:this.drawCursor.bind(this)}}update(n){var e;let t=n.state.field(Yt);t==null?this.cursor!=null&&((e=this.cursor)===null||e===void 0||e.remove(),this.cursor=null):(this.cursor||(this.cursor=this.view.scrollDOM.appendChild(document.createElement("div")),this.cursor.className="cm-dropCursor"),(n.startState.field(Yt)!=t||n.docChanged||n.geometryChanged)&&this.view.requestMeasure(this.measureReq))}readPos(){let n=this.view.state.field(Yt),e=n!=null&&this.view.coordsAtPos(n);if(!e)return null;let t=this.view.scrollDOM.getBoundingClientRect();return{left:e.left-t.left+this.view.scrollDOM.scrollLeft,top:e.top-t.top+this.view.scrollDOM.scrollTop,height:e.bottom-e.top}}drawCursor(n){this.cursor&&(n?(this.cursor.style.left=n.left+"px",this.cursor.style.top=n.top+"px",this.cursor.style.height=n.height+"px"):this.cursor.style.left="-100000px")}destroy(){this.cursor&&this.cursor.remove()}setDropPos(n){this.view.state.field(Yt)!=n&&this.view.dispatch({effects:eh.of(n)})}},{eventHandlers:{dragover(n){this.setDropPos(this.view.posAtCoords({x:n.clientX,y:n.clientY}))},dragleave(n){(n.target==this.view.contentDOM||!this.view.contentDOM.contains(n.relatedTarget))&&this.setDropPos(null)},dragend(){this.setDropPos(null)},drop(){this.setDropPos(null)}}});function Cf(){return[Yt,vf]}function no(n,e,t,i,s){e.lastIndex=0;for(let r=n.iterRange(t,i),o=t,l;!r.next().done;o+=r.value.length)if(!r.lineBreak)for(;l=e.exec(r.value);)s(o+l.index,o+l.index+l[0].length,l)}function Af(n,e){let t=n.visibleRanges;if(t.length==1&&t[0].from==n.viewport.from&&t[0].to==n.viewport.to)return t;let i=[];for(let{from:s,to:r}of t)s=Math.max(n.state.doc.lineAt(s).from,s-e),r=Math.min(n.state.doc.lineAt(r).to,r+e),i.length&&i[i.length-1].to>=s?i[i.length-1].to=r:i.push({from:s,to:r});return i}class Mf{constructor(e){let{regexp:t,decoration:i,boundary:s,maxLength:r=1e3}=e;if(!t.global)throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");this.regexp=t,this.getDeco=typeof i=="function"?i:()=>i,this.boundary=s,this.maxLength=r}createDeco(e){let t=new gt;for(let{from:i,to:s}of Af(e,this.maxLength))no(e.state.doc,this.regexp,i,s,(r,o,l)=>t.add(r,o,this.getDeco(l,e,r)));return t.finish()}updateDeco(e,t){let i=1e9,s=-1;return e.docChanged&&e.changes.iterChanges((r,o,l,h)=>{h>e.view.viewport.from&&l1e3?this.createDeco(e.view):s>-1?this.updateRange(e.view,t.map(e.changes),i,s):t}updateRange(e,t,i,s){for(let r of e.visibleRanges){let o=Math.max(r.from,i),l=Math.min(r.to,s);if(l>o){let h=e.state.doc.lineAt(o),a=h.toh.from;o--)if(this.boundary.test(h.text[o-1-h.from])){c=o;break}for(;lu.push(this.getDeco(y,e,p).range(p,g)));t=t.update({filterFrom:c,filterTo:f,filter:(p,g)=>pf,add:u})}}return t}}const ws=/x/.unicode!=null?"gu":"g",Df=new RegExp(`[\0-\b +-\x7F-\x9F\xAD\u061C\u200B\u200E\u200F\u2028\u2029\u202D\u202E\uFEFF\uFFF9-\uFFFC]`,ws),Of={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let Bn=null;function Tf(){var n;if(Bn==null&&typeof document!="undefined"&&document.body){let e=document.body.style;Bn=((n=e.tabSize)!==null&&n!==void 0?n:e.MozTabSize)!=null}return Bn||!1}const Ii=T.define({combine(n){let e=Ht(n,{render:null,specialChars:Df,addSpecialChars:null});return(e.replaceTabs=!Tf())&&(e.specialChars=new RegExp(" |"+e.specialChars.source,ws)),e.addSpecialChars&&(e.specialChars=new RegExp(e.specialChars.source+"|"+e.addSpecialChars.source,ws)),e}});function Bf(n={}){return[Ii.of(n),Pf()]}let so=null;function Pf(){return so||(so=ue.fromClass(class{constructor(n){this.view=n,this.decorations=P.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(n.state.facet(Ii)),this.decorations=this.decorator.createDeco(n)}makeDecorator(n){return new Mf({regexp:n.specialChars,decoration:(e,t,i)=>{let{doc:s}=t.state,r=re(e[0],0);if(r==9){let o=s.lineAt(i),l=t.state.tabSize,h=ui(o.text,l,i-o.from);return P.replace({widget:new If((l-h%l)*this.view.defaultCharacterWidth)})}return this.decorationCache[r]||(this.decorationCache[r]=P.replace({widget:new Ef(n,r)}))},boundary:n.replaceTabs?void 0:/[^]/})}update(n){let e=n.state.facet(Ii);n.startState.facet(Ii)!=e?(this.decorator=this.makeDecorator(e),this.decorations=this.decorator.createDeco(n.view)):this.decorations=this.decorator.updateDeco(n,this.decorations)}},{decorations:n=>n.decorations}))}const Rf="\u2022";function Lf(n){return n>=32?Rf:n==10?"\u2424":String.fromCharCode(9216+n)}class Ef extends xt{constructor(e,t){super(),this.options=e,this.code=t}eq(e){return e.code==this.code}toDOM(e){let t=Lf(this.code),i=e.state.phrase("Control character")+" "+(Of[this.code]||"0x"+this.code.toString(16)),s=this.options.render&&this.options.render(this.code,i,t);if(s)return s;let r=document.createElement("span");return r.textContent=t,r.title=i,r.setAttribute("aria-label",i),r.className="cm-specialChar",r}ignoreEvent(){return!1}}class If extends xt{constructor(e){super(),this.width=e}eq(e){return e.width==this.width}toDOM(){let e=document.createElement("span");return e.textContent=" ",e.className="cm-tab",e.style.width=this.width+"px",e}ignoreEvent(){return!1}}class Nf extends xt{constructor(e){super(),this.content=e}toDOM(){let e=document.createElement("span");return e.className="cm-placeholder",e.style.pointerEvents="none",e.appendChild(typeof this.content=="string"?document.createTextNode(this.content):this.content),typeof this.content=="string"?e.setAttribute("aria-label","placeholder "+this.content):e.setAttribute("aria-hidden","true"),e}ignoreEvent(){return!1}}function ro(n){return ue.fromClass(class{constructor(e){this.view=e,this.placeholder=P.set([P.widget({widget:new Nf(n),side:1}).range(0)])}get decorations(){return this.view.state.doc.length?P.none:this.placeholder}},{decorations:e=>e.decorations})}const xs=2e3;function Vf(n,e,t){let i=Math.min(e.line,t.line),s=Math.max(e.line,t.line),r=[];if(e.off>xs||t.off>xs||e.col<0||t.col<0){let o=Math.min(e.off,t.off),l=Math.max(e.off,t.off);for(let h=i;h<=s;h++){let a=n.doc.line(h);a.length<=l&&r.push(m.range(a.from+o,a.to+l))}}else{let o=Math.min(e.col,t.col),l=Math.max(e.col,t.col);for(let h=i;h<=s;h++){let a=n.doc.line(h),c=Qn(a.text,o,n.tabSize,!0);if(c>-1){let f=Qn(a.text,l,n.tabSize);r.push(m.range(a.from+c,a.from+f))}}}return r}function Ff(n,e){let t=n.coordsAtPos(n.viewport.from);return t?Math.round(Math.abs((t.left-e)/n.defaultCharacterWidth)):-1}function oo(n,e){let t=n.posAtCoords({x:e.clientX,y:e.clientY},!1),i=n.state.doc.lineAt(t),s=t-i.from,r=s>xs?-1:s==i.length?Ff(n,e.clientX):ui(i.text,n.state.tabSize,t-i.from);return{line:i.number,col:r,off:s}}function Hf(n,e){let t=oo(n,e),i=n.state.selection;return t?{update(s){if(s.docChanged){let r=s.changes.mapPos(s.startState.doc.line(t.line).from),o=s.state.doc.lineAt(r);t={line:o.number,col:t.col,off:Math.min(t.off,o.length)},i=i.map(s.changes)}},get(s,r,o){let l=oo(n,s);if(!l)return i;let h=Vf(n.state,t,l);return h.length?o?m.create(h.concat(i.ranges)):m.create(h):i}}:null}function Wf(n){let e=(n==null?void 0:n.eventFilter)||(t=>t.altKey&&t.button==0);return O.mouseSelectionStyle.of((t,i)=>e(i)?Hf(t,i):null)}const Pn="-10000px";class zf{constructor(e,t,i){this.facet=t,this.createTooltipView=i,this.input=e.state.facet(t),this.tooltips=this.input.filter(s=>s),this.tooltipViews=this.tooltips.map(i)}update(e){let t=e.state.facet(this.facet),i=t.filter(r=>r);if(t===this.input){for(let r of this.tooltipViews)r.update&&r.update(e);return!1}let s=[];for(let r=0;r{var e,t,i;return{position:M.ios?"absolute":((e=n.find(s=>s.position))===null||e===void 0?void 0:e.position)||"fixed",parent:((t=n.find(s=>s.parent))===null||t===void 0?void 0:t.parent)||null,tooltipSpace:((i=n.find(s=>s.tooltipSpace))===null||i===void 0?void 0:i.tooltipSpace)||qf}}}),th=ue.fromClass(class{constructor(n){var e;this.view=n,this.inView=!0,this.lastTransaction=0,this.measureTimeout=-1;let t=n.state.facet(Rn);this.position=t.position,this.parent=t.parent,this.classes=n.themeClasses,this.createContainer(),this.measureReq={read:this.readMeasure.bind(this),write:this.writeMeasure.bind(this),key:this},this.manager=new zf(n,ih,i=>this.createTooltip(i)),this.intersectionObserver=typeof IntersectionObserver=="function"?new IntersectionObserver(i=>{Date.now()>this.lastTransaction-50&&i.length>0&&i[i.length-1].intersectionRatio<1&&this.measureSoon()},{threshold:[1]}):null,this.observeIntersection(),(e=n.dom.ownerDocument.defaultView)===null||e===void 0||e.addEventListener("resize",this.measureSoon=this.measureSoon.bind(this)),this.maybeMeasure()}createContainer(){this.parent?(this.container=document.createElement("div"),this.container.style.position="relative",this.container.className=this.view.themeClasses,this.parent.appendChild(this.container)):this.container=this.view.dom}observeIntersection(){if(this.intersectionObserver){this.intersectionObserver.disconnect();for(let n of this.manager.tooltipViews)this.intersectionObserver.observe(n.dom)}}measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout(()=>{this.measureTimeout=-1,this.maybeMeasure()},50))}update(n){n.transactions.length&&(this.lastTransaction=Date.now());let e=this.manager.update(n);e&&this.observeIntersection();let t=e||n.geometryChanged,i=n.state.facet(Rn);if(i.position!=this.position){this.position=i.position;for(let s of this.manager.tooltipViews)s.dom.style.position=this.position;t=!0}if(i.parent!=this.parent){this.parent&&this.container.remove(),this.parent=i.parent,this.createContainer();for(let s of this.manager.tooltipViews)this.container.appendChild(s.dom);t=!0}else this.parent&&this.view.themeClasses!=this.classes&&(this.classes=this.container.className=this.view.themeClasses);t&&this.maybeMeasure()}createTooltip(n){let e=n.create(this.view);if(e.dom.classList.add("cm-tooltip"),n.arrow&&!e.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")){let t=document.createElement("div");t.className="cm-tooltip-arrow",e.dom.appendChild(t)}return e.dom.style.position=this.position,e.dom.style.top=Pn,this.container.appendChild(e.dom),e.mount&&e.mount(this.view),e}destroy(){var n,e;(n=this.view.dom.ownerDocument.defaultView)===null||n===void 0||n.removeEventListener("resize",this.measureSoon);for(let{dom:t}of this.manager.tooltipViews)t.remove();(e=this.intersectionObserver)===null||e===void 0||e.disconnect(),clearTimeout(this.measureTimeout)}readMeasure(){let n=this.view.dom.getBoundingClientRect();return{editor:n,parent:this.parent?this.container.getBoundingClientRect():n,pos:this.manager.tooltips.map((e,t)=>{let i=this.manager.tooltipViews[t];return i.getCoords?i.getCoords(e.pos):this.view.coordsAtPos(e.pos)}),size:this.manager.tooltipViews.map(({dom:e})=>e.getBoundingClientRect()),space:this.view.state.facet(Rn).tooltipSpace(this.view)}}writeMeasure(n){let{editor:e,space:t}=n,i=[];for(let s=0;s=Math.min(e.bottom,t.bottom)||h.rightMath.min(e.right,t.right)+.1){l.style.top=Pn;continue}let c=r.arrow?o.dom.querySelector(".cm-tooltip-arrow"):null,f=c?7:0,u=a.right-a.left,d=a.bottom-a.top,p=o.offset||Uf,g=this.view.textDirection==Z.LTR,y=a.width>t.right-t.left?g?t.left:t.right-a.width:g?Math.min(h.left-(c?14:0)+p.x,t.right-u):Math.max(t.left,h.left-u+(c?14:0)-p.x),b=!!r.above;!r.strictSide&&(b?h.top-(a.bottom-a.top)-p.yt.bottom)&&b==t.bottom-h.bottom>h.top-t.top&&(b=!b);let v=b?h.top-d-f-p.y:h.bottom+f+p.y,A=y+u;if(o.overlap!==!0)for(let x of i)x.lefty&&x.topv&&(v=b?x.top-d-2-f:x.bottom+f+2);this.position=="absolute"?(l.style.top=v-n.parent.top+"px",l.style.left=y-n.parent.left+"px"):(l.style.top=v+"px",l.style.left=y+"px"),c&&(c.style.left=`${h.left+(g?p.x:-p.x)-(y+14-7)}px`),o.overlap!==!0&&i.push({left:y,top:v,right:A,bottom:v+d}),l.classList.toggle("cm-tooltip-above",b),l.classList.toggle("cm-tooltip-below",!b),o.positioned&&o.positioned()}}maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this.view.requestMeasure(this.measureReq),this.inView!=this.view.inView&&(this.inView=this.view.inView,!this.inView)))for(let n of this.manager.tooltipViews)n.dom.style.top=Pn}},{eventHandlers:{scroll(){this.maybeMeasure()}}}),Kf=O.baseTheme({".cm-tooltip":{zIndex:100},"&light .cm-tooltip":{border:"1px solid #bbb",backgroundColor:"#f5f5f5"},"&light .cm-tooltip-section:not(:first-child)":{borderTop:"1px solid #bbb"},"&dark .cm-tooltip":{backgroundColor:"#333338",color:"white"},".cm-tooltip-arrow":{height:`${7}px`,width:`${7*2}px`,position:"absolute",zIndex:-1,overflow:"hidden","&:before, &:after":{content:"''",position:"absolute",width:0,height:0,borderLeft:`${7}px solid transparent`,borderRight:`${7}px solid transparent`},".cm-tooltip-above &":{bottom:`-${7}px`,"&:before":{borderTop:`${7}px solid #bbb`},"&:after":{borderTop:`${7}px solid #f5f5f5`,bottom:"1px"}},".cm-tooltip-below &":{top:`-${7}px`,"&:before":{borderBottom:`${7}px solid #bbb`},"&:after":{borderBottom:`${7}px solid #f5f5f5`,top:"1px"}}},"&dark .cm-tooltip .cm-tooltip-arrow":{"&:before":{borderTopColor:"#333338",borderBottomColor:"#333338"},"&:after":{borderTopColor:"transparent",borderBottomColor:"transparent"}}}),Uf={x:0,y:0},ih=T.define({enables:[th,Kf]});function jf(n,e){let t=n.plugin(th);if(!t)return null;let i=t.manager.tooltips.indexOf(e);return i<0?null:t.manager.tooltipViews[i]}const lo=T.define({combine(n){let e,t;for(let i of n)e=e||i.topContainer,t=t||i.bottomContainer;return{topContainer:e,bottomContainer:t}}});function ji(n,e){let t=n.plugin(nh),i=t?t.specs.indexOf(e):-1;return i>-1?t.panels[i]:null}const nh=ue.fromClass(class{constructor(n){this.input=n.state.facet(Gi),this.specs=this.input.filter(t=>t),this.panels=this.specs.map(t=>t(n));let e=n.state.facet(lo);this.top=new Ci(n,!0,e.topContainer),this.bottom=new Ci(n,!1,e.bottomContainer),this.top.sync(this.panels.filter(t=>t.top)),this.bottom.sync(this.panels.filter(t=>!t.top));for(let t of this.panels)t.dom.classList.add("cm-panel"),t.mount&&t.mount()}update(n){let e=n.state.facet(lo);this.top.container!=e.topContainer&&(this.top.sync([]),this.top=new Ci(n.view,!0,e.topContainer)),this.bottom.container!=e.bottomContainer&&(this.bottom.sync([]),this.bottom=new Ci(n.view,!1,e.bottomContainer)),this.top.syncClasses(),this.bottom.syncClasses();let t=n.state.facet(Gi);if(t!=this.input){let i=t.filter(h=>h),s=[],r=[],o=[],l=[];for(let h of i){let a=this.specs.indexOf(h),c;a<0?(c=h(n.view),l.push(c)):(c=this.panels[a],c.update&&c.update(n)),s.push(c),(c.top?r:o).push(c)}this.specs=i,this.panels=s,this.top.sync(r),this.bottom.sync(o);for(let h of l)h.dom.classList.add("cm-panel"),h.mount&&h.mount()}else for(let i of this.panels)i.update&&i.update(n)}destroy(){this.top.sync([]),this.bottom.sync([])}},{provide:n=>O.scrollMargins.of(e=>{let t=e.plugin(n);return t&&{top:t.top.scrollMargin(),bottom:t.bottom.scrollMargin()}})});class Ci{constructor(e,t,i){this.view=e,this.top=t,this.container=i,this.dom=void 0,this.classes="",this.panels=[],this.syncClasses()}sync(e){for(let t of this.panels)t.destroy&&e.indexOf(t)<0&&t.destroy();this.panels=e,this.syncDOM()}syncDOM(){if(this.panels.length==0){this.dom&&(this.dom.remove(),this.dom=void 0);return}if(!this.dom){this.dom=document.createElement("div"),this.dom.className=this.top?"cm-panels cm-panels-top":"cm-panels cm-panels-bottom",this.dom.style[this.top?"top":"bottom"]="0";let t=this.container||this.view.dom;t.insertBefore(this.dom,this.top?t.firstChild:null)}let e=this.dom.firstChild;for(let t of this.panels)if(t.dom.parentNode==this.dom){for(;e!=t.dom;)e=ho(e);e=e.nextSibling}else this.dom.insertBefore(t.dom,e);for(;e;)e=ho(e)}scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?this.dom.getBoundingClientRect().bottom-Math.max(0,this.view.scrollDOM.getBoundingClientRect().top):Math.min(innerHeight,this.view.scrollDOM.getBoundingClientRect().bottom)-this.dom.getBoundingClientRect().top)}syncClasses(){if(!(!this.container||this.classes==this.view.themeClasses)){for(let e of this.classes.split(" "))e&&this.container.classList.remove(e);for(let e of(this.classes=this.view.themeClasses).split(" "))e&&this.container.classList.add(e)}}}function ho(n){let e=n.nextSibling;return n.remove(),e}const Gi=T.define({enables:nh});class yt extends pt{compare(e){return this==e||this.constructor==e.constructor&&this.eq(e)}eq(e){return!1}destroy(e){}}yt.prototype.elementClass="";yt.prototype.toDOM=void 0;yt.prototype.mapMode=me.TrackBefore;yt.prototype.startSide=yt.prototype.endSide=-1;yt.prototype.point=!0;const Gf=T.define(),Jf=new class extends yt{constructor(){super(...arguments),this.elementClass="cm-activeLineGutter"}},$f=Gf.compute(["selection"],n=>{let e=[],t=-1;for(let i of n.selection.ranges)if(i.empty){let s=n.doc.lineAt(i.head).from;s>t&&(t=s,e.push(Jf.range(s)))}return Y.of(e)});function Xf(){return $f}const Yf=1024;let _f=0;class Ln{constructor(e,t){this.from=e,this.to=t}}class F{constructor(e={}){this.id=_f++,this.perNode=!!e.perNode,this.deserialize=e.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")})}add(e){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return typeof e!="function"&&(e=we.match(e)),t=>{let i=e(t);return i===void 0?null:[this,i]}}}F.closedBy=new F({deserialize:n=>n.split(" ")});F.openedBy=new F({deserialize:n=>n.split(" ")});F.group=new F({deserialize:n=>n.split(" ")});F.contextHash=new F({perNode:!0});F.lookAhead=new F({perNode:!0});F.mounted=new F({perNode:!0});const Qf=Object.create(null);class we{constructor(e,t,i,s=0){this.name=e,this.props=t,this.id=i,this.flags=s}static define(e){let t=e.props&&e.props.length?Object.create(null):Qf,i=(e.top?1:0)|(e.skipped?2:0)|(e.error?4:0)|(e.name==null?8:0),s=new we(e.name||"",t,e.id,i);if(e.props){for(let r of e.props)if(Array.isArray(r)||(r=r(s)),r){if(r[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");t[r[0].id]=r[1]}}return s}prop(e){return this.props[e.id]}get isTop(){return(this.flags&1)>0}get isSkipped(){return(this.flags&2)>0}get isError(){return(this.flags&4)>0}get isAnonymous(){return(this.flags&8)>0}is(e){if(typeof e=="string"){if(this.name==e)return!0;let t=this.prop(F.group);return t?t.indexOf(e)>-1:!1}return this.id==e}static match(e){let t=Object.create(null);for(let i in e)for(let s of i.split(" "))t[s]=e[i];return i=>{for(let s=i.prop(F.group),r=-1;r<(s?s.length:0);r++){let o=t[r<0?i.name:s[r]];if(o)return o}}}}we.none=new we("",Object.create(null),0,8);class zs{constructor(e){this.types=e;for(let t=0;t=s&&(o.type.isAnonymous||t(o)!==!1)){if(o.firstChild())continue;l=!0}for(;l&&i&&!o.type.isAnonymous&&i(o),!o.nextSibling();){if(!o.parent())return;l=!0}}}prop(e){return e.perNode?this.props?this.props[e.id]:void 0:this.type.prop(e)}get propValues(){let e=[];if(this.props)for(let t in this.props)e.push([+t,this.props[t]]);return e}balance(e={}){return this.children.length<=8?this:Us(we.none,this.children,this.positions,0,this.children.length,0,this.length,(t,i,s)=>new _(this.type,t,i,s,this.propValues),e.makeTree||((t,i,s)=>new _(we.none,t,i,s)))}static build(e){return eu(e)}}_.empty=new _(we.none,[],[],0);class qs{constructor(e,t){this.buffer=e,this.index=t}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new qs(this.buffer,this.index)}}class kt{constructor(e,t,i){this.buffer=e,this.length=t,this.set=i}get type(){return we.none}toString(){let e=[];for(let t=0;t0));h=o[h+3]);return l}slice(e,t,i,s){let r=this.buffer,o=new Uint16Array(t-e);for(let l=e,h=0;l=e&&te;case 1:return t<=e&&i>e;case 2:return i>e;case 4:return!0}}function rh(n,e){let t=n.childBefore(e);for(;t;){let i=t.lastChild;if(!i||i.to!=t.to)break;i.type.isError&&i.from==i.to?(n=t,t=i.prevSibling):t=i}return n}function Et(n,e,t,i){for(var s;n.from==n.to||(t<1?n.from>=e:n.from>e)||(t>-1?n.to<=e:n.to0?l.length:-1;e!=a;e+=t){let c=l[e],f=h[e]+o.from;if(!!sh(s,i,f,f+c.length)){if(c instanceof kt){if(r&ae.ExcludeBuffers)continue;let u=c.findChild(0,c.buffer.length,t,i-f,s);if(u>-1)return new tt(new Zf(o,c,e,f),null,u)}else if(r&ae.IncludeAnonymous||!c.type.isAnonymous||Ks(c)){let u;if(!(r&ae.IgnoreMounts)&&c.props&&(u=c.prop(F.mounted))&&!u.overlay)return new je(u.tree,f,e,o);let d=new je(c,f,e,o);return r&ae.IncludeAnonymous||!d.type.isAnonymous?d:d.nextChild(t<0?c.children.length-1:0,t,i,s)}}}if(r&ae.IncludeAnonymous||!o.type.isAnonymous||(o.index>=0?e=o.index+t:e=t<0?-1:o._parent._tree.children.length,o=o._parent,!o))return null}}get firstChild(){return this.nextChild(0,1,0,4)}get lastChild(){return this.nextChild(this._tree.children.length-1,-1,0,4)}childAfter(e){return this.nextChild(0,1,e,2)}childBefore(e){return this.nextChild(this._tree.children.length-1,-1,e,-2)}enter(e,t,i=0){let s;if(!(i&ae.IgnoreOverlays)&&(s=this._tree.prop(F.mounted))&&s.overlay){let r=e-this.from;for(let{from:o,to:l}of s.overlay)if((t>0?o<=r:o=r:l>r))return new je(s.tree,s.overlay[0].from+this.from,-1,this)}return this.nextChild(0,1,e,t,i)}nextSignificantParent(){let e=this;for(;e.type.isAnonymous&&e._parent;)e=e._parent;return e}get parent(){return this._parent?this._parent.nextSignificantParent():null}get nextSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index+1,1,0,4):null}get prevSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index-1,-1,0,4):null}cursor(e=0){return new Xi(this,e)}get tree(){return this._tree}toTree(){return this._tree}resolve(e,t=0){return Et(this,e,t,!1)}resolveInner(e,t=0){return Et(this,e,t,!0)}enterUnfinishedNodesBefore(e){return rh(this,e)}getChild(e,t=null,i=null){let s=Ji(this,e,t,i);return s.length?s[0]:null}getChildren(e,t=null,i=null){return Ji(this,e,t,i)}toString(){return this._tree.toString()}get node(){return this}matchContext(e){return $i(this,e)}}function Ji(n,e,t,i){let s=n.cursor(),r=[];if(!s.firstChild())return r;if(t!=null){for(;!s.type.is(t);)if(!s.nextSibling())return r}for(;;){if(i!=null&&s.type.is(i))return r;if(s.type.is(e)&&r.push(s.node),!s.nextSibling())return i==null?r:[]}}function $i(n,e,t=e.length-1){for(let i=n.parent;t>=0;i=i.parent){if(!i)return!1;if(!i.type.isAnonymous){if(e[t]&&e[t]!=i.name)return!1;t--}}return!0}class Zf{constructor(e,t,i,s){this.parent=e,this.buffer=t,this.index=i,this.start=s}}class tt{constructor(e,t,i){this.context=e,this._parent=t,this.index=i,this.type=e.buffer.set.types[e.buffer.buffer[i]]}get name(){return this.type.name}get from(){return this.context.start+this.context.buffer.buffer[this.index+1]}get to(){return this.context.start+this.context.buffer.buffer[this.index+2]}child(e,t,i){let{buffer:s}=this.context,r=s.findChild(this.index+4,s.buffer[this.index+3],e,t-this.context.start,i);return r<0?null:new tt(this.context,this,r)}get firstChild(){return this.child(1,0,4)}get lastChild(){return this.child(-1,0,4)}childAfter(e){return this.child(1,e,2)}childBefore(e){return this.child(-1,e,-2)}enter(e,t,i=0){if(i&ae.ExcludeBuffers)return null;let{buffer:s}=this.context,r=s.findChild(this.index+4,s.buffer[this.index+3],t>0?1:-1,e-this.context.start,t);return r<0?null:new tt(this.context,this,r)}get parent(){return this._parent||this.context.parent.nextSignificantParent()}externalSibling(e){return this._parent?null:this.context.parent.nextChild(this.context.index+e,e,0,4)}get nextSibling(){let{buffer:e}=this.context,t=e.buffer[this.index+3];return t<(this._parent?e.buffer[this._parent.index+3]:e.buffer.length)?new tt(this.context,this._parent,t):this.externalSibling(1)}get prevSibling(){let{buffer:e}=this.context,t=this._parent?this._parent.index+4:0;return this.index==t?this.externalSibling(-1):new tt(this.context,this._parent,e.findChild(t,this.index,-1,0,4))}cursor(e=0){return new Xi(this,e)}get tree(){return null}toTree(){let e=[],t=[],{buffer:i}=this.context,s=this.index+4,r=i.buffer[this.index+3];if(r>s){let o=i.buffer[this.index+1],l=i.buffer[this.index+2];e.push(i.slice(s,r,o,l)),t.push(0)}return new _(this.type,e,t,this.to-this.from)}resolve(e,t=0){return Et(this,e,t,!1)}resolveInner(e,t=0){return Et(this,e,t,!0)}enterUnfinishedNodesBefore(e){return rh(this,e)}toString(){return this.context.buffer.childString(this.index)}getChild(e,t=null,i=null){let s=Ji(this,e,t,i);return s.length?s[0]:null}getChildren(e,t=null,i=null){return Ji(this,e,t,i)}get node(){return this}matchContext(e){return $i(this,e)}}class Xi{constructor(e,t=0){if(this.mode=t,this.buffer=null,this.stack=[],this.index=0,this.bufferNode=null,e instanceof je)this.yieldNode(e);else{this._tree=e.context.parent,this.buffer=e.context;for(let i=e._parent;i;i=i._parent)this.stack.unshift(i.index);this.bufferNode=e,this.yieldBuf(e.index)}}get name(){return this.type.name}yieldNode(e){return e?(this._tree=e,this.type=e.type,this.from=e.from,this.to=e.to,!0):!1}yieldBuf(e,t){this.index=e;let{start:i,buffer:s}=this.buffer;return this.type=t||s.set.types[s.buffer[e]],this.from=i+s.buffer[e+1],this.to=i+s.buffer[e+2],!0}yield(e){return e?e instanceof je?(this.buffer=null,this.yieldNode(e)):(this.buffer=e.context,this.yieldBuf(e.index,e.type)):!1}toString(){return this.buffer?this.buffer.buffer.childString(this.index):this._tree.toString()}enterChild(e,t,i){if(!this.buffer)return this.yield(this._tree.nextChild(e<0?this._tree._tree.children.length-1:0,e,t,i,this.mode));let{buffer:s}=this.buffer,r=s.findChild(this.index+4,s.buffer[this.index+3],e,t-this.buffer.start,i);return r<0?!1:(this.stack.push(this.index),this.yieldBuf(r))}firstChild(){return this.enterChild(1,0,4)}lastChild(){return this.enterChild(-1,0,4)}childAfter(e){return this.enterChild(1,e,2)}childBefore(e){return this.enterChild(-1,e,-2)}enter(e,t,i=this.mode){return this.buffer?i&ae.ExcludeBuffers?!1:this.enterChild(1,e,t):this.yield(this._tree.enter(e,t,i))}parent(){if(!this.buffer)return this.yieldNode(this.mode&ae.IncludeAnonymous?this._tree._parent:this._tree.parent);if(this.stack.length)return this.yieldBuf(this.stack.pop());let e=this.mode&ae.IncludeAnonymous?this.buffer.parent:this.buffer.parent.nextSignificantParent();return this.buffer=null,this.yieldNode(e)}sibling(e){if(!this.buffer)return this._tree._parent?this.yield(this._tree.index<0?null:this._tree._parent.nextChild(this._tree.index+e,e,0,4,this.mode)):!1;let{buffer:t}=this.buffer,i=this.stack.length-1;if(e<0){let s=i<0?0:this.stack[i]+4;if(this.index!=s)return this.yieldBuf(t.findChild(s,this.index,-1,0,4))}else{let s=t.buffer[this.index+3];if(s<(i<0?t.buffer.length:t.buffer[this.stack[i]+3]))return this.yieldBuf(s)}return i<0?this.yield(this.buffer.parent.nextChild(this.buffer.index+e,e,0,4,this.mode)):!1}nextSibling(){return this.sibling(1)}prevSibling(){return this.sibling(-1)}atLastNode(e){let t,i,{buffer:s}=this;if(s){if(e>0){if(this.index-1)for(let r=t+e,o=e<0?-1:i._tree.children.length;r!=o;r+=e){let l=i._tree.children[r];if(this.mode&ae.IncludeAnonymous||l instanceof kt||!l.type.isAnonymous||Ks(l))return!1}return!0}move(e,t){if(t&&this.enterChild(e,0,4))return!0;for(;;){if(this.sibling(e))return!0;if(this.atLastNode(e)||!this.parent())return!1}}next(e=!0){return this.move(1,e)}prev(e=!0){return this.move(-1,e)}moveTo(e,t=0){for(;(this.from==this.to||(t<1?this.from>=e:this.from>e)||(t>-1?this.to<=e:this.to=0;){for(let o=e;o;o=o._parent)if(o.index==s){if(s==this.index)return o;t=o,i=r+1;break e}s=this.stack[--r]}}for(let s=i;s=0;r--){if(r<0)return $i(this.node,e,s);let o=i[t.buffer[this.stack[r]]];if(!o.isAnonymous){if(e[s]&&e[s]!=o.name)return!1;s--}}return!0}}function Ks(n){return n.children.some(e=>e instanceof kt||!e.type.isAnonymous||Ks(e))}function eu(n){var e;let{buffer:t,nodeSet:i,maxBufferLength:s=Yf,reused:r=[],minRepeatType:o=i.types.length}=n,l=Array.isArray(t)?new qs(t,t.length):t,h=i.types,a=0,c=0;function f(x,S,D,B,U){let{id:N,start:L,end:W,size:G}=l,C=c;for(;G<0;)if(l.next(),G==-1){let I=r[N];D.push(I),B.push(L-x);return}else if(G==-3){a=N;return}else if(G==-4){c=N;return}else throw new RangeError(`Unrecognized record size: ${G}`);let E=h[N],R,K,q=L-x;if(W-L<=s&&(K=g(l.pos-S,U))){let I=new Uint16Array(K.size-K.skip),ne=l.pos-K.size,de=I.length;for(;l.pos>ne;)de=y(K.start,I,de);R=new kt(I,W-K.start,i),q=K.start-x}else{let I=l.pos-G;l.next();let ne=[],de=[],ht=N>=o?N:-1,St=0,gi=W;for(;l.pos>I;)ht>=0&&l.id==ht&&l.size>=0?(l.end<=gi-s&&(d(ne,de,L,St,l.end,gi,ht,C),St=ne.length,gi=l.end),l.next()):f(L,I,ne,de,ht);if(ht>=0&&St>0&&St-1&&St>0){let lr=u(E);R=Us(E,ne,de,0,ne.length,0,W-L,lr,lr)}else R=p(E,ne,de,W-L,C-W)}D.push(R),B.push(q)}function u(x){return(S,D,B)=>{let U=0,N=S.length-1,L,W;if(N>=0&&(L=S[N])instanceof _){if(!N&&L.type==x&&L.length==B)return L;(W=L.prop(F.lookAhead))&&(U=D[N]+L.length+W)}return p(x,S,D,B,U)}}function d(x,S,D,B,U,N,L,W){let G=[],C=[];for(;x.length>B;)G.push(x.pop()),C.push(S.pop()+D-U);x.push(p(i.types[L],G,C,N-U,W-N)),S.push(U-D)}function p(x,S,D,B,U=0,N){if(a){let L=[F.contextHash,a];N=N?[L].concat(N):[L]}if(U>25){let L=[F.lookAhead,U];N=N?[L].concat(N):[L]}return new _(x,S,D,B,N)}function g(x,S){let D=l.fork(),B=0,U=0,N=0,L=D.end-s,W={size:0,start:0,skip:0};e:for(let G=D.pos-x;D.pos>G;){let C=D.size;if(D.id==S&&C>=0){W.size=B,W.start=U,W.skip=N,N+=4,B+=4,D.next();continue}let E=D.pos-C;if(C<0||E=o?4:0,K=D.start;for(D.next();D.pos>E;){if(D.size<0)if(D.size==-3)R+=4;else break e;else D.id>=o&&(R+=4);D.next()}U=K,B+=C,N+=R}return(S<0||B==x)&&(W.size=B,W.start=U,W.skip=N),W.size>4?W:void 0}function y(x,S,D){let{id:B,start:U,end:N,size:L}=l;if(l.next(),L>=0&&B4){let G=l.pos-(L-4);for(;l.pos>G;)D=y(x,S,D)}S[--D]=W,S[--D]=N-x,S[--D]=U-x,S[--D]=B}else L==-3?a=B:L==-4&&(c=B);return D}let b=[],v=[];for(;l.pos>0;)f(n.start||0,n.bufferStart||0,b,v,-1);let A=(e=n.length)!==null&&e!==void 0?e:b.length?v[0]+b[0].length:0;return new _(h[n.topID],b.reverse(),v.reverse(),A)}const co=new WeakMap;function Ni(n,e){if(!n.isAnonymous||e instanceof kt||e.type!=n)return 1;let t=co.get(e);if(t==null){t=1;for(let i of e.children){if(i.type!=n||!(i instanceof _)){t=1;break}t+=Ni(n,i)}co.set(e,t)}return t}function Us(n,e,t,i,s,r,o,l,h){let a=0;for(let p=i;p=c)break;D+=B}if(A==x+1){if(D>c){let B=p[x];d(B.children,B.positions,0,B.children.length,g[x]+v);continue}f.push(p[x])}else{let B=g[A-1]+p[A-1].length-S;f.push(Us(n,p,g,x,A,S,B,null,h))}u.push(S+v-r)}}return d(e,t,i,s,0),(l||h)(f,u,o)}class dt{constructor(e,t,i,s,r=!1,o=!1){this.from=e,this.to=t,this.tree=i,this.offset=s,this.open=(r?1:0)|(o?2:0)}get openStart(){return(this.open&1)>0}get openEnd(){return(this.open&2)>0}static addTree(e,t=[],i=!1){let s=[new dt(0,e.length,e,0,!1,i)];for(let r of t)r.to>e.length&&s.push(r);return s}static applyChanges(e,t,i=128){if(!t.length)return e;let s=[],r=1,o=e.length?e[0]:null;for(let l=0,h=0,a=0;;l++){let c=l=i)for(;o&&o.from=u.from||f<=u.to||a){let d=Math.max(u.from,h)-a,p=Math.min(u.to,f)-a;u=d>=p?null:new dt(d,p,u.tree,u.offset+a,l>0,!!c)}if(u&&s.push(u),o.to>f)break;o=rnew Ln(s.from,s.to)):[new Ln(0,0)]:[new Ln(0,e.length)],this.createParse(e,t||[],i)}parse(e,t,i){let s=this.startParse(e,t,i);for(;;){let r=s.advance();if(r)return r}}}class tu{constructor(e){this.string=e}get length(){return this.string.length}chunk(e){return this.string.slice(e)}get lineChunks(){return!1}read(e,t){return this.string.slice(e,t)}}new F({perNode:!0});let iu=0;class Ne{constructor(e,t,i){this.set=e,this.base=t,this.modified=i,this.id=iu++}static define(e){if(e!=null&&e.base)throw new Error("Can not derive from a modified tag");let t=new Ne([],null,[]);if(t.set.push(t),e)for(let i of e.set)t.set.push(i);return t}static defineModifier(){let e=new Yi;return t=>t.modified.indexOf(e)>-1?t:Yi.get(t.base||t,t.modified.concat(e).sort((i,s)=>i.id-s.id))}}let nu=0;class Yi{constructor(){this.instances=[],this.id=nu++}static get(e,t){if(!t.length)return e;let i=t[0].instances.find(l=>l.base==e&&su(t,l.modified));if(i)return i;let s=[],r=new Ne(s,e,t);for(let l of t)l.instances.push(r);let o=lh(t);for(let l of e.set)for(let h of o)s.push(Yi.get(l,h));return r}}function su(n,e){return n.length==e.length&&n.every((t,i)=>t==e[i])}function lh(n){let e=[n];for(let t=0;t0&&f+3==s.length){o=1;break}let u=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(l);if(!u)throw new RangeError("Invalid path: "+s);if(r.push(u[0]=="*"?"":u[0][0]=='"'?JSON.parse(u[0]):u[0]),f+=u[0].length,f==s.length)break;let d=s[f++];if(f==s.length&&d=="!"){o=0;break}if(d!="/")throw new RangeError("Invalid path: "+s);l=s.slice(f)}let h=r.length-1,a=r[h];if(!a)throw new RangeError("Invalid path: "+s);let c=new ou(i,o,h>0?r.slice(0,h):null);e[a]=c.sort(e[a])}}return hh.add(e)}const hh=new F;class ou{constructor(e,t,i,s){this.tags=e,this.mode=t,this.context=i,this.next=s}sort(e){return!e||e.depth{let o=s;for(let l of r)for(let h of l.set){let a=t[h.id];if(a){o=o?o+" "+a:a;break}}return o},scope:i}}function lu(n,e){let t=null;for(let i of n){let s=i.style(e);s&&(t=t?t+" "+s:s)}return t}function hu(n,e,t,i=0,s=n.length){let r=new au(i,Array.isArray(e)?e:[e],t);r.highlightRange(n.cursor(),i,s,"",r.highlighters),r.flush(s)}class au{constructor(e,t,i){this.at=e,this.highlighters=t,this.span=i,this.class=""}startSpan(e,t){t!=this.class&&(this.flush(e),e>this.at&&(this.at=e),this.class=t)}flush(e){e>this.at&&this.class&&this.span(this.at,e,this.class)}highlightRange(e,t,i,s,r){let{type:o,from:l,to:h}=e;if(l>=i||h<=t)return;o.isTop&&(r=this.highlighters.filter(d=>!d.scope||d.scope(o)));let a=s,c=o.prop(hh),f=!1;for(;c;){if(!c.context||e.matchContext(c.context)){let d=lu(r,c.tags);d&&(a&&(a+=" "),a+=d,c.mode==1?s+=(s?" ":"")+d:c.mode==0&&(f=!0));break}c=c.next}if(this.startSpan(e.from,a),f)return;let u=e.tree&&e.tree.prop(F.mounted);if(u&&u.overlay){let d=e.node.enter(u.overlay[0].from+l,1),p=this.highlighters.filter(y=>!y.scope||y.scope(u.tree.type)),g=e.firstChild();for(let y=0,b=l;;y++){let v=y=A||!e.nextSibling())););if(!v||A>i)break;b=v.to+l,b>t&&(this.highlightRange(d.cursor(),Math.max(t,v.from+l),Math.min(i,b),s,p),this.startSpan(b,a))}g&&e.parent()}else if(e.firstChild()){do if(!(e.to<=t)){if(e.from>=i)break;this.highlightRange(e,t,i,s,r),this.startSpan(Math.min(i,e.to),a)}while(e.nextSibling());e.parent()}}}const w=Ne.define,Mi=w(),Je=w(),fo=w(Je),uo=w(Je),$e=w(),Di=w($e),En=w($e),Ie=w(),at=w(Ie),Le=w(),Ee=w(),ks=w(),Kt=w(ks),Oi=w(),k={comment:Mi,lineComment:w(Mi),blockComment:w(Mi),docComment:w(Mi),name:Je,variableName:w(Je),typeName:fo,tagName:w(fo),propertyName:uo,attributeName:w(uo),className:w(Je),labelName:w(Je),namespace:w(Je),macroName:w(Je),literal:$e,string:Di,docString:w(Di),character:w(Di),attributeValue:w(Di),number:En,integer:w(En),float:w(En),bool:w($e),regexp:w($e),escape:w($e),color:w($e),url:w($e),keyword:Le,self:w(Le),null:w(Le),atom:w(Le),unit:w(Le),modifier:w(Le),operatorKeyword:w(Le),controlKeyword:w(Le),definitionKeyword:w(Le),moduleKeyword:w(Le),operator:Ee,derefOperator:w(Ee),arithmeticOperator:w(Ee),logicOperator:w(Ee),bitwiseOperator:w(Ee),compareOperator:w(Ee),updateOperator:w(Ee),definitionOperator:w(Ee),typeOperator:w(Ee),controlOperator:w(Ee),punctuation:ks,separator:w(ks),bracket:Kt,angleBracket:w(Kt),squareBracket:w(Kt),paren:w(Kt),brace:w(Kt),content:Ie,heading:at,heading1:w(at),heading2:w(at),heading3:w(at),heading4:w(at),heading5:w(at),heading6:w(at),contentSeparator:w(Ie),list:w(Ie),quote:w(Ie),emphasis:w(Ie),strong:w(Ie),link:w(Ie),monospace:w(Ie),strikethrough:w(Ie),inserted:w(),deleted:w(),changed:w(),invalid:w(),meta:Oi,documentMeta:w(Oi),annotation:w(Oi),processingInstruction:w(Oi),definition:Ne.defineModifier(),constant:Ne.defineModifier(),function:Ne.defineModifier(),standard:Ne.defineModifier(),local:Ne.defineModifier(),special:Ne.defineModifier()};ah([{tag:k.link,class:"tok-link"},{tag:k.heading,class:"tok-heading"},{tag:k.emphasis,class:"tok-emphasis"},{tag:k.strong,class:"tok-strong"},{tag:k.keyword,class:"tok-keyword"},{tag:k.atom,class:"tok-atom"},{tag:k.bool,class:"tok-bool"},{tag:k.url,class:"tok-url"},{tag:k.labelName,class:"tok-labelName"},{tag:k.inserted,class:"tok-inserted"},{tag:k.deleted,class:"tok-deleted"},{tag:k.literal,class:"tok-literal"},{tag:k.string,class:"tok-string"},{tag:k.number,class:"tok-number"},{tag:[k.regexp,k.escape,k.special(k.string)],class:"tok-string2"},{tag:k.variableName,class:"tok-variableName"},{tag:k.local(k.variableName),class:"tok-variableName tok-local"},{tag:k.definition(k.variableName),class:"tok-variableName tok-definition"},{tag:k.special(k.variableName),class:"tok-variableName2"},{tag:k.definition(k.propertyName),class:"tok-propertyName tok-definition"},{tag:k.typeName,class:"tok-typeName"},{tag:k.namespace,class:"tok-namespace"},{tag:k.className,class:"tok-className"},{tag:k.macroName,class:"tok-macroName"},{tag:k.propertyName,class:"tok-propertyName"},{tag:k.operator,class:"tok-operator"},{tag:k.comment,class:"tok-comment"},{tag:k.meta,class:"tok-meta"},{tag:k.invalid,class:"tok-invalid"},{tag:k.punctuation,class:"tok-punctuation"}]);var In;const hi=new F;function cu(n){return T.define({combine:n?e=>e.concat(n):void 0})}class Be{constructor(e,t,i=[]){this.data=e,V.prototype.hasOwnProperty("tree")||Object.defineProperty(V.prototype,"tree",{get(){return xe(this)}}),this.parser=t,this.extension=[Vt.of(this),V.languageData.of((s,r,o)=>s.facet(po(s,r,o)))].concat(i)}isActiveAt(e,t,i=-1){return po(e,t,i)==this.data}findRegions(e){let t=e.facet(Vt);if((t==null?void 0:t.data)==this.data)return[{from:0,to:e.doc.length}];if(!t||!t.allowsNesting)return[];let i=[],s=(r,o)=>{if(r.prop(hi)==this.data){i.push({from:o,to:o+r.length});return}let l=r.prop(F.mounted);if(l){if(l.tree.prop(hi)==this.data){if(l.overlay)for(let h of l.overlay)i.push({from:h.from+o,to:h.to+o});else i.push({from:o,to:o+r.length});return}else if(l.overlay){let h=i.length;if(s(l.tree,l.overlay[0].from+o),i.length>h)return}}for(let h=0;h=this.cursorPos?this.doc.sliceString(e,t):this.string.slice(e-i,t-i)}}let Ut=null;class It{constructor(e,t,i=[],s,r,o,l,h){this.parser=e,this.state=t,this.fragments=i,this.tree=s,this.treeLen=r,this.viewport=o,this.skipped=l,this.scheduleOn=h,this.parse=null,this.tempSkipped=[]}static create(e,t,i){return new It(e,t,[],_.empty,0,i,[],null)}startParse(){return this.parser.startParse(new fu(this.state.doc),this.fragments)}work(e,t){return t!=null&&t>=this.state.doc.length&&(t=void 0),this.tree!=_.empty&&this.isDone(t!=null?t:this.state.doc.length)?(this.takeTree(),!0):this.withContext(()=>{var i;if(typeof e=="number"){let s=Date.now()+e;e=()=>Date.now()>s}for(this.parse||(this.parse=this.startParse()),t!=null&&(this.parse.stoppedAt==null||this.parse.stoppedAt>t)&&t=this.treeLen&&((this.parse.stoppedAt==null||this.parse.stoppedAt>e)&&this.parse.stopAt(e),this.withContext(()=>{for(;!(t=this.parse.advance()););}),this.treeLen=e,this.tree=t,this.fragments=this.withoutTempSkipped(dt.addTree(this.tree,this.fragments,!0)),this.parse=null)}withContext(e){let t=Ut;Ut=this;try{return e()}finally{Ut=t}}withoutTempSkipped(e){for(let t;t=this.tempSkipped.pop();)e=go(e,t.from,t.to);return e}changes(e,t){let{fragments:i,tree:s,treeLen:r,viewport:o,skipped:l}=this;if(this.takeTree(),!e.empty){let h=[];if(e.iterChangedRanges((a,c,f,u)=>h.push({fromA:a,toA:c,fromB:f,toB:u})),i=dt.applyChanges(i,h),s=_.empty,r=0,o={from:e.mapPos(o.from,-1),to:e.mapPos(o.to,1)},this.skipped.length){l=[];for(let a of this.skipped){let c=e.mapPos(a.from,1),f=e.mapPos(a.to,-1);ce.from&&(this.fragments=go(this.fragments,s,r),this.skipped.splice(i--,1))}return this.skipped.length>=t?!1:(this.reset(),!0)}reset(){this.parse&&(this.takeTree(),this.parse=null)}skipUntilInView(e,t){this.skipped.push({from:e,to:t})}static getSkippingParser(e){return new class extends oh{createParse(t,i,s){let r=s[0].from,o=s[s.length-1].to;return{parsedPos:r,advance(){let h=Ut;if(h){for(let a of s)h.tempSkipped.push(a);e&&(h.scheduleOn=h.scheduleOn?Promise.all([h.scheduleOn,e]):e)}return this.parsedPos=o,new _(we.none,[],[],o-r)},stoppedAt:null,stopAt(){}}}}}isDone(e){e=Math.min(e,this.state.doc.length);let t=this.fragments;return this.treeLen>=e&&t.length&&t[0].from==0&&t[0].to>=e}static get(){return Ut}}function go(n,e,t){return dt.applyChanges(n,[{fromA:e,toA:t,fromB:e,toB:t}])}class Nt{constructor(e){this.context=e,this.tree=e.tree}apply(e){if(!e.docChanged&&this.tree==this.context.tree)return this;let t=this.context.changes(e.changes,e.state),i=this.context.treeLen==e.startState.doc.length?void 0:Math.max(e.changes.mapPos(this.context.treeLen),t.viewport.to);return t.work(20,i)||t.takeTree(),new Nt(t)}static init(e){let t=Math.min(3e3,e.doc.length),i=It.create(e.facet(Vt).parser,e,{from:0,to:t});return i.work(20,t)||i.takeTree(),new Nt(i)}}Be.state=ke.define({create:Nt.init,update(n,e){for(let t of e.effects)if(t.is(Be.setState))return t.value;return e.startState.facet(Vt)!=e.state.facet(Vt)?Nt.init(e.state):n.apply(e)}});let ch=n=>{let e=setTimeout(()=>n(),500);return()=>clearTimeout(e)};typeof requestIdleCallback!="undefined"&&(ch=n=>{let e=-1,t=setTimeout(()=>{e=requestIdleCallback(n,{timeout:500-100})},100);return()=>e<0?clearTimeout(t):cancelIdleCallback(e)});const Nn=typeof navigator!="undefined"&&((In=navigator.scheduling)===null||In===void 0?void 0:In.isInputPending)?()=>navigator.scheduling.isInputPending():null,uu=ue.fromClass(class{constructor(e){this.view=e,this.working=null,this.workScheduled=0,this.chunkEnd=-1,this.chunkBudget=-1,this.work=this.work.bind(this),this.scheduleWork()}update(e){let t=this.view.state.field(Be.state).context;(t.updateViewport(e.view.viewport)||this.view.viewport.to>t.treeLen)&&this.scheduleWork(),e.docChanged&&(this.view.hasFocus&&(this.chunkBudget+=50),this.scheduleWork()),this.checkAsyncSchedule(t)}scheduleWork(){if(this.working)return;let{state:e}=this.view,t=e.field(Be.state);(t.tree!=t.context.tree||!t.context.isDone(e.doc.length))&&(this.working=ch(this.work))}work(e){this.working=null;let t=Date.now();if(this.chunkEnds+1e3,h=r.context.work(()=>Nn&&Nn()||Date.now()>o,s+(l?0:1e5));this.chunkBudget-=Date.now()-t,(h||this.chunkBudget<=0)&&(r.context.takeTree(),this.view.dispatch({effects:Be.setState.of(new Nt(r.context))})),this.chunkBudget>0&&!(h&&!l)&&this.scheduleWork(),this.checkAsyncSchedule(r.context)}checkAsyncSchedule(e){e.scheduleOn&&(this.workScheduled++,e.scheduleOn.then(()=>this.scheduleWork()).catch(t=>Pe(this.view.state,t)).then(()=>this.workScheduled--),e.scheduleOn=null)}destroy(){this.working&&this.working()}isWorking(){return!!(this.working||this.workScheduled>0)}},{eventHandlers:{focus(){this.scheduleWork()}}}),Vt=T.define({combine(n){return n.length?n[0]:null},enables:[Be.state,uu]}),fh=T.define(),js=T.define({combine:n=>{if(!n.length)return" ";if(!/^(?: +|\t+)$/.test(n[0]))throw new Error("Invalid indent unit: "+JSON.stringify(n[0]));return n[0]}});function bt(n){let e=n.facet(js);return e.charCodeAt(0)==9?n.tabSize*e.length:e.length}function _i(n,e){let t="",i=n.tabSize;if(n.facet(js).charCodeAt(0)==9)for(;e>=i;)t+=" ",e-=i;for(let s=0;s=i.from&&s<=i.to?r&&s==e?{text:"",from:e}:(t<0?s-1&&(r+=o-this.countColumn(i,i.search(/\S|$/))),r}countColumn(e,t=e.length){return ui(e,this.state.tabSize,t)}lineIndent(e,t=1){let{text:i,from:s}=this.lineAt(e,t),r=this.options.overrideIndentation;if(r){let o=r(s);if(o>-1)return o}return this.countColumn(i,i.search(/\S|$/))}get simulatedBreak(){return this.options.simulateBreak||null}}const du=new F;function pu(n,e,t){return dh(e.resolveInner(t).enterUnfinishedNodesBefore(t),t,n)}function gu(n){return n.pos==n.options.simulateBreak&&n.options.simulateDoubleBreak}function mu(n){let e=n.type.prop(du);if(e)return e;let t=n.firstChild,i;if(t&&(i=t.type.prop(F.closedBy))){let s=n.lastChild,r=s&&i.indexOf(s.name)>-1;return o=>xu(o,!0,1,void 0,r&&!gu(o)?s.from:void 0)}return n.parent==null?yu:null}function dh(n,e,t){for(;n;n=n.parent){let i=mu(n);if(i)return i(Gs.create(t,e,n))}return null}function yu(){return 0}class Gs extends cn{constructor(e,t,i){super(e.state,e.options),this.base=e,this.pos=t,this.node=i}static create(e,t,i){return new Gs(e,t,i)}get textAfter(){return this.textAfterPos(this.pos)}get baseIndent(){let e=this.state.doc.lineAt(this.node.from);for(;;){let t=this.node.resolve(e.from);for(;t.parent&&t.parent.from==t.from;)t=t.parent;if(bu(t,this.node))break;e=this.state.doc.lineAt(t.from)}return this.lineIndent(e.from)}continue(){let e=this.node.parent;return e?dh(e,this.pos,this.base):0}}function bu(n,e){for(let t=e;t;t=t.parent)if(n==t)return!0;return!1}function wu(n){let e=n.node,t=e.childAfter(e.from),i=e.lastChild;if(!t)return null;let s=n.options.simulateBreak,r=n.state.doc.lineAt(t.from),o=s==null||s<=r.from?r.to:Math.min(r.to,s);for(let l=t.to;;){let h=e.childAfter(l);if(!h||h==i)return null;if(!h.type.isSkipped)return h.froml.prop(hi)==o.data:o?l=>l==o:void 0,this.style=ah(e.map(l=>({tag:l.tag,class:l.class||s(Object.assign({},l,{tag:null}))})),{all:r}).style,this.module=i?new st(i):null,this.themeType=t.themeType}static define(e,t){return new fn(e,t||{})}}const Ss=T.define(),ph=T.define({combine(n){return n.length?[n[0]]:null}});function Vn(n){let e=n.facet(Ss);return e.length?e:n.facet(ph)}function ku(n,e){let t=[vu],i;return n instanceof fn&&(n.module&&t.push(O.styleModule.of(n.module)),i=n.themeType),e!=null&&e.fallback?t.push(ph.of(n)):i?t.push(Ss.computeN([O.darkTheme],s=>s.facet(O.darkTheme)==(i=="dark")?[n]:[])):t.push(Ss.of(n)),t}class Su{constructor(e){this.markCache=Object.create(null),this.tree=xe(e.state),this.decorations=this.buildDeco(e,Vn(e.state))}update(e){let t=xe(e.state),i=Vn(e.state),s=i!=Vn(e.startState);t.length{i.add(o,l,this.markCache[h]||(this.markCache[h]=P.mark({class:h})))},s,r);return i.finish()}}const vu=fi.high(ue.fromClass(Su,{decorations:n=>n.decorations})),Cu=fn.define([{tag:k.meta,color:"#7a757a"},{tag:k.link,textDecoration:"underline"},{tag:k.heading,textDecoration:"underline",fontWeight:"bold"},{tag:k.emphasis,fontStyle:"italic"},{tag:k.strong,fontWeight:"bold"},{tag:k.strikethrough,textDecoration:"line-through"},{tag:k.keyword,color:"#708"},{tag:[k.atom,k.bool,k.url,k.contentSeparator,k.labelName],color:"#219"},{tag:[k.literal,k.inserted],color:"#164"},{tag:[k.string,k.deleted],color:"#a11"},{tag:[k.regexp,k.escape,k.special(k.string)],color:"#e40"},{tag:k.definition(k.variableName),color:"#00f"},{tag:k.local(k.variableName),color:"#30a"},{tag:[k.typeName,k.namespace],color:"#085"},{tag:k.className,color:"#167"},{tag:[k.special(k.variableName),k.macroName],color:"#256"},{tag:k.definition(k.propertyName),color:"#00c"},{tag:k.comment,color:"#940"},{tag:k.invalid,color:"#f00"}]),Au=O.baseTheme({"&.cm-focused .cm-matchingBracket":{backgroundColor:"#328c8252"},"&.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bb555544"}}),gh=1e4,mh="()[]{}",yh=T.define({combine(n){return Ht(n,{afterCursor:!0,brackets:mh,maxScanDistance:gh,renderMatch:Ou})}}),Mu=P.mark({class:"cm-matchingBracket"}),Du=P.mark({class:"cm-nonmatchingBracket"});function Ou(n){let e=[],t=n.matched?Mu:Du;return e.push(t.range(n.start.from,n.start.to)),n.end&&e.push(t.range(n.end.from,n.end.to)),e}const Tu=ke.define({create(){return P.none},update(n,e){if(!e.docChanged&&!e.selection)return n;let t=[],i=e.state.facet(yh);for(let s of e.state.selection.ranges){if(!s.empty)continue;let r=Fe(e.state,s.head,-1,i)||s.head>0&&Fe(e.state,s.head-1,1,i)||i.afterCursor&&(Fe(e.state,s.head,1,i)||s.headO.decorations.from(n)}),Bu=[Tu,Au];function Pu(n={}){return[yh.of(n),Bu]}function vs(n,e,t){let i=n.prop(e<0?F.openedBy:F.closedBy);if(i)return i;if(n.name.length==1){let s=t.indexOf(n.name);if(s>-1&&s%2==(e<0?1:0))return[t[s+e]]}return null}function Fe(n,e,t,i={}){let s=i.maxScanDistance||gh,r=i.brackets||mh,o=xe(n),l=o.resolveInner(e,t);for(let h=l;h;h=h.parent){let a=vs(h.type,t,r);if(a&&h.from=i.to){if(h==0&&s.indexOf(a.type.name)>-1&&a.from0)return null;let a={from:t<0?e-1:e,to:t>0?e+1:e},c=n.doc.iterRange(e,t>0?n.doc.length:0),f=0;for(let u=0;!c.next().done&&u<=r;){let d=c.value;t<0&&(u+=d.length);let p=e+u*t;for(let g=t>0?0:d.length-1,y=t>0?d.length:-1;g!=y;g+=t){let b=o.indexOf(d[g]);if(!(b<0||i.resolveInner(p+g,1).type!=s))if(b%2==0==t>0)f++;else{if(f==1)return{start:a,end:{from:p+g,to:p+g+1},matched:b>>1==h>>1};f--}}t>0&&(u+=d.length)}return c.done?{start:a,matched:!1}:null}function mo(n,e,t,i=0,s=0){e==null&&(e=n.search(/[^\s\u00a0]/),e==-1&&(e=n.length));let r=s;for(let o=i;o=this.string.length}sol(){return this.pos==0}peek(){return this.string.charAt(this.pos)||void 0}next(){if(this.post}eatSpace(){let e=this.pos;for(;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e}skipToEnd(){this.pos=this.string.length}skipTo(e){let t=this.string.indexOf(e,this.pos);if(t>-1)return this.pos=t,!0}backUp(e){this.pos-=e}column(){return this.lastColumnPosi?o.toLowerCase():o,r=this.string.substr(this.pos,e.length);return s(r)==s(e)?(t!==!1&&(this.pos+=e.length),!0):null}else{let s=this.string.slice(this.pos).match(e);return s&&s.index>0?null:(s&&t!==!1&&(this.pos+=s[0].length),s)}}current(){return this.string.slice(this.start,this.pos)}}function Eu(n){return{token:n.token,blankLine:n.blankLine||(()=>{}),startState:n.startState||(()=>!0),copyState:n.copyState||Iu,indent:n.indent||(()=>null),languageData:n.languageData||{},tokenTable:n.tokenTable||Xs}}function Iu(n){if(typeof n!="object")return n;let e={};for(let t in n){let i=n[t];e[t]=i instanceof Array?i.slice():i}return e}class Js extends Be{constructor(e){let t=cu(e.languageData),i=Eu(e),s,r=new class extends oh{createParse(o,l,h){return new Vu(s,o,l,h)}};super(t,r,[fh.of((o,l)=>this.getIndent(o,l))]),this.topNode=Wu(t),s=this,this.streamParser=i,this.stateAfter=new F({perNode:!0}),this.tokenTable=e.tokenTable?new Sh(i.tokenTable):Hu}static define(e){return new Js(e)}getIndent(e,t){let i=xe(e.state),s=i.resolve(t);for(;s&&s.type!=this.topNode;)s=s.parent;if(!s)return null;let r=$s(this,i,0,s.from,t),o,l;if(r?(l=r.state,o=r.pos+1):(l=this.streamParser.startState(e.unit),o=0),t-o>1e4)return null;for(;o=i&&t+e.length<=s&&e.prop(n.stateAfter);if(r)return{state:n.streamParser.copyState(r),pos:t+e.length};for(let o=e.children.length-1;o>=0;o--){let l=e.children[o],h=t+e.positions[o],a=l instanceof _&&h=e.length)return e;!s&&e.type==n.topNode&&(s=!0);for(let r=e.children.length-1;r>=0;r--){let o=e.positions[r],l=e.children[r],h;if(ot&&$s(n,s.tree,0-s.offset,t,o),h;if(l&&(h=wh(n,s.tree,t+s.offset,l.pos+s.offset,!1)))return{state:l.state,tree:h}}return{state:n.streamParser.startState(i?bt(i):4),tree:_.empty}}class Vu{constructor(e,t,i,s){this.lang=e,this.input=t,this.fragments=i,this.ranges=s,this.stoppedAt=null,this.chunks=[],this.chunkPos=[],this.chunk=[],this.chunkReused=void 0,this.rangeIndex=0,this.to=s[s.length-1].to;let r=It.get(),o=s[0].from,{state:l,tree:h}=Nu(e,i,o,r==null?void 0:r.state);this.state=l,this.parsedPos=this.chunkStart=o+h.length;for(let a=0;a=t?this.finish():e&&this.parsedPos>=e.viewport.to?(e.skipUntilInView(this.parsedPos,t),this.finish()):null}stopAt(e){this.stoppedAt=e}lineAfter(e){let t=this.input.chunk(e);if(this.input.lineChunks)t==` +`&&(t="");else{let i=t.indexOf(` +`);i>-1&&(t=t.slice(0,i))}return e+t.length<=this.to?t:t.slice(0,this.to-e)}nextLine(){let e=this.parsedPos,t=this.lineAfter(e),i=e+t.length;for(let s=this.rangeIndex;;){let r=this.ranges[s].to;if(r>=i||(t=t.slice(0,r-(i-t.length)),s++,s==this.ranges.length))break;let o=this.ranges[s].from,l=this.lineAfter(o);t+=l,i=o+l.length}return{line:t,end:i}}skipGapsTo(e,t,i){for(;;){let s=this.ranges[this.rangeIndex].to,r=e+t;if(i>0?s>r:s>=r)break;t+=this.ranges[++this.rangeIndex].from-s}return t}moveRangeIndex(){for(;this.ranges[this.rangeIndex].to1){r=this.skipGapsTo(t,r,1),t+=r;let o=this.chunk.length;r=this.skipGapsTo(i,r,-1),i+=r,s+=this.chunk.length-o}return this.chunk.push(e,t,i,s),r}parseLine(e){let{line:t,end:i}=this.nextLine(),s=0,{streamParser:r}=this.lang,o=new bh(t,e?e.state.tabSize:4,e?bt(e.state):2);if(o.eol())r.blankLine(this.state,o.indentUnit);else for(;!o.eol();){let l=xh(r.token,o,this.state);if(l&&(s=this.emitToken(this.lang.tokenTable.resolve(l),this.parsedPos+o.start,this.parsedPos+o.pos,4,s)),o.start>1e4)break}this.parsedPos=i,this.moveRangeIndex(),this.parsedPose.start)return s}throw new Error("Stream parser failed to advance stream.")}const Xs=Object.create(null),ai=[we.none],Fu=new zs(ai),yo=[],kh=Object.create(null);for(let[n,e]of[["variable","variableName"],["variable-2","variableName.special"],["string-2","string.special"],["def","variableName.definition"],["tag","tagName"],["attribute","attributeName"],["type","typeName"],["builtin","variableName.standard"],["qualifier","modifier"],["error","invalid"],["header","heading"],["property","propertyName"]])kh[n]=vh(Xs,e);class Sh{constructor(e){this.extra=e,this.table=Object.assign(Object.create(null),kh)}resolve(e){return e?this.table[e]||(this.table[e]=vh(this.extra,e)):0}}const Hu=new Sh(Xs);function Fn(n,e){yo.indexOf(n)>-1||(yo.push(n),console.warn(e))}function vh(n,e){let t=null;for(let r of e.split(".")){let o=n[r]||k[r];o?typeof o=="function"?t?t=o(t):Fn(r,`Modifier ${r} used at start of tag`):t?Fn(r,`Tag ${r} used as modifier`):t=o:Fn(r,`Unknown highlighting tag ${r}`)}if(!t)return 0;let i=e.replace(/ /g,"_"),s=we.define({id:ai.length,name:i,props:[ru({[i]:t})]});return ai.push(s),s.id}function Wu(n){let e=we.define({id:ai.length,name:"Document",props:[hi.add(()=>n)]});return ai.push(e),e}const zu=n=>{let e=_s(n.state);return e.line?qu(n):e.block?Uu(n):!1};function Ys(n,e){return({state:t,dispatch:i})=>{if(t.readOnly)return!1;let s=n(e,t);return s?(i(t.update(s)),!0):!1}}const qu=Ys(Ju,0),Ku=Ys(Ch,0),Uu=Ys((n,e)=>Ch(n,e,Gu(e)),0);function _s(n,e=n.selection.main.head){let t=n.languageDataAt("commentTokens",e);return t.length?t[0]:{}}const jt=50;function ju(n,{open:e,close:t},i,s){let r=n.sliceDoc(i-jt,i),o=n.sliceDoc(s,s+jt),l=/\s*$/.exec(r)[0].length,h=/^\s*/.exec(o)[0].length,a=r.length-l;if(r.slice(a-e.length,a)==e&&o.slice(h,h+t.length)==t)return{open:{pos:i-l,margin:l&&1},close:{pos:s+h,margin:h&&1}};let c,f;s-i<=2*jt?c=f=n.sliceDoc(i,s):(c=n.sliceDoc(i,i+jt),f=n.sliceDoc(s-jt,s));let u=/^\s*/.exec(c)[0].length,d=/\s*$/.exec(f)[0].length,p=f.length-d-t.length;return c.slice(u,u+e.length)==e&&f.slice(p,p+t.length)==t?{open:{pos:i+u+e.length,margin:/\s/.test(c.charAt(u+e.length))?1:0},close:{pos:s-d-t.length,margin:/\s/.test(f.charAt(p-1))?1:0}}:null}function Gu(n){let e=[];for(let t of n.selection.ranges){let i=n.doc.lineAt(t.from),s=t.to<=i.to?i:n.doc.lineAt(t.to),r=e.length-1;r>=0&&e[r].to>i.from?e[r].to=s.to:e.push({from:i.from,to:s.to})}return e}function Ch(n,e,t=e.selection.ranges){let i=t.map(r=>_s(e,r.from).block);if(!i.every(r=>r))return null;let s=t.map((r,o)=>ju(e,i[o],r.from,r.to));if(n!=2&&!s.every(r=>r))return{changes:e.changes(t.map((r,o)=>s[o]?[]:[{from:r.from,insert:i[o].open+" "},{from:r.to,insert:" "+i[o].close}]))};if(n!=1&&s.some(r=>r)){let r=[];for(let o=0,l;os&&(r==o||o>c.from)){s=c.from;let f=_s(e,a).line;if(!f)continue;let u=/^\s*/.exec(c.text)[0].length,d=u==c.length,p=c.text.slice(u,u+f.length)==f?u:-1;ur.comment<0&&(!r.empty||r.single))){let r=[];for(let{line:l,token:h,indent:a,empty:c,single:f}of i)(f||!c)&&r.push({from:l.from+a,insert:h+" "});let o=e.changes(r);return{changes:o,selection:e.selection.map(o,1)}}else if(n!=1&&i.some(r=>r.comment>=0)){let r=[];for(let{line:o,comment:l,token:h}of i)if(l>=0){let a=o.from+l,c=a+h.length;o.text[c-o.from]==" "&&c++,r.push({from:a,to:c})}return{changes:r}}return null}const Cs=wt.define(),$u=wt.define(),Xu=T.define(),Ah=T.define({combine(n){return Ht(n,{minDepth:100,newGroupDelay:500},{minDepth:Math.max,newGroupDelay:Math.min})}});function Yu(n){let e=0;return n.iterChangedRanges((t,i)=>e=i),e}const Mh=ke.define({create(){return He.empty},update(n,e){let t=e.state.facet(Ah),i=e.annotation(Cs);if(i){let h=e.docChanged?m.single(Yu(e.changes)):void 0,a=ye.fromTransaction(e,h),c=i.side,f=c==0?n.undone:n.done;return a?f=Qi(f,f.length,t.minDepth,a):f=Th(f,e.startState.selection),new He(c==0?i.rest:f,c==0?f:i.rest)}let s=e.annotation($u);if((s=="full"||s=="before")&&(n=n.isolate()),e.annotation(te.addToHistory)===!1)return e.changes.empty?n:n.addMapping(e.changes.desc);let r=ye.fromTransaction(e),o=e.annotation(te.time),l=e.annotation(te.userEvent);return r?n=n.addChanges(r,o,l,t.newGroupDelay,t.minDepth):e.selection&&(n=n.addSelection(e.startState.selection,o,l,t.newGroupDelay)),(s=="full"||s=="after")&&(n=n.isolate()),n},toJSON(n){return{done:n.done.map(e=>e.toJSON()),undone:n.undone.map(e=>e.toJSON())}},fromJSON(n){return new He(n.done.map(ye.fromJSON),n.undone.map(ye.fromJSON))}});function _u(n={}){return[Mh,Ah.of(n),O.domEventHandlers({beforeinput(e,t){let i=e.inputType=="historyUndo"?Dh:e.inputType=="historyRedo"?As:null;return i?(e.preventDefault(),i(t)):!1}})]}function un(n,e){return function({state:t,dispatch:i}){if(!e&&t.readOnly)return!1;let s=t.field(Mh,!1);if(!s)return!1;let r=s.pop(n,t,e);return r?(i(r),!0):!1}}const Dh=un(0,!1),As=un(1,!1),Qu=un(0,!0),Zu=un(1,!0);class ye{constructor(e,t,i,s,r){this.changes=e,this.effects=t,this.mapped=i,this.startSelection=s,this.selectionsAfter=r}setSelAfter(e){return new ye(this.changes,this.effects,this.mapped,this.startSelection,e)}toJSON(){var e,t,i;return{changes:(e=this.changes)===null||e===void 0?void 0:e.toJSON(),mapped:(t=this.mapped)===null||t===void 0?void 0:t.toJSON(),startSelection:(i=this.startSelection)===null||i===void 0?void 0:i.toJSON(),selectionsAfter:this.selectionsAfter.map(s=>s.toJSON())}}static fromJSON(e){return new ye(e.changes&&ee.fromJSON(e.changes),[],e.mapped&&We.fromJSON(e.mapped),e.startSelection&&m.fromJSON(e.startSelection),e.selectionsAfter.map(m.fromJSON))}static fromTransaction(e,t){let i=Oe;for(let s of e.startState.facet(Xu)){let r=s(e);r.length&&(i=i.concat(r))}return!i.length&&e.changes.empty?null:new ye(e.changes.invert(e.startState.doc),i,void 0,t||e.startState.selection,Oe)}static selection(e){return new ye(void 0,Oe,void 0,void 0,e)}}function Qi(n,e,t,i){let s=e+1>t+20?e-t-1:0,r=n.slice(s,e);return r.push(i),r}function ed(n,e){let t=[],i=!1;return n.iterChangedRanges((s,r)=>t.push(s,r)),e.iterChangedRanges((s,r,o,l)=>{for(let h=0;h=a&&o<=c&&(i=!0)}}),i}function td(n,e){return n.ranges.length==e.ranges.length&&n.ranges.filter((t,i)=>t.empty!=e.ranges[i].empty).length===0}function Oh(n,e){return n.length?e.length?n.concat(e):n:e}const Oe=[],id=200;function Th(n,e){if(n.length){let t=n[n.length-1],i=t.selectionsAfter.slice(Math.max(0,t.selectionsAfter.length-id));return i.length&&i[i.length-1].eq(e)?n:(i.push(e),Qi(n,n.length-1,1e9,t.setSelAfter(i)))}else return[ye.selection([e])]}function nd(n){let e=n[n.length-1],t=n.slice();return t[n.length-1]=e.setSelAfter(e.selectionsAfter.slice(0,e.selectionsAfter.length-1)),t}function Hn(n,e){if(!n.length)return n;let t=n.length,i=Oe;for(;t;){let s=sd(n[t-1],e,i);if(s.changes&&!s.changes.empty||s.effects.length){let r=n.slice(0,t);return r[t-1]=s,r}else e=s.mapped,t--,i=s.selectionsAfter}return i.length?[ye.selection(i)]:Oe}function sd(n,e,t){let i=Oh(n.selectionsAfter.length?n.selectionsAfter.map(l=>l.map(e)):Oe,t);if(!n.changes)return ye.selection(i);let s=n.changes.map(e),r=e.mapDesc(n.changes,!0),o=n.mapped?n.mapped.composeDesc(r):r;return new ye(s,H.mapEffects(n.effects,e),o,n.startSelection.map(r),i)}const rd=/^(input\.type|delete)($|\.)/;class He{constructor(e,t,i=0,s=void 0){this.done=e,this.undone=t,this.prevTime=i,this.prevUserEvent=s}isolate(){return this.prevTime?new He(this.done,this.undone):this}addChanges(e,t,i,s,r){let o=this.done,l=o[o.length-1];return l&&l.changes&&!l.changes.empty&&e.changes&&(!i||rd.test(i))&&(!l.selectionsAfter.length&&t-this.prevTime0&&t-this.prevTimet.empty?n.moveByChar(t,e):dn(t,e))}function Te(n){return n.textDirectionAt(n.state.selection.main.head)==Z.LTR}const Ph=n=>Bh(n,!Te(n)),Rh=n=>Bh(n,Te(n));function Lh(n,e){return Ge(n,t=>t.empty?n.moveByGroup(t,e):dn(t,e))}const ld=n=>Lh(n,!Te(n)),hd=n=>Lh(n,Te(n));function ad(n,e,t){if(e.type.prop(t))return!0;let i=e.to-e.from;return i&&(i>2||/[^\s,.;:]/.test(n.sliceDoc(e.from,e.to)))||e.firstChild}function pn(n,e,t){let i=xe(n).resolveInner(e.head),s=t?F.closedBy:F.openedBy;for(let h=e.head;;){let a=t?i.childAfter(h):i.childBefore(h);if(!a)break;ad(n,a,s)?i=a:h=t?a.to:a.from}let r=i.type.prop(s),o,l;return r&&(o=t?Fe(n,i.from,1):Fe(n,i.to,-1))&&o.matched?l=t?o.end.to:o.end.from:l=t?i.to:i.from,m.cursor(l,t?-1:1)}const cd=n=>Ge(n,e=>pn(n.state,e,!Te(n))),fd=n=>Ge(n,e=>pn(n.state,e,Te(n)));function Eh(n,e){return Ge(n,t=>{if(!t.empty)return dn(t,e);let i=n.moveVertically(t,e);return i.head!=t.head?i:n.moveToLineBoundary(t,e)})}const Ih=n=>Eh(n,!1),Nh=n=>Eh(n,!0);function Vh(n){return Math.max(n.defaultLineHeight,Math.min(n.dom.clientHeight,innerHeight)-5)}function Fh(n,e){let{state:t}=n,i=Wt(t.selection,l=>l.empty?n.moveVertically(l,e,Vh(n)):dn(l,e));if(i.eq(t.selection))return!1;let s=n.coordsAtPos(t.selection.main.head),r=n.scrollDOM.getBoundingClientRect(),o;return s&&s.top>r.top&&s.bottomFh(n,!1),Ms=n=>Fh(n,!0);function gn(n,e,t){let i=n.lineBlockAt(e.head),s=n.moveToLineBoundary(e,t);if(s.head==e.head&&s.head!=(t?i.to:i.from)&&(s=n.moveToLineBoundary(e,t,!1)),!t&&s.head==i.from&&i.length){let r=/^\s*/.exec(n.state.sliceDoc(i.from,Math.min(i.from+100,i.to)))[0].length;r&&e.head!=i.from+r&&(s=m.cursor(i.from+r))}return s}const wo=n=>Ge(n,e=>gn(n,e,!0)),xo=n=>Ge(n,e=>gn(n,e,!1)),ud=n=>Ge(n,e=>m.cursor(n.lineBlockAt(e.head).from,1)),dd=n=>Ge(n,e=>m.cursor(n.lineBlockAt(e.head).to,-1));function pd(n,e,t){let i=!1,s=Wt(n.selection,r=>{let o=Fe(n,r.head,-1)||Fe(n,r.head,1)||r.head>0&&Fe(n,r.head-1,1)||r.headpd(n,e,!1);function Ke(n,e){let t=Wt(n.state.selection,i=>{let s=e(i);return m.range(i.anchor,s.head,s.goalColumn)});return t.eq(n.state.selection)?!1:(n.dispatch(qe(n.state,t)),!0)}function Hh(n,e){return Ke(n,t=>n.moveByChar(t,e))}const Wh=n=>Hh(n,!Te(n)),zh=n=>Hh(n,Te(n));function qh(n,e){return Ke(n,t=>n.moveByGroup(t,e))}const md=n=>qh(n,!Te(n)),yd=n=>qh(n,Te(n)),bd=n=>Ke(n,e=>pn(n.state,e,!Te(n))),wd=n=>Ke(n,e=>pn(n.state,e,Te(n)));function Kh(n,e){return Ke(n,t=>n.moveVertically(t,e))}const Uh=n=>Kh(n,!1),jh=n=>Kh(n,!0);function Gh(n,e){return Ke(n,t=>n.moveVertically(t,e,Vh(n)))}const ko=n=>Gh(n,!1),So=n=>Gh(n,!0),vo=n=>Ke(n,e=>gn(n,e,!0)),Co=n=>Ke(n,e=>gn(n,e,!1)),xd=n=>Ke(n,e=>m.cursor(n.lineBlockAt(e.head).from)),kd=n=>Ke(n,e=>m.cursor(n.lineBlockAt(e.head).to)),Ao=({state:n,dispatch:e})=>(e(qe(n,{anchor:0})),!0),Mo=({state:n,dispatch:e})=>(e(qe(n,{anchor:n.doc.length})),!0),Do=({state:n,dispatch:e})=>(e(qe(n,{anchor:n.selection.main.anchor,head:0})),!0),Oo=({state:n,dispatch:e})=>(e(qe(n,{anchor:n.selection.main.anchor,head:n.doc.length})),!0),Sd=({state:n,dispatch:e})=>(e(n.update({selection:{anchor:0,head:n.doc.length},userEvent:"select"})),!0),vd=({state:n,dispatch:e})=>{let t=bn(n).map(({from:i,to:s})=>m.range(i,Math.min(s+1,n.doc.length)));return e(n.update({selection:m.create(t),userEvent:"select"})),!0},Cd=({state:n,dispatch:e})=>{let t=Wt(n.selection,i=>{var s;let r=xe(n).resolveInner(i.head,1);for(;!(r.from=i.to||r.to>i.to&&r.from<=i.from||!(!((s=r.parent)===null||s===void 0)&&s.parent));)r=r.parent;return m.range(r.to,r.from)});return e(qe(n,t)),!0},Ad=({state:n,dispatch:e})=>{let t=n.selection,i=null;return t.ranges.length>1?i=m.create([t.main]):t.main.empty||(i=m.create([m.cursor(t.main.head)])),i?(e(qe(n,i)),!0):!1};function mn({state:n,dispatch:e},t){if(n.readOnly)return!1;let i="delete.selection",s=n.changeByRange(r=>{let{from:o,to:l}=r;if(o==l){let h=t(o);ho&&(i="delete.forward"),o=Math.min(o,h),l=Math.max(l,h)}return o==l?{range:r}:{changes:{from:o,to:l},range:m.cursor(o)}});return s.changes.empty?!1:(e(n.update(s,{scrollIntoView:!0,userEvent:i,effects:i=="delete.selection"?O.announce.of(n.phrase("Selection deleted")):void 0})),!0)}function yn(n,e,t){if(n instanceof O)for(let i of n.state.facet(O.atomicRanges).map(s=>s(n)))i.between(e,e,(s,r)=>{se&&(e=t?r:s)});return e}const Jh=(n,e)=>mn(n,t=>{let{state:i}=n,s=i.doc.lineAt(t),r,o;if(!e&&t>s.from&&tJh(n,!1),$h=n=>Jh(n,!0),Xh=(n,e)=>mn(n,t=>{let i=t,{state:s}=n,r=s.doc.lineAt(i),o=s.charCategorizer(i);for(let l=null;;){if(i==(e?r.to:r.from)){i==t&&r.number!=(e?s.doc.lines:1)&&(i+=e?1:-1);break}let h=Ae(r.text,i-r.from,e)+r.from,a=r.text.slice(Math.min(i,h)-r.from,Math.max(i,h)-r.from),c=o(a);if(l!=null&&c!=l)break;(a!=" "||i!=t)&&(l=c),i=h}return yn(n,i,e)}),Yh=n=>Xh(n,!1),Md=n=>Xh(n,!0),_h=n=>mn(n,e=>{let t=n.lineBlockAt(e).to;return yn(n,emn(n,e=>{let t=n.lineBlockAt(e).from;return yn(n,e>t?t:Math.max(0,e-1),!1)}),Od=({state:n,dispatch:e})=>{if(n.readOnly)return!1;let t=n.changeByRange(i=>({changes:{from:i.from,to:i.to,insert:z.of(["",""])},range:m.cursor(i.from)}));return e(n.update(t,{scrollIntoView:!0,userEvent:"input"})),!0},Td=({state:n,dispatch:e})=>{if(n.readOnly)return!1;let t=n.changeByRange(i=>{if(!i.empty||i.from==0||i.from==n.doc.length)return{range:i};let s=i.from,r=n.doc.lineAt(s),o=s==r.from?s-1:Ae(r.text,s-r.from,!1)+r.from,l=s==r.to?s+1:Ae(r.text,s-r.from,!0)+r.from;return{changes:{from:o,to:l,insert:n.doc.slice(s,l).append(n.doc.slice(o,s))},range:m.cursor(l)}});return t.changes.empty?!1:(e(n.update(t,{scrollIntoView:!0,userEvent:"move.character"})),!0)};function bn(n){let e=[],t=-1;for(let i of n.selection.ranges){let s=n.doc.lineAt(i.from),r=n.doc.lineAt(i.to);if(!i.empty&&i.to==r.from&&(r=n.doc.lineAt(i.to-1)),t>=s.number){let o=e[e.length-1];o.to=r.to,o.ranges.push(i)}else e.push({from:s.from,to:r.to,ranges:[i]});t=r.number+1}return e}function Qh(n,e,t){if(n.readOnly)return!1;let i=[],s=[];for(let r of bn(n)){if(t?r.to==n.doc.length:r.from==0)continue;let o=n.doc.lineAt(t?r.to+1:r.from-1),l=o.length+1;if(t){i.push({from:r.to,to:o.to},{from:r.from,insert:o.text+n.lineBreak});for(let h of r.ranges)s.push(m.range(Math.min(n.doc.length,h.anchor+l),Math.min(n.doc.length,h.head+l)))}else{i.push({from:o.from,to:r.from},{from:r.to,insert:n.lineBreak+o.text});for(let h of r.ranges)s.push(m.range(h.anchor-l,h.head-l))}}return i.length?(e(n.update({changes:i,scrollIntoView:!0,selection:m.create(s,n.selection.mainIndex),userEvent:"move.line"})),!0):!1}const Bd=({state:n,dispatch:e})=>Qh(n,e,!1),Pd=({state:n,dispatch:e})=>Qh(n,e,!0);function Zh(n,e,t){if(n.readOnly)return!1;let i=[];for(let s of bn(n))t?i.push({from:s.from,insert:n.doc.slice(s.from,s.to)+n.lineBreak}):i.push({from:s.to,insert:n.lineBreak+n.doc.slice(s.from,s.to)});return e(n.update({changes:i,scrollIntoView:!0,userEvent:"input.copyline"})),!0}const Rd=({state:n,dispatch:e})=>Zh(n,e,!1),Ld=({state:n,dispatch:e})=>Zh(n,e,!0),Ed=n=>{if(n.state.readOnly)return!1;let{state:e}=n,t=e.changes(bn(e).map(({from:s,to:r})=>(s>0?s--:rn.moveVertically(s,!0)).map(t);return n.dispatch({changes:t,selection:i,scrollIntoView:!0,userEvent:"delete.line"}),!0};function Id(n,e){if(/\(\)|\[\]|\{\}/.test(n.sliceDoc(e-1,e+1)))return{from:e,to:e};let t=xe(n).resolveInner(e),i=t.childBefore(e),s=t.childAfter(e),r;return i&&s&&i.to<=e&&s.from>=e&&(r=i.type.prop(F.closedBy))&&r.indexOf(s.name)>-1&&n.doc.lineAt(i.to).from==n.doc.lineAt(s.from).from?{from:i.to,to:s.from}:null}const Nd=ea(!1),Vd=ea(!0);function ea(n){return({state:e,dispatch:t})=>{if(e.readOnly)return!1;let i=e.changeByRange(s=>{let{from:r,to:o}=s,l=e.doc.lineAt(r),h=!n&&r==o&&Id(e,r);n&&(r=o=(o<=l.to?l:e.doc.lineAt(o)).to);let a=new cn(e,{simulateBreak:r,simulateDoubleBreak:!!h}),c=uh(a,r);for(c==null&&(c=/^\s*/.exec(e.doc.lineAt(r).text)[0].length);ol.from&&r{let s=[];for(let o=i.from;o<=i.to;){let l=n.doc.lineAt(o);l.number>t&&(i.empty||i.to>l.from)&&(e(l,s,i),t=l.number),o=l.to+1}let r=n.changes(s);return{changes:s,range:m.range(r.mapPos(i.anchor,1),r.mapPos(i.head,1))}})}const Fd=({state:n,dispatch:e})=>{if(n.readOnly)return!1;let t=Object.create(null),i=new cn(n,{overrideIndentation:r=>{let o=t[r];return o==null?-1:o}}),s=Qs(n,(r,o,l)=>{let h=uh(i,r.from);if(h==null)return;/\S/.test(r.text)||(h=0);let a=/^\s*/.exec(r.text)[0],c=_i(n,h);(a!=c||l.fromn.readOnly?!1:(e(n.update(Qs(n,(t,i)=>{i.push({from:t.from,insert:n.facet(js)})}),{userEvent:"input.indent"})),!0),Wd=({state:n,dispatch:e})=>n.readOnly?!1:(e(n.update(Qs(n,(t,i)=>{let s=/^\s*/.exec(t.text)[0];if(!s)return;let r=ui(s,n.tabSize),o=0,l=_i(n,Math.max(0,r-bt(n)));for(;o({mac:n.key,run:n.run,shift:n.shift}))),Kd=[{key:"Alt-ArrowLeft",mac:"Ctrl-ArrowLeft",run:cd,shift:bd},{key:"Alt-ArrowRight",mac:"Ctrl-ArrowRight",run:fd,shift:wd},{key:"Alt-ArrowUp",run:Bd},{key:"Shift-Alt-ArrowUp",run:Rd},{key:"Alt-ArrowDown",run:Pd},{key:"Shift-Alt-ArrowDown",run:Ld},{key:"Escape",run:Ad},{key:"Mod-Enter",run:Vd},{key:"Alt-l",mac:"Ctrl-l",run:vd},{key:"Mod-i",run:Cd,preventDefault:!0},{key:"Mod-[",run:Wd},{key:"Mod-]",run:Hd},{key:"Mod-Alt-\\",run:Fd},{key:"Shift-Mod-k",run:Ed},{key:"Shift-Mod-\\",run:gd},{key:"Mod-/",run:zu},{key:"Alt-A",run:Ku}].concat(qd);function pe(){var n=arguments[0];typeof n=="string"&&(n=document.createElement(n));var e=1,t=arguments[1];if(t&&typeof t=="object"&&t.nodeType==null&&!Array.isArray(t)){for(var i in t)if(Object.prototype.hasOwnProperty.call(t,i)){var s=t[i];typeof s=="string"?n.setAttribute(i,s):s!=null&&(n[i]=s)}e++}for(;en.normalize("NFKD"):n=>n;class Ft{constructor(e,t,i=0,s=e.length,r){this.value={from:0,to:0},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=e.iterRange(i,s),this.bufferStart=i,this.normalize=r?o=>r(To(o)):To,this.query=this.normalize(t)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return re(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let e=this.peek();if(e<0)return this.done=!0,this;let t=Ps(e),i=this.bufferStart+this.bufferPos;this.bufferPos+=ve(e);let s=this.normalize(t);for(let r=0,o=i;;r++){let l=s.charCodeAt(r),h=this.match(l,o);if(h)return this.value=h,this;if(r==s.length-1)break;o==i&&rthis.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let e=this.matchPos-this.curLineStart;;){this.re.lastIndex=e;let t=this.matchPos<=this.to&&this.re.exec(this.curLine);if(t){let i=this.curLineStart+t.index,s=i+t[0].length;if(this.matchPos=s+(i==s?1:0),i==this.curLine.length&&this.nextLine(),ithis.value.to)return this.value={from:i,to:s,match:t},this;e=this.matchPos-this.curLineStart}else if(this.curLineStart+this.curLine.length=i||s.to<=t){let l=new Bt(t,e.sliceString(t,i));return Wn.set(e,l),l}if(s.from==t&&s.to==i)return s;let{text:r,from:o}=s;return o>t&&(r=e.sliceString(t,o)+r,o=t),s.to=this.to?this.to:this.text.lineAt(e).to}next(){for(;;){let e=this.re.lastIndex=this.matchPos-this.flat.from,t=this.re.exec(this.flat.text);if(t&&!t[0]&&t.index==e&&(this.re.lastIndex=e+1,t=this.re.exec(this.flat.text)),t&&this.flat.tothis.flat.text.length-10&&(t=null),t){let i=this.flat.from+t.index,s=i+t[0].length;return this.value={from:i,to:s,match:t},this.matchPos=s+(i==s?1:0),this}else{if(this.flat.to==this.to)return this.done=!0,this;this.flat=Bt.get(this.text,this.flat.from,this.chunkEnd(this.flat.from+this.flat.text.length*2))}}}}typeof Symbol!="undefined"&&(na.prototype[Symbol.iterator]=sa.prototype[Symbol.iterator]=function(){return this});function Ud(n){try{return new RegExp(n,Zs),!0}catch{return!1}}function Os(n){let e=pe("input",{class:"cm-textfield",name:"line"}),t=pe("form",{class:"cm-gotoLine",onkeydown:s=>{s.keyCode==27?(s.preventDefault(),n.dispatch({effects:Zi.of(!1)}),n.focus()):s.keyCode==13&&(s.preventDefault(),i())},onsubmit:s=>{s.preventDefault(),i()}},pe("label",n.state.phrase("Go to line"),": ",e)," ",pe("button",{class:"cm-button",type:"submit"},n.state.phrase("go")));function i(){let s=/^([+-])?(\d+)?(:\d+)?(%)?$/.exec(e.value);if(!s)return;let{state:r}=n,o=r.doc.lineAt(r.selection.main.head),[,l,h,a,c]=s,f=a?+a.slice(1):0,u=h?+h:o.number;if(h&&c){let p=u/100;l&&(p=p*(l=="-"?-1:1)+o.number/r.doc.lines),u=Math.round(r.doc.lines*p)}else h&&l&&(u=u*(l=="-"?-1:1)+o.number);let d=r.doc.line(Math.max(1,Math.min(r.doc.lines,u)));n.dispatch({effects:Zi.of(!1),selection:m.cursor(d.from+Math.max(0,Math.min(f,d.length))),scrollIntoView:!0}),n.focus()}return{dom:t}}const Zi=H.define(),Bo=ke.define({create(){return!0},update(n,e){for(let t of e.effects)t.is(Zi)&&(n=t.value);return n},provide:n=>Gi.from(n,e=>e?Os:null)}),jd=n=>{let e=ji(n,Os);if(!e){let t=[Zi.of(!0)];n.state.field(Bo,!1)==null&&t.push(H.appendConfig.of([Bo,Gd])),n.dispatch({effects:t}),e=ji(n,Os)}return e&&e.dom.querySelector("input").focus(),!0},Gd=O.baseTheme({".cm-panel.cm-gotoLine":{padding:"2px 6px 4px","& label":{fontSize:"80%"}}}),Jd={highlightWordAroundCursor:!1,minSelectionLength:1,maxMatches:100,wholeWords:!1},ra=T.define({combine(n){return Ht(n,Jd,{highlightWordAroundCursor:(e,t)=>e||t,minSelectionLength:Math.min,maxMatches:Math.min})}});function $d(n){let e=[Zd,Qd];return n&&e.push(ra.of(n)),e}const Xd=P.mark({class:"cm-selectionMatch"}),Yd=P.mark({class:"cm-selectionMatch cm-selectionMatch-main"});function Po(n,e,t,i){return(t==0||n(e.sliceDoc(t-1,t))!=ce.Word)&&(i==e.doc.length||n(e.sliceDoc(i,i+1))!=ce.Word)}function _d(n,e,t,i){return n(e.sliceDoc(t,t+1))==ce.Word&&n(e.sliceDoc(i-1,i))==ce.Word}const Qd=ue.fromClass(class{constructor(n){this.decorations=this.getDeco(n)}update(n){(n.selectionSet||n.docChanged||n.viewportChanged)&&(this.decorations=this.getDeco(n.view))}getDeco(n){let e=n.state.facet(ra),{state:t}=n,i=t.selection;if(i.ranges.length>1)return P.none;let s=i.main,r,o=null;if(s.empty){if(!e.highlightWordAroundCursor)return P.none;let h=t.wordAt(s.head);if(!h)return P.none;o=t.charCategorizer(s.head),r=t.sliceDoc(h.from,h.to)}else{let h=s.to-s.from;if(h200)return P.none;if(e.wholeWords){if(r=t.sliceDoc(s.from,s.to),o=t.charCategorizer(s.head),!(Po(o,t,s.from,s.to)&&_d(o,t,s.from,s.to)))return P.none}else if(r=t.sliceDoc(s.from,s.to).trim(),!r)return P.none}let l=[];for(let h of n.visibleRanges){let a=new Ft(t.doc,r,h.from,h.to);for(;!a.next().done;){let{from:c,to:f}=a.value;if((!o||Po(o,t,c,f))&&(s.empty&&c<=s.from&&f>=s.to?l.push(Yd.range(c,f)):(c>=s.to||f<=s.from)&&l.push(Xd.range(c,f)),l.length>e.maxMatches))return P.none}}return P.set(l)}},{decorations:n=>n.decorations}),Zd=O.baseTheme({".cm-selectionMatch":{backgroundColor:"#99ff7780"},".cm-searchMatch .cm-selectionMatch":{backgroundColor:"transparent"}}),ep=({state:n,dispatch:e})=>{let{selection:t}=n,i=m.create(t.ranges.map(s=>n.wordAt(s.head)||m.cursor(s.head)),t.mainIndex);return i.eq(t)?!1:(e(n.update({selection:i})),!0)};function tp(n,e){let{main:t,ranges:i}=n.selection,s=n.wordAt(t.head),r=s&&s.from==t.from&&s.to==t.to;for(let o=!1,l=new Ft(n.doc,e,i[i.length-1].to);;)if(l.next(),l.done){if(o)return null;l=new Ft(n.doc,e,0,Math.max(0,i[i.length-1].from-1)),o=!0}else{if(o&&i.some(h=>h.from==l.value.from))continue;if(r){let h=n.wordAt(l.value.from);if(!h||h.from!=l.value.from||h.to!=l.value.to)continue}return l.value}}const ip=({state:n,dispatch:e})=>{let{ranges:t}=n.selection;if(t.some(r=>r.from===r.to))return ep({state:n,dispatch:e});let i=n.sliceDoc(t[0].from,t[0].to);if(n.selection.ranges.some(r=>n.sliceDoc(r.from,r.to)!=i))return!1;let s=tp(n,i);return s?(e(n.update({selection:n.selection.addRange(m.range(s.from,s.to),!1),effects:O.scrollIntoView(s.to)})),!0):!1},er=T.define({combine(n){var e;return{top:n.reduce((t,i)=>t!=null?t:i.top,void 0)||!1,caseSensitive:n.reduce((t,i)=>t!=null?t:i.caseSensitive,void 0)||!1,createPanel:((e=n.find(t=>t.createPanel))===null||e===void 0?void 0:e.createPanel)||(t=>new up(t))}}});class oa{constructor(e){this.search=e.search,this.caseSensitive=!!e.caseSensitive,this.regexp=!!e.regexp,this.replace=e.replace||"",this.valid=!!this.search&&(!this.regexp||Ud(this.search)),this.unquoted=e.literal?this.search:this.search.replace(/\\([nrt\\])/g,(t,i)=>i=="n"?` +`:i=="r"?"\r":i=="t"?" ":"\\")}eq(e){return this.search==e.search&&this.replace==e.replace&&this.caseSensitive==e.caseSensitive&&this.regexp==e.regexp}create(){return this.regexp?new sp(this):new np(this)}getCursor(e,t=0,i=e.length){return this.regexp?Mt(this,e,t,i):At(this,e,t,i)}}class la{constructor(e){this.spec=e}}function At(n,e,t,i){return new Ft(e,n.unquoted,t,i,n.caseSensitive?void 0:s=>s.toLowerCase())}class np extends la{constructor(e){super(e)}nextMatch(e,t,i){let s=At(this.spec,e,i,e.length).nextOverlapping();return s.done&&(s=At(this.spec,e,0,t).nextOverlapping()),s.done?null:s.value}prevMatchInRange(e,t,i){for(let s=i;;){let r=Math.max(t,s-1e4-this.spec.unquoted.length),o=At(this.spec,e,r,s),l=null;for(;!o.nextOverlapping().done;)l=o.value;if(l)return l;if(r==t)return null;s-=1e4}}prevMatch(e,t,i){return this.prevMatchInRange(e,0,t)||this.prevMatchInRange(e,i,e.length)}getReplacement(e){return this.spec.replace}matchAll(e,t){let i=At(this.spec,e,0,e.length),s=[];for(;!i.next().done;){if(s.length>=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=At(this.spec,e,Math.max(0,t-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,e.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}function Mt(n,e,t,i){return new na(e,n.search,n.caseSensitive?void 0:{ignoreCase:!0},t,i)}class sp extends la{nextMatch(e,t,i){let s=Mt(this.spec,e,i,e.length).next();return s.done&&(s=Mt(this.spec,e,0,t).next()),s.done?null:s.value}prevMatchInRange(e,t,i){for(let s=1;;s++){let r=Math.max(t,i-s*1e4),o=Mt(this.spec,e,r,i),l=null;for(;!o.next().done;)l=o.value;if(l&&(r==t||l.from>r+10))return l;if(r==t)return null}}prevMatch(e,t,i){return this.prevMatchInRange(e,0,t)||this.prevMatchInRange(e,i,e.length)}getReplacement(e){return this.spec.replace.replace(/\$([$&\d+])/g,(t,i)=>i=="$"?"$":i=="&"?e.match[0]:i!="0"&&+i=t)return null;s.push(i.value)}return s}highlight(e,t,i,s){let r=Mt(this.spec,e,Math.max(0,t-250),Math.min(i+250,e.length));for(;!r.next().done;)s(r.value.from,r.value.to)}}const ci=H.define(),tr=H.define(),it=ke.define({create(n){return new zn(Ts(n).create(),null)},update(n,e){for(let t of e.effects)t.is(ci)?n=new zn(t.value.create(),n.panel):t.is(tr)&&(n=new zn(n.query,t.value?ir:null));return n},provide:n=>Gi.from(n,e=>e.panel)});class zn{constructor(e,t){this.query=e,this.panel=t}}const rp=P.mark({class:"cm-searchMatch"}),op=P.mark({class:"cm-searchMatch cm-searchMatch-selected"}),lp=ue.fromClass(class{constructor(n){this.view=n,this.decorations=this.highlight(n.state.field(it))}update(n){let e=n.state.field(it);(e!=n.startState.field(it)||n.docChanged||n.selectionSet||n.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:n,panel:e}){if(!e||!n.spec.valid)return P.none;let{view:t}=this,i=new gt;for(let s=0,r=t.visibleRanges,o=r.length;sr[s+1].from-2*250;)h=r[++s].to;n.highlight(t.state.doc,l,h,(a,c)=>{let f=t.state.selection.ranges.some(u=>u.from==a&&u.to==c);i.add(a,c,f?op:rp)})}return i.finish()}},{decorations:n=>n.decorations});function pi(n){return e=>{let t=e.state.field(it,!1);return t&&t.query.spec.valid?n(e,t):ha(e)}}const en=pi((n,{query:e})=>{let{from:t,to:i}=n.state.selection.main,s=e.nextMatch(n.state.doc,t,i);return!s||s.from==t&&s.to==i?!1:(n.dispatch({selection:{anchor:s.from,head:s.to},scrollIntoView:!0,effects:nr(n,s),userEvent:"select.search"}),!0)}),tn=pi((n,{query:e})=>{let{state:t}=n,{from:i,to:s}=t.selection.main,r=e.prevMatch(t.doc,i,s);return r?(n.dispatch({selection:{anchor:r.from,head:r.to},scrollIntoView:!0,effects:nr(n,r),userEvent:"select.search"}),!0):!1}),hp=pi((n,{query:e})=>{let t=e.matchAll(n.state.doc,1e3);return!t||!t.length?!1:(n.dispatch({selection:m.create(t.map(i=>m.range(i.from,i.to))),userEvent:"select.search.matches"}),!0)}),ap=({state:n,dispatch:e})=>{let t=n.selection;if(t.ranges.length>1||t.main.empty)return!1;let{from:i,to:s}=t.main,r=[],o=0;for(let l=new Ft(n.doc,n.sliceDoc(i,s));!l.next().done;){if(r.length>1e3)return!1;l.value.from==i&&(o=r.length),r.push(m.range(l.value.from,l.value.to))}return e(n.update({selection:m.create(r,o),userEvent:"select.search.matches"})),!0},Ro=pi((n,{query:e})=>{let{state:t}=n,{from:i,to:s}=t.selection.main;if(t.readOnly)return!1;let r=e.nextMatch(t.doc,i,i);if(!r)return!1;let o=[],l,h,a=[];if(r.from==i&&r.to==s&&(h=t.toText(e.getReplacement(r)),o.push({from:r.from,to:r.to,insert:h}),r=e.nextMatch(t.doc,r.from,r.to),a.push(O.announce.of(t.phrase("replaced match on line $",t.doc.lineAt(i).number)+"."))),r){let c=o.length==0||o[0].from>=r.to?0:r.to-r.from-h.length;l={anchor:r.from-c,head:r.to-c},a.push(nr(n,r))}return n.dispatch({changes:o,selection:l,scrollIntoView:!!l,effects:a,userEvent:"input.replace"}),!0}),cp=pi((n,{query:e})=>{if(n.state.readOnly)return!1;let t=e.matchAll(n.state.doc,1e9).map(s=>{let{from:r,to:o}=s;return{from:r,to:o,insert:e.getReplacement(s)}});if(!t.length)return!1;let i=n.state.phrase("replaced $ matches",t.length)+".";return n.dispatch({changes:t,effects:O.announce.of(i),userEvent:"input.replace.all"}),!0});function ir(n){return n.state.facet(er).createPanel(n)}function Ts(n,e){var t;let i=n.selection.main,s=i.empty||i.to>i.from+100?"":n.sliceDoc(i.from,i.to),r=(t=e==null?void 0:e.caseSensitive)!==null&&t!==void 0?t:n.facet(er).caseSensitive;return e&&!s?e:new oa({search:s.replace(/\n/g,"\\n"),caseSensitive:r})}const ha=n=>{let e=n.state.field(it,!1);if(e&&e.panel){let t=ji(n,ir);if(!t)return!1;let i=t.dom.querySelector("[main-field]");if(i&&i!=n.root.activeElement){let s=Ts(n.state,e.query.spec);s.valid&&n.dispatch({effects:ci.of(s)}),i.focus(),i.select()}}else n.dispatch({effects:[tr.of(!0),e?ci.of(Ts(n.state,e.query.spec)):H.appendConfig.of(pp)]});return!0},aa=n=>{let e=n.state.field(it,!1);if(!e||!e.panel)return!1;let t=ji(n,ir);return t&&t.dom.contains(n.root.activeElement)&&n.focus(),n.dispatch({effects:tr.of(!1)}),!0},fp=[{key:"Mod-f",run:ha,scope:"editor search-panel"},{key:"F3",run:en,shift:tn,scope:"editor search-panel",preventDefault:!0},{key:"Mod-g",run:en,shift:tn,scope:"editor search-panel",preventDefault:!0},{key:"Escape",run:aa,scope:"editor search-panel"},{key:"Mod-Shift-l",run:ap},{key:"Alt-g",run:jd},{key:"Mod-d",run:ip,preventDefault:!0}];class up{constructor(e){this.view=e;let t=this.query=e.state.field(it).query.spec;this.commit=this.commit.bind(this),this.searchField=pe("input",{value:t.search,placeholder:Me(e,"Find"),"aria-label":Me(e,"Find"),class:"cm-textfield",name:"search","main-field":"true",onchange:this.commit,onkeyup:this.commit}),this.replaceField=pe("input",{value:t.replace,placeholder:Me(e,"Replace"),"aria-label":Me(e,"Replace"),class:"cm-textfield",name:"replace",onchange:this.commit,onkeyup:this.commit}),this.caseField=pe("input",{type:"checkbox",name:"case",checked:t.caseSensitive,onchange:this.commit}),this.reField=pe("input",{type:"checkbox",name:"re",checked:t.regexp,onchange:this.commit});function i(s,r,o){return pe("button",{class:"cm-button",name:s,onclick:r,type:"button"},o)}this.dom=pe("div",{onkeydown:s=>this.keydown(s),class:"cm-search"},[this.searchField,i("next",()=>en(e),[Me(e,"next")]),i("prev",()=>tn(e),[Me(e,"previous")]),i("select",()=>hp(e),[Me(e,"all")]),pe("label",null,[this.caseField,Me(e,"match case")]),pe("label",null,[this.reField,Me(e,"regexp")]),...e.state.readOnly?[]:[pe("br"),this.replaceField,i("replace",()=>Ro(e),[Me(e,"replace")]),i("replaceAll",()=>cp(e),[Me(e,"replace all")]),pe("button",{name:"close",onclick:()=>aa(e),"aria-label":Me(e,"close"),type:"button"},["\xD7"])]])}commit(){let e=new oa({search:this.searchField.value,caseSensitive:this.caseField.checked,regexp:this.reField.checked,replace:this.replaceField.value});e.eq(this.query)||(this.query=e,this.view.dispatch({effects:ci.of(e)}))}keydown(e){gf(this.view,e,"search-panel")?e.preventDefault():e.keyCode==13&&e.target==this.searchField?(e.preventDefault(),(e.shiftKey?tn:en)(this.view)):e.keyCode==13&&e.target==this.replaceField&&(e.preventDefault(),Ro(this.view))}update(e){for(let t of e.transactions)for(let i of t.effects)i.is(ci)&&!i.value.eq(this.query)&&this.setQuery(i.value)}setQuery(e){this.query=e,this.searchField.value=e.search,this.replaceField.value=e.replace,this.caseField.checked=e.caseSensitive,this.reField.checked=e.regexp}mount(){this.searchField.select()}get pos(){return 80}get top(){return this.view.state.facet(er).top}}function Me(n,e){return n.state.phrase(e)}const Ti=30,Bi=/[\s\.,:;?!]/;function nr(n,{from:e,to:t}){let i=n.state.doc.lineAt(e),s=n.state.doc.lineAt(t).to,r=Math.max(i.from,e-Ti),o=Math.min(s,t+Ti),l=n.state.sliceDoc(r,o);if(r!=i.from){for(let h=0;hl.length-Ti;h--)if(!Bi.test(l[h-1])&&Bi.test(l[h])){l=l.slice(0,h);break}}return O.announce.of(`${n.state.phrase("current match")}. ${l} ${n.state.phrase("on line")} ${i.number}.`)}const dp=O.baseTheme({".cm-panel.cm-search":{padding:"2px 6px 4px",position:"relative","& [name=close]":{position:"absolute",top:"0",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",padding:0,margin:0},"& input, & button, & label":{margin:".2em .6em .2em 0"},"& input[type=checkbox]":{marginRight:".2em"},"& label":{fontSize:"80%",whiteSpace:"pre"}},"&light .cm-searchMatch":{backgroundColor:"#ffff0054"},"&dark .cm-searchMatch":{backgroundColor:"#00ffff8a"},"&light .cm-searchMatch-selected":{backgroundColor:"#ff6a0054"},"&dark .cm-searchMatch-selected":{backgroundColor:"#ff00ff8a"}}),pp=[it,fi.lowest(lp),dp];class ca{constructor(e,t,i){this.state=e,this.pos=t,this.explicit=i,this.abortListeners=[]}tokenBefore(e){let t=xe(this.state).resolveInner(this.pos,-1);for(;t&&e.indexOf(t.name)<0;)t=t.parent;return t?{from:t.from,to:this.pos,text:this.state.sliceDoc(t.from,this.pos),type:t.type}:null}matchBefore(e){let t=this.state.doc.lineAt(this.pos),i=Math.max(t.from,this.pos-250),s=t.text.slice(i-t.from,this.pos-t.from),r=s.search(fa(e,!1));return r<0?null:{from:i+r,to:this.pos,text:s.slice(r)}}get aborted(){return this.abortListeners==null}addEventListener(e,t){e=="abort"&&this.abortListeners&&this.abortListeners.push(t)}}function Lo(n){let e=Object.keys(n).join(""),t=/\w/.test(e);return t&&(e=e.replace(/\w/g,"")),`[${t?"\\w":""}${e.replace(/[^\w\s]/g,"\\$&")}]`}function gp(n){let e=Object.create(null),t=Object.create(null);for(let{label:s}of n){e[s[0]]=!0;for(let r=1;rtypeof s=="string"?{label:s}:s),[t,i]=e.every(s=>/^\w+$/.test(s.label))?[/\w*$/,/\w+$/]:gp(e);return s=>{let r=s.matchBefore(i);return r||s.explicit?{from:r?r.from:s.pos,options:e,validFor:t}:null}}class Eo{constructor(e,t,i){this.completion=e,this.source=t,this.match=i}}function nt(n){return n.selection.main.head}function fa(n,e){var t;let{source:i}=n,s=e&&i[0]!="^",r=i[i.length-1]!="$";return!s&&!r?n:new RegExp(`${s?"^":""}(?:${i})${r?"$":""}`,(t=n.flags)!==null&&t!==void 0?t:n.ignoreCase?"i":"")}function yp(n,e,t,i){return Object.assign(Object.assign({},n.changeByRange(s=>{if(s==n.selection.main)return{changes:{from:t,to:i,insert:e},range:m.cursor(t+e.length)};let r=i-t;return!s.empty||r&&n.sliceDoc(s.from-r,s.from)!=n.sliceDoc(t,i)?{range:s}:{changes:{from:s.from-r,to:s.from,insert:e},range:m.cursor(s.from-r+e.length)}})),{userEvent:"input.complete"})}function ua(n,e){const t=e.completion.apply||e.completion.label;let i=e.source;typeof t=="string"?n.dispatch(yp(n.state,t,i.from,i.to)):t(n,e.completion,i.from,i.to)}const Io=new WeakMap;function bp(n){if(!Array.isArray(n))return n;let e=Io.get(n);return e||Io.set(n,e=mp(n)),e}class wp{constructor(e){this.pattern=e,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[];for(let t=0;t=48&&S<=57||S>=97&&S<=122?2:S>=65&&S<=90?1:0:(D=Ps(S))!=D.toLowerCase()?1:D!=D.toUpperCase()?2:0;(!v||B==1&&y||x==0&&B!=0)&&(t[f]==S||i[f]==S&&(u=!0)?o[f++]=v:o.length&&(b=!1)),x=B,v+=ve(S)}return f==h&&o[0]==0&&b?this.result(-100+(u?-200:0),o,e):d==h&&p==0?[-200-e.length,0,g]:l>-1?[-700-e.length,l,l+this.pattern.length]:d==h?[-200+-700-e.length,p,g]:f==h?this.result(-100+(u?-200:0)+-700+(b?0:-1100),o,e):t.length==2?null:this.result((s[0]?-700:0)+-200+-1100,s,e)}result(e,t,i){let s=[e-i.length],r=1;for(let o of t){let l=o+(this.astral?ve(re(i,o)):1);r>1&&s[r-1]==o?s[r-1]=l:(s[r++]=o,s[r++]=l)}return s}}const lt=T.define({combine(n){return Ht(n,{activateOnTyping:!0,override:null,closeOnBlur:!0,maxRenderedOptions:100,defaultKeymap:!0,optionClass:()=>"",aboveCursor:!1,icons:!0,addToOptions:[]},{defaultKeymap:(e,t)=>e&&t,closeOnBlur:(e,t)=>e&&t,icons:(e,t)=>e&&t,optionClass:(e,t)=>i=>xp(e(i),t(i)),addToOptions:(e,t)=>e.concat(t)})}});function xp(n,e){return n?e?n+" "+e:n:e}function kp(n){let e=n.addToOptions.slice();return n.icons&&e.push({render(t){let i=document.createElement("div");return i.classList.add("cm-completionIcon"),t.type&&i.classList.add(...t.type.split(/\s+/g).map(s=>"cm-completionIcon-"+s)),i.setAttribute("aria-hidden","true"),i},position:20}),e.push({render(t,i,s){let r=document.createElement("span");r.className="cm-completionLabel";let{label:o}=t,l=0;for(let h=1;hl&&r.appendChild(document.createTextNode(o.slice(l,a)));let f=r.appendChild(document.createElement("span"));f.appendChild(document.createTextNode(o.slice(a,c))),f.className="cm-completionMatchedText",l=c}return lt.position-i.position).map(t=>t.render)}function No(n,e,t){if(n<=t)return{from:0,to:n};if(e<=n>>1){let s=Math.floor(e/t);return{from:s*t,to:(s+1)*t}}let i=Math.floor((n-e)/t);return{from:n-(i+1)*t,to:n-i*t}}class Sp{constructor(e,t){this.view=e,this.stateField=t,this.info=null,this.placeInfo={read:()=>this.measureInfo(),write:l=>this.positionInfo(l),key:this};let i=e.state.field(t),{options:s,selected:r}=i.open,o=e.state.facet(lt);this.optionContent=kp(o),this.optionClass=o.optionClass,this.range=No(s.length,r,o.maxRenderedOptions),this.dom=document.createElement("div"),this.dom.className="cm-tooltip-autocomplete",this.dom.addEventListener("mousedown",l=>{for(let h=l.target,a;h&&h!=this.dom;h=h.parentNode)if(h.nodeName=="LI"&&(a=/-(\d+)$/.exec(h.id))&&+a[1]{this.info&&this.view.requestMeasure(this.placeInfo)})}mount(){this.updateSel()}update(e){e.state.field(this.stateField)!=e.startState.field(this.stateField)&&this.updateSel()}positioned(){this.info&&this.view.requestMeasure(this.placeInfo)}updateSel(){let e=this.view.state.field(this.stateField),t=e.open;if((t.selected=this.range.to)&&(this.range=No(t.options.length,t.selected,this.view.state.facet(lt).maxRenderedOptions),this.list.remove(),this.list=this.dom.appendChild(this.createListBox(t.options,e.id,this.range)),this.list.addEventListener("scroll",()=>{this.info&&this.view.requestMeasure(this.placeInfo)})),this.updateSelectedOption(t.selected)){this.info&&(this.info.remove(),this.info=null);let{completion:i}=t.options[t.selected],{info:s}=i;if(!s)return;let r=typeof s=="string"?document.createTextNode(s):s(i);if(!r)return;"then"in r?r.then(o=>{o&&this.view.state.field(this.stateField,!1)==e&&this.addInfoPane(o)}).catch(o=>Pe(this.view.state,o,"completion info")):this.addInfoPane(r)}}addInfoPane(e){let t=this.info=document.createElement("div");t.className="cm-tooltip cm-completionInfo",t.appendChild(e),this.dom.appendChild(t),this.view.requestMeasure(this.placeInfo)}updateSelectedOption(e){let t=null;for(let i=this.list.firstChild,s=this.range.from;i;i=i.nextSibling,s++)s==e?i.hasAttribute("aria-selected")||(i.setAttribute("aria-selected","true"),t=i):i.hasAttribute("aria-selected")&&i.removeAttribute("aria-selected");return t&&Cp(this.list,t),t}measureInfo(){let e=this.dom.querySelector("[aria-selected]");if(!e||!this.info)return null;let t=this.dom.getBoundingClientRect(),i=this.info.getBoundingClientRect(),s=e.getBoundingClientRect();if(s.top>Math.min(innerHeight,t.bottom)-10||s.bottomnew Sp(e,n)}function Cp(n,e){let t=n.getBoundingClientRect(),i=e.getBoundingClientRect();i.topt.bottom&&(n.scrollTop+=i.bottom-t.bottom)}function Vo(n){return(n.boost||0)*100+(n.apply?10:0)+(n.info?5:0)+(n.type?1:0)}function Ap(n,e){let t=[],i=0;for(let o of n)if(o.hasResult())if(o.result.filter===!1){let l=o.result.getMatch;for(let h of o.result.options){let a=[1e9-i++];if(l)for(let c of l(h))a.push(c);t.push(new Eo(h,o,a))}}else{let l=new wp(e.sliceDoc(o.from,o.to)),h;for(let a of o.result.options)(h=l.match(a.label))&&(a.boost!=null&&(h[0]+=a.boost),t.push(new Eo(a,o,h)))}let s=[],r=null;for(let o of t.sort(Tp))!r||r.label!=o.completion.label||r.detail!=o.completion.detail||r.type!=null&&o.completion.type!=null&&r.type!=o.completion.type||r.apply!=o.completion.apply?s.push(o):Vo(o.completion)>Vo(r)&&(s[s.length-1]=o),r=o.completion;return s}class ei{constructor(e,t,i,s,r){this.options=e,this.attrs=t,this.tooltip=i,this.timestamp=s,this.selected=r}setSelected(e,t){return e==this.selected||e>=this.options.length?this:new ei(this.options,Fo(t,e),this.tooltip,this.timestamp,e)}static build(e,t,i,s,r){let o=Ap(e,t);if(!o.length)return null;let l=0;if(s&&s.selected){let h=s.options[s.selected].completion;for(let a=0;aa.hasResult()?Math.min(h,a.from):h,1e8),create:vp(De),above:r.aboveCursor},s?s.timestamp:Date.now(),l)}map(e){return new ei(this.options,this.attrs,Object.assign(Object.assign({},this.tooltip),{pos:e.mapPos(this.tooltip.pos)}),this.timestamp,this.selected)}}class nn{constructor(e,t,i){this.active=e,this.id=t,this.open=i}static start(){return new nn(Op,"cm-ac-"+Math.floor(Math.random()*2e6).toString(36),null)}update(e){let{state:t}=e,i=t.facet(lt),r=(i.override||t.languageDataAt("autocomplete",nt(t)).map(bp)).map(l=>(this.active.find(a=>a.source==l)||new ge(l,this.active.some(a=>a.state!=0)?1:0)).update(e,i));r.length==this.active.length&&r.every((l,h)=>l==this.active[h])&&(r=this.active);let o=e.selection||r.some(l=>l.hasResult()&&e.changes.touchesRange(l.from,l.to))||!Mp(r,this.active)?ei.build(r,t,this.id,this.open,i):this.open&&e.docChanged?this.open.map(e.changes):this.open;!o&&r.every(l=>l.state!=1)&&r.some(l=>l.hasResult())&&(r=r.map(l=>l.hasResult()?new ge(l.source,0):l));for(let l of e.effects)l.is(pa)&&(o=o&&o.setSelected(l.value,this.id));return r==this.active&&o==this.open?this:new nn(r,this.id,o)}get tooltip(){return this.open?this.open.tooltip:null}get attrs(){return this.open?this.open.attrs:Dp}}function Mp(n,e){if(n==e)return!0;for(let t=0,i=0;;){for(;to||t=="delete"&&nt(e.startState)==this.from)return new ge(this.source,t=="input"&&i.activateOnTyping?1:0);let h=this.explicitPos<0?-1:e.changes.mapPos(this.explicitPos),a;return Bp(this.result.validFor,e.state,r,o)?new ti(this.source,h,this.result,r,o):this.result.update&&(a=this.result.update(this.result,r,o,new ca(e.state,l,h>=0)))?new ti(this.source,h,a,a.from,(s=a.to)!==null&&s!==void 0?s:nt(e.state)):new ge(this.source,1,h)}handleChange(e){return e.changes.touchesRange(this.from,this.to)?new ge(this.source,0):this.map(e.changes)}map(e){return e.empty?this:new ti(this.source,this.explicitPos<0?-1:e.mapPos(this.explicitPos),this.result,e.mapPos(this.from),e.mapPos(this.to,1))}}function Bp(n,e,t,i){if(!n)return!1;let s=e.sliceDoc(t,i);return typeof n=="function"?n(s,t,i,e):fa(n,!0).test(s)}const sr=H.define(),sn=H.define(),da=H.define({map(n,e){return n.map(t=>t.map(e))}}),pa=H.define(),De=ke.define({create(){return nn.start()},update(n,e){return n.update(e)},provide:n=>[ih.from(n,e=>e.tooltip),O.contentAttributes.from(n,e=>e.attrs)]}),ga=75;function Pi(n,e="option"){return t=>{let i=t.state.field(De,!1);if(!i||!i.open||Date.now()-i.open.timestamp=l&&(o=e=="page"?l-1:0),t.dispatch({effects:pa.of(o)}),!0}}const Pp=n=>{let e=n.state.field(De,!1);return n.state.readOnly||!e||!e.open||Date.now()-e.open.timestampn.state.field(De,!1)?(n.dispatch({effects:sr.of(!0)}),!0):!1,Lp=n=>{let e=n.state.field(De,!1);return!e||!e.active.some(t=>t.state!=0)?!1:(n.dispatch({effects:sn.of(null)}),!0)};class Ep{constructor(e,t){this.active=e,this.context=t,this.time=Date.now(),this.updates=[],this.done=void 0}}const Ho=50,Ip=50,Np=1e3,Vp=ue.fromClass(class{constructor(n){this.view=n,this.debounceUpdate=-1,this.running=[],this.debounceAccept=-1,this.composing=0;for(let e of n.state.field(De).active)e.state==1&&this.startQuery(e)}update(n){let e=n.state.field(De);if(!n.selectionSet&&!n.docChanged&&n.startState.field(De)==e)return;let t=n.transactions.some(i=>(i.selection||i.docChanged)&&!Bs(i));for(let i=0;iIp&&Date.now()-s.time>Np){for(let r of s.context.abortListeners)try{r()}catch(o){Pe(this.view.state,o)}s.context.abortListeners=null,this.running.splice(i--,1)}else s.updates.push(...n.transactions)}if(this.debounceUpdate>-1&&clearTimeout(this.debounceUpdate),this.debounceUpdate=e.active.some(i=>i.state==1&&!this.running.some(s=>s.active.source==i.source))?setTimeout(()=>this.startUpdate(),Ho):-1,this.composing!=0)for(let i of n.transactions)Bs(i)=="input"?this.composing=2:this.composing==2&&i.selection&&(this.composing=3)}startUpdate(){this.debounceUpdate=-1;let{state:n}=this.view,e=n.field(De);for(let t of e.active)t.state==1&&!this.running.some(i=>i.active.source==t.source)&&this.startQuery(t)}startQuery(n){let{state:e}=this.view,t=nt(e),i=new ca(e,t,n.explicitPos==t),s=new Ep(n,i);this.running.push(s),Promise.resolve(n.source(i)).then(r=>{s.context.aborted||(s.done=r||null,this.scheduleAccept())},r=>{this.view.dispatch({effects:sn.of(null)}),Pe(this.view.state,r)})}scheduleAccept(){this.running.every(n=>n.done!==void 0)?this.accept():this.debounceAccept<0&&(this.debounceAccept=setTimeout(()=>this.accept(),Ho))}accept(){var n;this.debounceAccept>-1&&clearTimeout(this.debounceAccept),this.debounceAccept=-1;let e=[],t=this.view.state.facet(lt);for(let i=0;io.source==s.active.source);if(r&&r.state==1)if(s.done==null){let o=new ge(s.active.source,0);for(let l of s.updates)o=o.update(l,t);o.state!=1&&e.push(o)}else this.startQuery(r)}e.length&&this.view.dispatch({effects:da.of(e)})}},{eventHandlers:{blur(){let n=this.view.state.field(De,!1);n&&n.tooltip&&this.view.state.facet(lt).closeOnBlur&&this.view.dispatch({effects:sn.of(null)})},compositionstart(){this.composing=1},compositionend(){this.composing==3&&setTimeout(()=>this.view.dispatch({effects:sr.of(!1)}),20),this.composing=0}}}),Fp=O.baseTheme({".cm-tooltip.cm-tooltip-autocomplete":{"& > ul":{fontFamily:"monospace",whiteSpace:"nowrap",overflow:"hidden auto",maxWidth_fallback:"700px",maxWidth:"min(700px, 95vw)",minWidth:"250px",maxHeight:"10em",listStyle:"none",margin:0,padding:0,"& > li":{overflowX:"hidden",textOverflow:"ellipsis",cursor:"pointer",padding:"1px 3px",lineHeight:1.2}}},"&light .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#17c",color:"white"},"&dark .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#347",color:"white"},".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after":{content:'"\xB7\xB7\xB7"',opacity:.5,display:"block",textAlign:"center"},".cm-tooltip.cm-completionInfo":{position:"absolute",padding:"3px 9px",width:"max-content",maxWidth:"300px"},".cm-completionInfo.cm-completionInfo-left":{right:"100%"},".cm-completionInfo.cm-completionInfo-right":{left:"100%"},"&light .cm-snippetField":{backgroundColor:"#00000022"},"&dark .cm-snippetField":{backgroundColor:"#ffffff22"},".cm-snippetFieldPosition":{verticalAlign:"text-top",width:0,height:"1.15em",margin:"0 -0.7px -.7em",borderLeft:"1.4px dotted #888"},".cm-completionMatchedText":{textDecoration:"underline"},".cm-completionDetail":{marginLeft:"0.5em",fontStyle:"italic"},".cm-completionIcon":{fontSize:"90%",width:".8em",display:"inline-block",textAlign:"center",paddingRight:".6em",opacity:"0.6"},".cm-completionIcon-function, .cm-completionIcon-method":{"&:after":{content:"'\u0192'"}},".cm-completionIcon-class":{"&:after":{content:"'\u25CB'"}},".cm-completionIcon-interface":{"&:after":{content:"'\u25CC'"}},".cm-completionIcon-variable":{"&:after":{content:"'\u{1D465}'"}},".cm-completionIcon-constant":{"&:after":{content:"'\u{1D436}'"}},".cm-completionIcon-type":{"&:after":{content:"'\u{1D461}'"}},".cm-completionIcon-enum":{"&:after":{content:"'\u222A'"}},".cm-completionIcon-property":{"&:after":{content:"'\u25A1'"}},".cm-completionIcon-keyword":{"&:after":{content:"'\u{1F511}\uFE0E'"}},".cm-completionIcon-namespace":{"&:after":{content:"'\u25A2'"}},".cm-completionIcon-text":{"&:after":{content:"'abc'",fontSize:"50%",verticalAlign:"middle"}}}),rn={brackets:["(","[","{","'",'"'],before:")]}:;>"},ft=H.define({map(n,e){let t=e.mapPos(n,-1,me.TrackAfter);return t==null?void 0:t}}),rr=H.define({map(n,e){return e.mapPos(n)}}),or=new class extends pt{};or.startSide=1;or.endSide=-1;const ma=ke.define({create(){return Y.empty},update(n,e){if(e.selection){let t=e.state.doc.lineAt(e.selection.main.head).from,i=e.startState.doc.lineAt(e.startState.selection.main.head).from;t!=e.changes.mapPos(i,-1)&&(n=Y.empty)}n=n.map(e.changes);for(let t of e.effects)t.is(ft)?n=n.update({add:[or.range(t.value,t.value+1)]}):t.is(rr)&&(n=n.update({filter:i=>i!=t.value}));return n}});function Hp(){return[zp,ma]}const qn="()[]{}<>";function ya(n){for(let e=0;e{if((Wp?n.composing:n.compositionStarted)||n.state.readOnly)return!1;let s=n.state.selection.main;if(i.length>2||i.length==2&&ve(re(i,0))==1||e!=s.from||t!=s.to)return!1;let r=Up(n.state,i);return r?(n.dispatch(r),!0):!1}),qp=({state:n,dispatch:e})=>{if(n.readOnly)return!1;let i=ba(n,n.selection.main.head).brackets||rn.brackets,s=null,r=n.changeByRange(o=>{if(o.empty){let l=jp(n.doc,o.head);for(let h of i)if(h==l&&wn(n.doc,o.head)==ya(re(h,0)))return{changes:{from:o.head-h.length,to:o.head+h.length},range:m.cursor(o.head-h.length),userEvent:"delete.backward"}}return{range:s=o}});return s||e(n.update(r,{scrollIntoView:!0})),!s},Kp=[{key:"Backspace",run:qp}];function Up(n,e){let t=ba(n,n.selection.main.head),i=t.brackets||rn.brackets;for(let s of i){let r=ya(re(s,0));if(e==s)return r==s?$p(n,s,i.indexOf(s+s+s)>-1):Gp(n,s,r,t.before||rn.before);if(e==r&&wa(n,n.selection.main.from))return Jp(n,s,r)}return null}function wa(n,e){let t=!1;return n.field(ma).between(0,n.doc.length,i=>{i==e&&(t=!0)}),t}function wn(n,e){let t=n.sliceString(e,e+2);return t.slice(0,ve(re(t,0)))}function jp(n,e){let t=n.sliceString(e-2,e);return ve(re(t,0))==t.length?t:t.slice(1)}function Gp(n,e,t,i){let s=null,r=n.changeByRange(o=>{if(!o.empty)return{changes:[{insert:e,from:o.from},{insert:t,from:o.to}],effects:ft.of(o.to+e.length),range:m.range(o.anchor+e.length,o.head+e.length)};let l=wn(n.doc,o.head);return!l||/\s/.test(l)||i.indexOf(l)>-1?{changes:{insert:e+t,from:o.head},effects:ft.of(o.head+e.length),range:m.cursor(o.head+e.length)}:{range:s=o}});return s?null:n.update(r,{scrollIntoView:!0,userEvent:"input.type"})}function Jp(n,e,t){let i=null,s=n.selection.ranges.map(r=>r.empty&&wn(n.doc,r.head)==t?m.cursor(r.head+t.length):i=r);return i?null:n.update({selection:m.create(s,n.selection.mainIndex),scrollIntoView:!0,effects:n.selection.ranges.map(({from:r})=>rr.of(r))})}function $p(n,e,t){let i=null,s=n.changeByRange(r=>{if(!r.empty)return{changes:[{insert:e,from:r.from},{insert:e,from:r.to}],effects:ft.of(r.to+e.length),range:m.range(r.anchor+e.length,r.head+e.length)};let o=r.head,l=wn(n.doc,o);if(l==e){if(Wo(n,o))return{changes:{insert:e+e,from:o},effects:ft.of(o+e.length),range:m.cursor(o+e.length)};if(wa(n,o)){let h=t&&n.sliceDoc(o,o+e.length*3)==e+e+e;return{range:m.cursor(o+e.length*(h?3:1)),effects:rr.of(o)}}}else{if(t&&n.sliceDoc(o-2*e.length,o)==e+e&&Wo(n,o-2*e.length))return{changes:{insert:e+e+e+e,from:o},effects:ft.of(o+e.length),range:m.cursor(o+e.length)};if(n.charCategorizer(o)(l)!=ce.Word){let h=n.sliceDoc(o-1,o);if(h!=e&&n.charCategorizer(o)(h)!=ce.Word&&!Xp(n,o,e))return{changes:{insert:e+e,from:o},effects:ft.of(o+e.length),range:m.cursor(o+e.length)}}}return{range:i=r}});return i?null:n.update(s,{scrollIntoView:!0,userEvent:"input.type"})}function Wo(n,e){let t=xe(n).resolveInner(e+1);return t.parent&&t.from==e}function Xp(n,e,t){console.log("check pos",e);let i=xe(n).resolveInner(e,-1);for(let s=0;s<5;s++){if(n.sliceDoc(i.from,i.from+t.length)==t){let o=i.firstChild;for(;o&&o.from==i.from&&o.to-o.from>t.length;){if(n.sliceDoc(o.to-t.length,o.to)==t)return!1;o=o.firstChild}return!0}let r=i.to==e&&i.parent;if(!r)break;i=r}return!1}function Yp(n={}){return[De,lt.of(n),Vp,_p,Fp]}const xa=[{key:"Ctrl-Space",run:Rp},{key:"Escape",run:Lp},{key:"ArrowDown",run:Pi(!0)},{key:"ArrowUp",run:Pi(!1)},{key:"PageDown",run:Pi(!0,"page")},{key:"PageUp",run:Pi(!1,"page")},{key:"Enter",run:Pp}],_p=fi.highest(Ws.computeN([lt],n=>n.facet(lt).defaultKeymap?[xa]:[]));function Qp(n){ka(n,"start");var e={},t=n.languageData||{},i=!1;for(var s in n)if(s!=t&&n.hasOwnProperty(s))for(var r=e[s]=[],o=n[s],l=0;l2&&o.token&&typeof o.token!="string"){t.pending=[];for(var a=2;a-1)return null;var s=t.indent.length-1,r=n[t.state];e:for(;;){for(var o=0;ot(11,s=C));const r=Pa();let{value:o=""}=e,{disabled:l=!1}=e,{placeholder:h=""}=e,{baseCollection:a=new Ra}=e,{singleLine:c=!1}=e,{extraAutocompleteKeys:f=[]}=e,{disableRequestKeys:u=!1}=e,{disableIndirectCollectionsKeys:d=!1}=e,p,g,y=new _e,b=new _e,v=new _e,A=new _e;function x(){p==null||p.focus()}function S(C){let E=C.slice();return kn.pushOrReplaceByKey(E,a,"id"),E}function D(){g==null||g.dispatchEvent(new CustomEvent("change",{detail:{value:o},bubbles:!0}))}function B(C,E="",R=0){let K=i.find(I=>I.name==C||I.id==C);if(!K||R>=4)return[];let q=[E+"id",E+"created",E+"updated"];for(const I of K.schema){const ne=E+I.name;if(I.type==="relation"&&I.options.collectionId){const de=B(I.options.collectionId,ne+".",R+1);de.length?q=q.concat(de):q.push(ne)}else q.push(ne)}return q}function U(C=!0,E=!0){let R=[].concat(f);const K=B(a.name);for(const q of K)R.push(q);if(C&&(R.push("@request.method"),R.push("@request.query."),R.push("@request.data."),R.push("@request.user.id"),R.push("@request.user.email"),R.push("@request.user.verified"),R.push("@request.user.created"),R.push("@request.user.updated")),C||E)for(const q of i){let I="";if(q.name==="profiles"){if(!C)continue;I="@request.user.profile."}else{if(!E)continue;I="@collection."+q.name+"."}const ne=B(q.name,I);for(const de of ne)R.push(de)}return R.sort(function(q,I){return I.length-q.length}),R}function N(C){let E=C.matchBefore(/[\@\w\.]*/);if(E.from==E.to&&!C.explicit)return null;let R=[{label:"null"},{label:"false"},{label:"true"}];d||R.push({label:"@collection.*",apply:"@collection."});const K=["@request.user.profile.id","@request.user.profile.userId","@request.user.profile.created","@request.user.profile.updated"],q=U(!u,!u&&E.text.startsWith("@c"));for(const I of q)K.includes(I)||R.push({label:I.endsWith(".")?I+"*":I,apply:I});return{from:E.from,options:R}}function L(){const C=[],E=U(!u,!d);for(const R of E){let K;R.endsWith(".")?K=kn.escapeRegExp(R)+"\\w+[\\w.]*":K=kn.escapeRegExp(R),C.push({regex:K,token:"keyword"})}return C}function W(){return Js.define(Qp({start:[{regex:/true|false|null/,token:"atom"},{regex:/"(?:[^\\]|\\.)*?(?:"|$)/,token:"string"},{regex:/'(?:[^\\]|\\.)*?(?:'|$)/,token:"string"},{regex:/0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,token:"number"},{regex:/\&\&|\|\||\=|\!\=|\~|\!\~|\>|\<|\>\=|\<\=/,token:"operator"},{regex:/[\{\[\(]/,indent:!0},{regex:/[\}\]\)]/,dedent:!0}].concat(L())}))}La(()=>{const C={key:"Enter",run:E=>{c&&r("submit",o)}};return t(10,p=new O({parent:g,state:V.create({doc:o,extensions:[Xf(),Bf(),_u(),bf(),Cf(),V.allowMultipleSelections.of(!0),ku(Cu,{fallback:!0}),Pu(),Hp(),Wf(),$d(),Ws.of([C,...Kp,...Kd,...fp,...od,...xa]),O.lineWrapping,Yp({override:[N],icons:!1}),A.of(ro(h)),b.of(O.editable.of(!0)),v.of(V.readOnly.of(!1)),y.of(W()),V.transactionFilter.of(E=>c&&E.newDoc.lines>1?[]:E),O.updateListener.of(E=>{!E.docChanged||l||(t(1,o=E.state.doc.toString()),D())})]})})),()=>p==null?void 0:p.destroy()});function G(C){Ea[C?"unshift":"push"](()=>{g=C,t(0,g)})}return n.$$set=C=>{"value"in C&&t(1,o=C.value),"disabled"in C&&t(2,l=C.disabled),"placeholder"in C&&t(3,h=C.placeholder),"baseCollection"in C&&t(4,a=C.baseCollection),"singleLine"in C&&t(5,c=C.singleLine),"extraAutocompleteKeys"in C&&t(6,f=C.extraAutocompleteKeys),"disableRequestKeys"in C&&t(7,u=C.disableRequestKeys),"disableIndirectCollectionsKeys"in C&&t(8,d=C.disableIndirectCollectionsKeys)},n.$$.update=()=>{n.$$.dirty&2048&&(i=S(s)),n.$$.dirty&1040&&p&&(a==null?void 0:a.schema)&&p.dispatch({effects:[y.reconfigure(W())]}),n.$$.dirty&1028&&p&&typeof l!="undefined"&&p.dispatch({effects:[b.reconfigure(O.editable.of(!l)),v.reconfigure(V.readOnly.of(l))]}),n.$$.dirty&1026&&p&&o!=p.state.doc.toString()&&p.dispatch({changes:{from:0,to:p.state.doc.length,insert:o}}),n.$$.dirty&1032&&p&&typeof h!="undefined"&&p.dispatch({effects:[A.reconfigure(ro(h))]})},[g,o,l,h,a,c,f,u,d,x,p,s,G]}class hg extends Sa{constructor(e){super(),va(this,e,rg,sg,Ca,{value:1,disabled:2,placeholder:3,baseCollection:4,singleLine:5,extraAutocompleteKeys:6,disableRequestKeys:7,disableIndirectCollectionsKeys:8,focus:9})}get focus(){return this.$$.ctx[9]}}export{hg as default}; diff --git a/ui/dist/assets/NotFoundPage.8b4364cc.js b/ui/dist/assets/NotFoundPage.8b4364cc.js new file mode 100644 index 00000000..a83426e8 --- /dev/null +++ b/ui/dist/assets/NotFoundPage.8b4364cc.js @@ -0,0 +1 @@ +import{S as t,i as n,s as o,K as a}from"./index.944ee0db.js";function l(e){return a("/collections"),[]}class c extends t{constructor(s){super(),n(this,s,l,null,o,{})}}export{c as default}; diff --git a/ui/dist/assets/PageAdminConfirmPasswordReset.a49a8974.js b/ui/dist/assets/PageAdminConfirmPasswordReset.a49a8974.js new file mode 100644 index 00000000..6abd2591 --- /dev/null +++ b/ui/dist/assets/PageAdminConfirmPasswordReset.a49a8974.js @@ -0,0 +1,2 @@ +import{S as I,i as K,s as L,F as W,f as F,m as H,n as B,o as J,q as N,H as M,D as q,e as c,g as u,h as b,j as _,E as O,p as w,z,d as k,A as D,B as T,C as Q,k as U,v as V,I as X,y as E,J as Y,K as Z,G as S}from"./index.944ee0db.js";function G(f){let e,o,s;return{c(){e=q("for "),o=c("strong"),s=q(f[3]),u(o,"class","txt-nowrap")},m(l,t){b(l,e,t),b(l,o,t),_(o,s)},p(l,t){t&8&&O(s,l[3])},d(l){l&&w(e),l&&w(o)}}}function x(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0,t.autofocus=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[0]),t.focus(),p||(d=T(t,"input",f[6]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&1&&t.value!==n[0]&&S(t,n[0])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function ee(f){let e,o,s,l,t,r,p,d;return{c(){e=c("label"),o=q("New password confirm"),l=k(),t=c("input"),u(e,"for",s=f[8]),u(t,"type","password"),u(t,"id",r=f[8]),t.required=!0},m(n,i){b(n,e,i),_(e,o),b(n,l,i),b(n,t,i),S(t,f[1]),p||(d=T(t,"input",f[7]),p=!0)},p(n,i){i&256&&s!==(s=n[8])&&u(e,"for",s),i&256&&r!==(r=n[8])&&u(t,"id",r),i&2&&t.value!==n[1]&&S(t,n[1])},d(n){n&&w(e),n&&w(l),n&&w(t),p=!1,d()}}}function te(f){let e,o,s,l,t,r,p,d,n,i,g,R,C,v,P,A,h,m=f[3]&&G(f);return r=new z({props:{class:"form-field required",name:"password",$$slots:{default:[x,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),d=new z({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[ee,({uniqueId:a})=>({8:a}),({uniqueId:a})=>a?256:0]},$$scope:{ctx:f}}}),{c(){e=c("form"),o=c("div"),s=c("h4"),l=q(`Reset your admin password + `),m&&m.c(),t=k(),F(r.$$.fragment),p=k(),F(d.$$.fragment),n=k(),i=c("button"),g=c("span"),g.textContent="Set new password",R=k(),C=c("div"),v=c("a"),v.textContent="Back to login",u(s,"class","m-b-xs"),u(o,"class","content txt-center m-b-sm"),u(g,"class","txt"),u(i,"type","submit"),u(i,"class","btn btn-lg btn-block"),i.disabled=f[2],D(i,"btn-loading",f[2]),u(e,"class","m-b-base"),u(v,"href","/login"),u(v,"class","link-hint"),u(C,"class","content txt-center")},m(a,$){b(a,e,$),_(e,o),_(o,s),_(s,l),m&&m.m(s,null),_(e,t),H(r,e,null),_(e,p),H(d,e,null),_(e,n),_(e,i),_(i,g),b(a,R,$),b(a,C,$),_(C,v),P=!0,A||(h=[T(e,"submit",Q(f[4])),U(V.call(null,v))],A=!0)},p(a,$){a[3]?m?m.p(a,$):(m=G(a),m.c(),m.m(s,null)):m&&(m.d(1),m=null);const j={};$&769&&(j.$$scope={dirty:$,ctx:a}),r.$set(j);const y={};$&770&&(y.$$scope={dirty:$,ctx:a}),d.$set(y),(!P||$&4)&&(i.disabled=a[2]),$&4&&D(i,"btn-loading",a[2])},i(a){P||(B(r.$$.fragment,a),B(d.$$.fragment,a),P=!0)},o(a){J(r.$$.fragment,a),J(d.$$.fragment,a),P=!1},d(a){a&&w(e),m&&m.d(),N(r),N(d),a&&w(R),a&&w(C),A=!1,X(h)}}}function se(f){let e,o;return e=new W({props:{$$slots:{default:[te]},$$scope:{ctx:f}}}),{c(){F(e.$$.fragment)},m(s,l){H(e,s,l),o=!0},p(s,[l]){const t={};l&527&&(t.$$scope={dirty:l,ctx:s}),e.$set(t)},i(s){o||(B(e.$$.fragment,s),o=!0)},o(s){J(e.$$.fragment,s),o=!1},d(s){N(e,s)}}}function le(f,e,o){let s,{params:l}=e,t="",r="",p=!1;async function d(){if(!p){o(2,p=!0);try{await E.Admins.confirmPasswordReset(l==null?void 0:l.token,t,r),Y("Successfully set a new admin password."),Z("/")}catch(g){E.errorResponseHandler(g)}o(2,p=!1)}}function n(){t=this.value,o(0,t)}function i(){r=this.value,o(1,r)}return f.$$set=g=>{"params"in g&&o(5,l=g.params)},f.$$.update=()=>{f.$$.dirty&32&&o(3,s=M.getJWTPayload(l==null?void 0:l.token).email||"")},[t,r,p,s,d,l,n,i]}class ae extends I{constructor(e){super(),K(this,e,le,se,L,{params:5})}}export{ae as default}; diff --git a/ui/dist/assets/PageAdminRequestPasswordReset.f5bd52f0.js b/ui/dist/assets/PageAdminRequestPasswordReset.f5bd52f0.js new file mode 100644 index 00000000..112e008e --- /dev/null +++ b/ui/dist/assets/PageAdminRequestPasswordReset.f5bd52f0.js @@ -0,0 +1,2 @@ +import{S,i as B,s as M,F as T,f as A,m as E,n as w,o as y,q as H,d as g,e as _,g as p,h as k,j as d,k as j,v as z,w as D,x as G,p as v,y as C,z as N,A as F,B as L,C as I,D as h,E as J,u as P,G as R}from"./index.944ee0db.js";function K(c){let e,s,n,l,t,o,f,m,i,a,b,u;return l=new N({props:{class:"form-field required",name:"email",$$slots:{default:[Q,({uniqueId:r})=>({5:r}),({uniqueId:r})=>r?32:0]},$$scope:{ctx:c}}}),{c(){e=_("form"),s=_("div"),s.innerHTML=`

    Forgotten admin password

    +

    Enter the email associated with your account and we\u2019ll send you a recovery link:

    `,n=g(),A(l.$$.fragment),t=g(),o=_("button"),f=_("i"),m=g(),i=_("span"),i.textContent="Send recovery link",p(s,"class","content txt-center m-b-sm"),p(f,"class","ri-mail-send-line"),p(i,"class","txt"),p(o,"type","submit"),p(o,"class","btn btn-lg btn-block"),o.disabled=c[1],F(o,"btn-loading",c[1]),p(e,"class","m-b-base")},m(r,$){k(r,e,$),d(e,s),d(e,n),E(l,e,null),d(e,t),d(e,o),d(o,f),d(o,m),d(o,i),a=!0,b||(u=L(e,"submit",I(c[3])),b=!0)},p(r,$){const q={};$&97&&(q.$$scope={dirty:$,ctx:r}),l.$set(q),(!a||$&2)&&(o.disabled=r[1]),$&2&&F(o,"btn-loading",r[1])},i(r){a||(w(l.$$.fragment,r),a=!0)},o(r){y(l.$$.fragment,r),a=!1},d(r){r&&v(e),H(l),b=!1,u()}}}function O(c){let e,s,n,l,t,o,f,m,i;return{c(){e=_("div"),s=_("div"),s.innerHTML='',n=g(),l=_("div"),t=_("p"),o=h("Check "),f=_("strong"),m=h(c[0]),i=h(" for the recovery link."),p(s,"class","icon"),p(f,"class","txt-nowrap"),p(l,"class","content"),p(e,"class","alert alert-success")},m(a,b){k(a,e,b),d(e,s),d(e,n),d(e,l),d(l,t),d(t,o),d(t,f),d(f,m),d(t,i)},p(a,b){b&1&&J(m,a[0])},i:P,o:P,d(a){a&&v(e)}}}function Q(c){let e,s,n,l,t,o,f,m;return{c(){e=_("label"),s=h("Email"),l=g(),t=_("input"),p(e,"for",n=c[5]),p(t,"type","email"),p(t,"id",o=c[5]),t.required=!0,t.autofocus=!0},m(i,a){k(i,e,a),d(e,s),k(i,l,a),k(i,t,a),R(t,c[0]),t.focus(),f||(m=L(t,"input",c[4]),f=!0)},p(i,a){a&32&&n!==(n=i[5])&&p(e,"for",n),a&32&&o!==(o=i[5])&&p(t,"id",o),a&1&&t.value!==i[0]&&R(t,i[0])},d(i){i&&v(e),i&&v(l),i&&v(t),f=!1,m()}}}function U(c){let e,s,n,l,t,o,f,m;const i=[O,K],a=[];function b(u,r){return u[2]?0:1}return e=b(c),s=a[e]=i[e](c),{c(){s.c(),n=g(),l=_("div"),t=_("a"),t.textContent="Back to login",p(t,"href","/login"),p(t,"class","link-hint"),p(l,"class","content txt-center")},m(u,r){a[e].m(u,r),k(u,n,r),k(u,l,r),d(l,t),o=!0,f||(m=j(z.call(null,t)),f=!0)},p(u,r){let $=e;e=b(u),e===$?a[e].p(u,r):(D(),y(a[$],1,1,()=>{a[$]=null}),G(),s=a[e],s?s.p(u,r):(s=a[e]=i[e](u),s.c()),w(s,1),s.m(n.parentNode,n))},i(u){o||(w(s),o=!0)},o(u){y(s),o=!1},d(u){a[e].d(u),u&&v(n),u&&v(l),f=!1,m()}}}function V(c){let e,s;return e=new T({props:{$$slots:{default:[U]},$$scope:{ctx:c}}}),{c(){A(e.$$.fragment)},m(n,l){E(e,n,l),s=!0},p(n,[l]){const t={};l&71&&(t.$$scope={dirty:l,ctx:n}),e.$set(t)},i(n){s||(w(e.$$.fragment,n),s=!0)},o(n){y(e.$$.fragment,n),s=!1},d(n){H(e,n)}}}function W(c,e,s){let n="",l=!1,t=!1;async function o(){if(!l){s(1,l=!0);try{await C.Admins.requestPasswordReset(n),s(2,t=!0)}catch(m){C.errorResponseHandler(m)}s(1,l=!1)}}function f(){n=this.value,s(0,n)}return[n,l,t,o,f]}class Y extends S{constructor(e){super(),B(this,e,W,V,M,{})}}export{Y as default}; diff --git a/ui/dist/assets/PageUserConfirmEmailChange.172a5083.js b/ui/dist/assets/PageUserConfirmEmailChange.172a5083.js new file mode 100644 index 00000000..e7e8b588 --- /dev/null +++ b/ui/dist/assets/PageUserConfirmEmailChange.172a5083.js @@ -0,0 +1,4 @@ +import{S as z,i as A,s as B,F as D,f as S,m as U,n as $,o as v,q as j,H as G,L as J,h as _,w as M,x as N,p as b,y as P,D as y,e as m,g as d,j as g,E as R,z as W,d as C,A as F,B as E,C as Y,u as h,G as L}from"./index.944ee0db.js";function I(r){let e,s,t,l,n,o,c,a,i,u,k,q,p=r[3]&&T(r);return o=new W({props:{class:"form-field required",name:"password",$$slots:{default:[O,({uniqueId:f})=>({8:f}),({uniqueId:f})=>f?256:0]},$$scope:{ctx:r}}}),{c(){e=m("form"),s=m("div"),t=m("h4"),l=y(`Type your password to confirm changing your email address + `),p&&p.c(),n=C(),S(o.$$.fragment),c=C(),a=m("button"),i=m("span"),i.textContent="Confirm new email",d(t,"class","m-b-xs"),d(s,"class","content txt-center m-b-sm"),d(i,"class","txt"),d(a,"type","submit"),d(a,"class","btn btn-lg btn-block"),a.disabled=r[1],F(a,"btn-loading",r[1])},m(f,w){_(f,e,w),g(e,s),g(s,t),g(t,l),p&&p.m(t,null),g(e,n),U(o,e,null),g(e,c),g(e,a),g(a,i),u=!0,k||(q=E(e,"submit",Y(r[4])),k=!0)},p(f,w){f[3]?p?p.p(f,w):(p=T(f),p.c(),p.m(t,null)):p&&(p.d(1),p=null);const H={};w&769&&(H.$$scope={dirty:w,ctx:f}),o.$set(H),(!u||w&2)&&(a.disabled=f[1]),w&2&&F(a,"btn-loading",f[1])},i(f){u||($(o.$$.fragment,f),u=!0)},o(f){v(o.$$.fragment,f),u=!1},d(f){f&&b(e),p&&p.d(),j(o),k=!1,q()}}}function K(r){let e,s,t,l,n;return{c(){e=m("div"),e.innerHTML=`
    +

    Email address changed

    +

    You can now sign in with your new email address.

    `,s=C(),t=m("button"),t.textContent="Close",d(e,"class","alert alert-success"),d(t,"type","button"),d(t,"class","btn btn-secondary btn-block")},m(o,c){_(o,e,c),_(o,s,c),_(o,t,c),l||(n=E(t,"click",r[6]),l=!0)},p:h,i:h,o:h,d(o){o&&b(e),o&&b(s),o&&b(t),l=!1,n()}}}function T(r){let e,s,t;return{c(){e=y("to "),s=m("strong"),t=y(r[3]),d(s,"class","txt-nowrap")},m(l,n){_(l,e,n),_(l,s,n),g(s,t)},p(l,n){n&8&&R(t,l[3])},d(l){l&&b(e),l&&b(s)}}}function O(r){let e,s,t,l,n,o,c,a;return{c(){e=m("label"),s=y("Password"),l=C(),n=m("input"),d(e,"for",t=r[8]),d(n,"type","password"),d(n,"id",o=r[8]),n.required=!0,n.autofocus=!0},m(i,u){_(i,e,u),g(e,s),_(i,l,u),_(i,n,u),L(n,r[0]),n.focus(),c||(a=E(n,"input",r[7]),c=!0)},p(i,u){u&256&&t!==(t=i[8])&&d(e,"for",t),u&256&&o!==(o=i[8])&&d(n,"id",o),u&1&&n.value!==i[0]&&L(n,i[0])},d(i){i&&b(e),i&&b(l),i&&b(n),c=!1,a()}}}function Q(r){let e,s,t,l;const n=[K,I],o=[];function c(a,i){return a[2]?0:1}return e=c(r),s=o[e]=n[e](r),{c(){s.c(),t=J()},m(a,i){o[e].m(a,i),_(a,t,i),l=!0},p(a,i){let u=e;e=c(a),e===u?o[e].p(a,i):(M(),v(o[u],1,1,()=>{o[u]=null}),N(),s=o[e],s?s.p(a,i):(s=o[e]=n[e](a),s.c()),$(s,1),s.m(t.parentNode,t))},i(a){l||($(s),l=!0)},o(a){v(s),l=!1},d(a){o[e].d(a),a&&b(t)}}}function V(r){let e,s;return e=new D({props:{nobranding:!0,$$slots:{default:[Q]},$$scope:{ctx:r}}}),{c(){S(e.$$.fragment)},m(t,l){U(e,t,l),s=!0},p(t,[l]){const n={};l&527&&(n.$$scope={dirty:l,ctx:t}),e.$set(n)},i(t){s||($(e.$$.fragment,t),s=!0)},o(t){v(e.$$.fragment,t),s=!1},d(t){j(e,t)}}}function X(r,e,s){let t,{params:l}=e,n="",o=!1,c=!1;async function a(){if(!o){s(1,o=!0);try{await P.Users.confirmEmailChange(l==null?void 0:l.token,n),s(2,c=!0)}catch(k){P.errorResponseHandler(k)}s(1,o=!1)}}const i=()=>window.close();function u(){n=this.value,s(0,n)}return r.$$set=k=>{"params"in k&&s(5,l=k.params)},r.$$.update=()=>{r.$$.dirty&32&&s(3,t=G.getJWTPayload(l==null?void 0:l.token).newEmail||"")},[n,o,c,t,a,l,i,u]}class x extends z{constructor(e){super(),A(this,e,X,V,B,{params:5})}}export{x as default}; diff --git a/ui/dist/assets/PageUserConfirmPasswordReset.b297807e.js b/ui/dist/assets/PageUserConfirmPasswordReset.b297807e.js new file mode 100644 index 00000000..5db77ca8 --- /dev/null +++ b/ui/dist/assets/PageUserConfirmPasswordReset.b297807e.js @@ -0,0 +1,4 @@ +import{S as D,i as E,s as G,F as J,f as F,m as L,n as P,o as q,q as N,H as M,L as W,h as _,w as Y,x as I,p as m,y as j,D as y,e as b,j as w,E as K,z,d as C,g as c,A,B as R,C as O,u as h,G as H}from"./index.944ee0db.js";function Q(r){let e,l,t,n,s,o,p,a,i,u,v,g,k,S,d=r[4]&&B(r);return o=new z({props:{class:"form-field required",name:"password",$$slots:{default:[X,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),a=new z({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[Z,({uniqueId:f})=>({10:f}),({uniqueId:f})=>f?1024:0]},$$scope:{ctx:r}}}),{c(){e=b("form"),l=b("div"),t=b("h4"),n=y(`Reset your user password + `),d&&d.c(),s=C(),F(o.$$.fragment),p=C(),F(a.$$.fragment),i=C(),u=b("button"),v=b("span"),v.textContent="Set new password",c(t,"class","m-b-xs"),c(l,"class","content txt-center m-b-sm"),c(v,"class","txt"),c(u,"type","submit"),c(u,"class","btn btn-lg btn-block"),u.disabled=r[2],A(u,"btn-loading",r[2])},m(f,$){_(f,e,$),w(e,l),w(l,t),w(t,n),d&&d.m(t,null),w(e,s),L(o,e,null),w(e,p),L(a,e,null),w(e,i),w(e,u),w(u,v),g=!0,k||(S=R(e,"submit",O(r[5])),k=!0)},p(f,$){f[4]?d?d.p(f,$):(d=B(f),d.c(),d.m(t,null)):d&&(d.d(1),d=null);const T={};$&3073&&(T.$$scope={dirty:$,ctx:f}),o.$set(T);const U={};$&3074&&(U.$$scope={dirty:$,ctx:f}),a.$set(U),(!g||$&4)&&(u.disabled=f[2]),$&4&&A(u,"btn-loading",f[2])},i(f){g||(P(o.$$.fragment,f),P(a.$$.fragment,f),g=!0)},o(f){q(o.$$.fragment,f),q(a.$$.fragment,f),g=!1},d(f){f&&m(e),d&&d.d(),N(o),N(a),k=!1,S()}}}function V(r){let e,l,t,n,s;return{c(){e=b("div"),e.innerHTML=`
    +

    Password changed

    +

    You can now sign in with your new password.

    `,l=C(),t=b("button"),t.textContent="Close",c(e,"class","alert alert-success"),c(t,"type","button"),c(t,"class","btn btn-secondary btn-block")},m(o,p){_(o,e,p),_(o,l,p),_(o,t,p),n||(s=R(t,"click",r[7]),n=!0)},p:h,i:h,o:h,d(o){o&&m(e),o&&m(l),o&&m(t),n=!1,s()}}}function B(r){let e,l,t;return{c(){e=y("for "),l=b("strong"),t=y(r[4])},m(n,s){_(n,e,s),_(n,l,s),w(l,t)},p(n,s){s&16&&K(t,n[4])},d(n){n&&m(e),n&&m(l)}}}function X(r){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password"),n=C(),s=b("input"),c(e,"for",t=r[10]),c(s,"type","password"),c(s,"id",o=r[10]),s.required=!0,s.autofocus=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,s,u),H(s,r[0]),s.focus(),p||(a=R(s,"input",r[8]),p=!0)},p(i,u){u&1024&&t!==(t=i[10])&&c(e,"for",t),u&1024&&o!==(o=i[10])&&c(s,"id",o),u&1&&s.value!==i[0]&&H(s,i[0])},d(i){i&&m(e),i&&m(n),i&&m(s),p=!1,a()}}}function Z(r){let e,l,t,n,s,o,p,a;return{c(){e=b("label"),l=y("New password confirm"),n=C(),s=b("input"),c(e,"for",t=r[10]),c(s,"type","password"),c(s,"id",o=r[10]),s.required=!0},m(i,u){_(i,e,u),w(e,l),_(i,n,u),_(i,s,u),H(s,r[1]),p||(a=R(s,"input",r[9]),p=!0)},p(i,u){u&1024&&t!==(t=i[10])&&c(e,"for",t),u&1024&&o!==(o=i[10])&&c(s,"id",o),u&2&&s.value!==i[1]&&H(s,i[1])},d(i){i&&m(e),i&&m(n),i&&m(s),p=!1,a()}}}function x(r){let e,l,t,n;const s=[V,Q],o=[];function p(a,i){return a[3]?0:1}return e=p(r),l=o[e]=s[e](r),{c(){l.c(),t=W()},m(a,i){o[e].m(a,i),_(a,t,i),n=!0},p(a,i){let u=e;e=p(a),e===u?o[e].p(a,i):(Y(),q(o[u],1,1,()=>{o[u]=null}),I(),l=o[e],l?l.p(a,i):(l=o[e]=s[e](a),l.c()),P(l,1),l.m(t.parentNode,t))},i(a){n||(P(l),n=!0)},o(a){q(l),n=!1},d(a){o[e].d(a),a&&m(t)}}}function ee(r){let e,l;return e=new J({props:{nobranding:!0,$$slots:{default:[x]},$$scope:{ctx:r}}}),{c(){F(e.$$.fragment)},m(t,n){L(e,t,n),l=!0},p(t,[n]){const s={};n&2079&&(s.$$scope={dirty:n,ctx:t}),e.$set(s)},i(t){l||(P(e.$$.fragment,t),l=!0)},o(t){q(e.$$.fragment,t),l=!1},d(t){N(e,t)}}}function te(r,e,l){let t,{params:n}=e,s="",o="",p=!1,a=!1;async function i(){if(!p){l(2,p=!0);try{await j.Users.confirmPasswordReset(n==null?void 0:n.token,s,o),l(3,a=!0)}catch(k){j.errorResponseHandler(k)}l(2,p=!1)}}const u=()=>window.close();function v(){s=this.value,l(0,s)}function g(){o=this.value,l(1,o)}return r.$$set=k=>{"params"in k&&l(6,n=k.params)},r.$$.update=()=>{r.$$.dirty&64&&l(4,t=M.getJWTPayload(n==null?void 0:n.token).email||"")},[s,o,p,a,t,i,n,u,v,g]}class le extends D{constructor(e){super(),E(this,e,te,ee,G,{params:6})}}export{le as default}; diff --git a/ui/dist/assets/PageUserConfirmVerification.07c01eba.js b/ui/dist/assets/PageUserConfirmVerification.07c01eba.js new file mode 100644 index 00000000..007ff84b --- /dev/null +++ b/ui/dist/assets/PageUserConfirmVerification.07c01eba.js @@ -0,0 +1,3 @@ +import{S as k,i as v,s as y,F as w,f as x,m as C,n as g,o as L,q as $,y as H,L as M,h as r,p as a,e as u,d as m,g as f,B as _,u as p}from"./index.944ee0db.js";function P(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`
    +

    Invalid or expired verification token.

    `,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-danger"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[4]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function S(o){let t,s,e,n,i;return{c(){t=u("div"),t.innerHTML=`
    +

    Successfully verified email address.

    `,s=m(),e=u("button"),e.textContent="Close",f(t,"class","alert alert-success"),f(e,"type","button"),f(e,"class","btn btn-secondary btn-block")},m(l,c){r(l,t,c),r(l,s,c),r(l,e,c),n||(i=_(e,"click",o[3]),n=!0)},p,d(l){l&&a(t),l&&a(s),l&&a(e),n=!1,i()}}}function T(o){let t;return{c(){t=u("div"),t.innerHTML='
    Please wait...
    ',f(t,"class","txt-center")},m(s,e){r(s,t,e)},p,d(s){s&&a(t)}}}function q(o){let t;function s(i,l){return i[1]?T:i[0]?S:P}let e=s(o),n=e(o);return{c(){n.c(),t=M()},m(i,l){n.m(i,l),r(i,t,l)},p(i,l){e===(e=s(i))&&n?n.p(i,l):(n.d(1),n=e(i),n&&(n.c(),n.m(t.parentNode,t)))},d(i){n.d(i),i&&a(t)}}}function F(o){let t,s;return t=new w({props:{nobranding:!0,$$slots:{default:[q]},$$scope:{ctx:o}}}),{c(){x(t.$$.fragment)},m(e,n){C(t,e,n),s=!0},p(e,[n]){const i={};n&67&&(i.$$scope={dirty:n,ctx:e}),t.$set(i)},i(e){s||(g(t.$$.fragment,e),s=!0)},o(e){L(t.$$.fragment,e),s=!1},d(e){$(t,e)}}}function U(o,t,s){let{params:e}=t,n=!1,i=!1;l();async function l(){s(1,i=!0);try{await H.Users.confirmVerification(e==null?void 0:e.token),s(0,n=!0)}catch(d){console.warn(d),s(0,n=!1)}s(1,i=!1)}const c=()=>window.close(),b=()=>window.close();return o.$$set=d=>{"params"in d&&s(2,e=d.params)},[n,i,e,c,b]}class B extends k{constructor(t){super(),v(this,t,U,F,y,{params:2})}}export{B as default}; diff --git a/ui/dist/assets/index.35a93598.css b/ui/dist/assets/index.35a93598.css new file mode 100644 index 00000000..d16b970a --- /dev/null +++ b/ui/dist/assets/index.35a93598.css @@ -0,0 +1 @@ +@font-face{font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?v=1) format("woff2"),url(../fonts/remixicon/remixicon.woff?v=1) format("woff"),url(../fonts/remixicon/remixicon.ttf?v=1) format("truetype"),url(../fonts/remixicon/remixicon.svg?v=1#remixicon) format("svg");font-display:swap}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:400;src:local(""),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff2) format("woff2"),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-regular.woff) format("woff")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:400;src:local(""),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff2) format("woff2"),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-italic.woff) format("woff")}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:600;src:local(""),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff2) format("woff2"),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600.woff) format("woff")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:600;src:local(""),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff2) format("woff2"),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-600italic.woff) format("woff")}@font-face{font-family:Source Sans Pro;font-style:normal;font-weight:700;src:local(""),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2) format("woff2"),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff) format("woff")}@font-face{font-family:Source Sans Pro;font-style:italic;font-weight:700;src:local(""),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff2) format("woff2"),url(../fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700italic.woff) format("woff")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400;src:local(""),url(../fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff2) format("woff2"),url(../fonts/jetbrains-mono/jetbrains-mono-v12-latin-regular.woff) format("woff")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:600;src:local(""),url(../fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff2) format("woff2"),url(../fonts/jetbrains-mono/jetbrains-mono-v12-latin-600.woff) format("woff")}:root{--baseFontFamily: "Source Sans Pro", sans-serif, emoji;--monospaceFontFamily: "Ubuntu Mono", monospace, emoji;--iconFontFamily: "remixicon";--txtPrimaryColor: #16161a;--txtHintColor: #666f75;--txtDisabledColor: #adb3b8;--primaryColor: #16161a;--bodyColor: #f8f9fa;--baseColor: #ffffff;--baseAlt1Color: #edf0f3;--baseAlt2Color: #dee3e8;--baseAlt3Color: #a9b4bc;--baseAlt4Color: #7c868d;--infoColor: #3da9fc;--infoAltColor: #d8eefe;--successColor: #2cb67d;--successAltColor: #daf6ea;--dangerColor: #ef4565;--dangerAltColor: #fcdee4;--warningColor: #ff8e3c;--warningAltColor: #ffe7d6;--overlayColor: rgba(88, 95, 101, .3);--tooltipColor: rgba(0, 0, 0, .85);--shadowColor: rgba(0, 0, 0, .05);--baseFontSize: 14.5px;--xsFontSize: 12px;--smFontSize: 13px;--lgFontSize: 15px;--xlFontSize: 16px;--baseLineHeight: 22px;--smLineHeight: 16px;--lgLineHeight: 24px;--inputHeight: 34px;--btnHeight: 40px;--xsBtnHeight: 24px;--smBtnHeight: 30px;--lgBtnHeight: 54px;--baseSpacing: 30px;--xsSpacing: 15px;--smSpacing: 20px;--lgSpacing: 50px;--xlSpacing: 60px;--wrapperWidth: 850px;--smWrapperWidth: 420px;--lgWrapperWidth: 1200px;--appSidebarWidth: 75px;--pageSidebarWidth: 220px;--baseAnimationSpeed: .15s;--activeAnimationSpeed: 70ms;--baseRadius: 3px;--lgRadius: 12px;--btnRadius: 3px;accent-color:var(--primaryColor)}html,body,div,span,applet,object,iframe,h1,h2,.breadcrumbs .breadcrumb-item,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:"";content:none}table{border-collapse:collapse;border-spacing:0}html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}i{font-family:remixicon!important;font-style:normal;font-weight:400;font-size:1.1238rem;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}i:before{vertical-align:top;margin-top:1px;display:inline-block}.ri-24-hours-fill:before{content:"\ea01"}.ri-24-hours-line:before{content:"\ea02"}.ri-4k-fill:before{content:"\ea03"}.ri-4k-line:before{content:"\ea04"}.ri-a-b:before{content:"\ea05"}.ri-account-box-fill:before{content:"\ea06"}.ri-account-box-line:before{content:"\ea07"}.ri-account-circle-fill:before{content:"\ea08"}.ri-account-circle-line:before{content:"\ea09"}.ri-account-pin-box-fill:before{content:"\ea0a"}.ri-account-pin-box-line:before{content:"\ea0b"}.ri-account-pin-circle-fill:before{content:"\ea0c"}.ri-account-pin-circle-line:before{content:"\ea0d"}.ri-add-box-fill:before{content:"\ea0e"}.ri-add-box-line:before{content:"\ea0f"}.ri-add-circle-fill:before{content:"\ea10"}.ri-add-circle-line:before{content:"\ea11"}.ri-add-fill:before{content:"\ea12"}.ri-add-line:before{content:"\ea13"}.ri-admin-fill:before{content:"\ea14"}.ri-admin-line:before{content:"\ea15"}.ri-advertisement-fill:before{content:"\ea16"}.ri-advertisement-line:before{content:"\ea17"}.ri-airplay-fill:before{content:"\ea18"}.ri-airplay-line:before{content:"\ea19"}.ri-alarm-fill:before{content:"\ea1a"}.ri-alarm-line:before{content:"\ea1b"}.ri-alarm-warning-fill:before{content:"\ea1c"}.ri-alarm-warning-line:before{content:"\ea1d"}.ri-album-fill:before{content:"\ea1e"}.ri-album-line:before{content:"\ea1f"}.ri-alert-fill:before{content:"\ea20"}.ri-alert-line:before{content:"\ea21"}.ri-aliens-fill:before{content:"\ea22"}.ri-aliens-line:before{content:"\ea23"}.ri-align-bottom:before{content:"\ea24"}.ri-align-center:before{content:"\ea25"}.ri-align-justify:before{content:"\ea26"}.ri-align-left:before{content:"\ea27"}.ri-align-right:before{content:"\ea28"}.ri-align-top:before{content:"\ea29"}.ri-align-vertically:before{content:"\ea2a"}.ri-alipay-fill:before{content:"\ea2b"}.ri-alipay-line:before{content:"\ea2c"}.ri-amazon-fill:before{content:"\ea2d"}.ri-amazon-line:before{content:"\ea2e"}.ri-anchor-fill:before{content:"\ea2f"}.ri-anchor-line:before{content:"\ea30"}.ri-ancient-gate-fill:before{content:"\ea31"}.ri-ancient-gate-line:before{content:"\ea32"}.ri-ancient-pavilion-fill:before{content:"\ea33"}.ri-ancient-pavilion-line:before{content:"\ea34"}.ri-android-fill:before{content:"\ea35"}.ri-android-line:before{content:"\ea36"}.ri-angularjs-fill:before{content:"\ea37"}.ri-angularjs-line:before{content:"\ea38"}.ri-anticlockwise-2-fill:before{content:"\ea39"}.ri-anticlockwise-2-line:before{content:"\ea3a"}.ri-anticlockwise-fill:before{content:"\ea3b"}.ri-anticlockwise-line:before{content:"\ea3c"}.ri-app-store-fill:before{content:"\ea3d"}.ri-app-store-line:before{content:"\ea3e"}.ri-apple-fill:before{content:"\ea3f"}.ri-apple-line:before{content:"\ea40"}.ri-apps-2-fill:before{content:"\ea41"}.ri-apps-2-line:before{content:"\ea42"}.ri-apps-fill:before{content:"\ea43"}.ri-apps-line:before{content:"\ea44"}.ri-archive-drawer-fill:before{content:"\ea45"}.ri-archive-drawer-line:before{content:"\ea46"}.ri-archive-fill:before{content:"\ea47"}.ri-archive-line:before{content:"\ea48"}.ri-arrow-down-circle-fill:before{content:"\ea49"}.ri-arrow-down-circle-line:before{content:"\ea4a"}.ri-arrow-down-fill:before{content:"\ea4b"}.ri-arrow-down-line:before{content:"\ea4c"}.ri-arrow-down-s-fill:before{content:"\ea4d"}.ri-arrow-down-s-line:before{content:"\ea4e"}.ri-arrow-drop-down-fill:before{content:"\ea4f"}.ri-arrow-drop-down-line:before{content:"\ea50"}.ri-arrow-drop-left-fill:before{content:"\ea51"}.ri-arrow-drop-left-line:before{content:"\ea52"}.ri-arrow-drop-right-fill:before{content:"\ea53"}.ri-arrow-drop-right-line:before{content:"\ea54"}.ri-arrow-drop-up-fill:before{content:"\ea55"}.ri-arrow-drop-up-line:before{content:"\ea56"}.ri-arrow-go-back-fill:before{content:"\ea57"}.ri-arrow-go-back-line:before{content:"\ea58"}.ri-arrow-go-forward-fill:before{content:"\ea59"}.ri-arrow-go-forward-line:before{content:"\ea5a"}.ri-arrow-left-circle-fill:before{content:"\ea5b"}.ri-arrow-left-circle-line:before{content:"\ea5c"}.ri-arrow-left-down-fill:before{content:"\ea5d"}.ri-arrow-left-down-line:before{content:"\ea5e"}.ri-arrow-left-fill:before{content:"\ea5f"}.ri-arrow-left-line:before{content:"\ea60"}.ri-arrow-left-right-fill:before{content:"\ea61"}.ri-arrow-left-right-line:before{content:"\ea62"}.ri-arrow-left-s-fill:before{content:"\ea63"}.ri-arrow-left-s-line:before{content:"\ea64"}.ri-arrow-left-up-fill:before{content:"\ea65"}.ri-arrow-left-up-line:before{content:"\ea66"}.ri-arrow-right-circle-fill:before{content:"\ea67"}.ri-arrow-right-circle-line:before{content:"\ea68"}.ri-arrow-right-down-fill:before{content:"\ea69"}.ri-arrow-right-down-line:before{content:"\ea6a"}.ri-arrow-right-fill:before{content:"\ea6b"}.ri-arrow-right-line:before{content:"\ea6c"}.ri-arrow-right-s-fill:before{content:"\ea6d"}.ri-arrow-right-s-line:before{content:"\ea6e"}.ri-arrow-right-up-fill:before{content:"\ea6f"}.ri-arrow-right-up-line:before{content:"\ea70"}.ri-arrow-up-circle-fill:before{content:"\ea71"}.ri-arrow-up-circle-line:before{content:"\ea72"}.ri-arrow-up-down-fill:before{content:"\ea73"}.ri-arrow-up-down-line:before{content:"\ea74"}.ri-arrow-up-fill:before{content:"\ea75"}.ri-arrow-up-line:before{content:"\ea76"}.ri-arrow-up-s-fill:before{content:"\ea77"}.ri-arrow-up-s-line:before{content:"\ea78"}.ri-artboard-2-fill:before{content:"\ea79"}.ri-artboard-2-line:before{content:"\ea7a"}.ri-artboard-fill:before{content:"\ea7b"}.ri-artboard-line:before{content:"\ea7c"}.ri-article-fill:before{content:"\ea7d"}.ri-article-line:before{content:"\ea7e"}.ri-aspect-ratio-fill:before{content:"\ea7f"}.ri-aspect-ratio-line:before{content:"\ea80"}.ri-asterisk:before{content:"\ea81"}.ri-at-fill:before{content:"\ea82"}.ri-at-line:before{content:"\ea83"}.ri-attachment-2:before{content:"\ea84"}.ri-attachment-fill:before{content:"\ea85"}.ri-attachment-line:before{content:"\ea86"}.ri-auction-fill:before{content:"\ea87"}.ri-auction-line:before{content:"\ea88"}.ri-award-fill:before{content:"\ea89"}.ri-award-line:before{content:"\ea8a"}.ri-baidu-fill:before{content:"\ea8b"}.ri-baidu-line:before{content:"\ea8c"}.ri-ball-pen-fill:before{content:"\ea8d"}.ri-ball-pen-line:before{content:"\ea8e"}.ri-bank-card-2-fill:before{content:"\ea8f"}.ri-bank-card-2-line:before{content:"\ea90"}.ri-bank-card-fill:before{content:"\ea91"}.ri-bank-card-line:before{content:"\ea92"}.ri-bank-fill:before{content:"\ea93"}.ri-bank-line:before{content:"\ea94"}.ri-bar-chart-2-fill:before{content:"\ea95"}.ri-bar-chart-2-line:before{content:"\ea96"}.ri-bar-chart-box-fill:before{content:"\ea97"}.ri-bar-chart-box-line:before{content:"\ea98"}.ri-bar-chart-fill:before{content:"\ea99"}.ri-bar-chart-grouped-fill:before{content:"\ea9a"}.ri-bar-chart-grouped-line:before{content:"\ea9b"}.ri-bar-chart-horizontal-fill:before{content:"\ea9c"}.ri-bar-chart-horizontal-line:before{content:"\ea9d"}.ri-bar-chart-line:before{content:"\ea9e"}.ri-barcode-box-fill:before{content:"\ea9f"}.ri-barcode-box-line:before{content:"\eaa0"}.ri-barcode-fill:before{content:"\eaa1"}.ri-barcode-line:before{content:"\eaa2"}.ri-barricade-fill:before{content:"\eaa3"}.ri-barricade-line:before{content:"\eaa4"}.ri-base-station-fill:before{content:"\eaa5"}.ri-base-station-line:before{content:"\eaa6"}.ri-basketball-fill:before{content:"\eaa7"}.ri-basketball-line:before{content:"\eaa8"}.ri-battery-2-charge-fill:before{content:"\eaa9"}.ri-battery-2-charge-line:before{content:"\eaaa"}.ri-battery-2-fill:before{content:"\eaab"}.ri-battery-2-line:before{content:"\eaac"}.ri-battery-charge-fill:before{content:"\eaad"}.ri-battery-charge-line:before{content:"\eaae"}.ri-battery-fill:before{content:"\eaaf"}.ri-battery-line:before{content:"\eab0"}.ri-battery-low-fill:before{content:"\eab1"}.ri-battery-low-line:before{content:"\eab2"}.ri-battery-saver-fill:before{content:"\eab3"}.ri-battery-saver-line:before{content:"\eab4"}.ri-battery-share-fill:before{content:"\eab5"}.ri-battery-share-line:before{content:"\eab6"}.ri-bear-smile-fill:before{content:"\eab7"}.ri-bear-smile-line:before{content:"\eab8"}.ri-behance-fill:before{content:"\eab9"}.ri-behance-line:before{content:"\eaba"}.ri-bell-fill:before{content:"\eabb"}.ri-bell-line:before{content:"\eabc"}.ri-bike-fill:before{content:"\eabd"}.ri-bike-line:before{content:"\eabe"}.ri-bilibili-fill:before{content:"\eabf"}.ri-bilibili-line:before{content:"\eac0"}.ri-bill-fill:before{content:"\eac1"}.ri-bill-line:before{content:"\eac2"}.ri-billiards-fill:before{content:"\eac3"}.ri-billiards-line:before{content:"\eac4"}.ri-bit-coin-fill:before{content:"\eac5"}.ri-bit-coin-line:before{content:"\eac6"}.ri-blaze-fill:before{content:"\eac7"}.ri-blaze-line:before{content:"\eac8"}.ri-bluetooth-connect-fill:before{content:"\eac9"}.ri-bluetooth-connect-line:before{content:"\eaca"}.ri-bluetooth-fill:before{content:"\eacb"}.ri-bluetooth-line:before{content:"\eacc"}.ri-blur-off-fill:before{content:"\eacd"}.ri-blur-off-line:before{content:"\eace"}.ri-body-scan-fill:before{content:"\eacf"}.ri-body-scan-line:before{content:"\ead0"}.ri-bold:before{content:"\ead1"}.ri-book-2-fill:before{content:"\ead2"}.ri-book-2-line:before{content:"\ead3"}.ri-book-3-fill:before{content:"\ead4"}.ri-book-3-line:before{content:"\ead5"}.ri-book-fill:before{content:"\ead6"}.ri-book-line:before{content:"\ead7"}.ri-book-mark-fill:before{content:"\ead8"}.ri-book-mark-line:before{content:"\ead9"}.ri-book-open-fill:before{content:"\eada"}.ri-book-open-line:before{content:"\eadb"}.ri-book-read-fill:before{content:"\eadc"}.ri-book-read-line:before{content:"\eadd"}.ri-booklet-fill:before{content:"\eade"}.ri-booklet-line:before{content:"\eadf"}.ri-bookmark-2-fill:before{content:"\eae0"}.ri-bookmark-2-line:before{content:"\eae1"}.ri-bookmark-3-fill:before{content:"\eae2"}.ri-bookmark-3-line:before{content:"\eae3"}.ri-bookmark-fill:before{content:"\eae4"}.ri-bookmark-line:before{content:"\eae5"}.ri-boxing-fill:before{content:"\eae6"}.ri-boxing-line:before{content:"\eae7"}.ri-braces-fill:before{content:"\eae8"}.ri-braces-line:before{content:"\eae9"}.ri-brackets-fill:before{content:"\eaea"}.ri-brackets-line:before{content:"\eaeb"}.ri-briefcase-2-fill:before{content:"\eaec"}.ri-briefcase-2-line:before{content:"\eaed"}.ri-briefcase-3-fill:before{content:"\eaee"}.ri-briefcase-3-line:before{content:"\eaef"}.ri-briefcase-4-fill:before{content:"\eaf0"}.ri-briefcase-4-line:before{content:"\eaf1"}.ri-briefcase-5-fill:before{content:"\eaf2"}.ri-briefcase-5-line:before{content:"\eaf3"}.ri-briefcase-fill:before{content:"\eaf4"}.ri-briefcase-line:before{content:"\eaf5"}.ri-bring-forward:before{content:"\eaf6"}.ri-bring-to-front:before{content:"\eaf7"}.ri-broadcast-fill:before{content:"\eaf8"}.ri-broadcast-line:before{content:"\eaf9"}.ri-brush-2-fill:before{content:"\eafa"}.ri-brush-2-line:before{content:"\eafb"}.ri-brush-3-fill:before{content:"\eafc"}.ri-brush-3-line:before{content:"\eafd"}.ri-brush-4-fill:before{content:"\eafe"}.ri-brush-4-line:before{content:"\eaff"}.ri-brush-fill:before{content:"\eb00"}.ri-brush-line:before{content:"\eb01"}.ri-bubble-chart-fill:before{content:"\eb02"}.ri-bubble-chart-line:before{content:"\eb03"}.ri-bug-2-fill:before{content:"\eb04"}.ri-bug-2-line:before{content:"\eb05"}.ri-bug-fill:before{content:"\eb06"}.ri-bug-line:before{content:"\eb07"}.ri-building-2-fill:before{content:"\eb08"}.ri-building-2-line:before{content:"\eb09"}.ri-building-3-fill:before{content:"\eb0a"}.ri-building-3-line:before{content:"\eb0b"}.ri-building-4-fill:before{content:"\eb0c"}.ri-building-4-line:before{content:"\eb0d"}.ri-building-fill:before{content:"\eb0e"}.ri-building-line:before{content:"\eb0f"}.ri-bus-2-fill:before{content:"\eb10"}.ri-bus-2-line:before{content:"\eb11"}.ri-bus-fill:before{content:"\eb12"}.ri-bus-line:before{content:"\eb13"}.ri-bus-wifi-fill:before{content:"\eb14"}.ri-bus-wifi-line:before{content:"\eb15"}.ri-cactus-fill:before{content:"\eb16"}.ri-cactus-line:before{content:"\eb17"}.ri-cake-2-fill:before{content:"\eb18"}.ri-cake-2-line:before{content:"\eb19"}.ri-cake-3-fill:before{content:"\eb1a"}.ri-cake-3-line:before{content:"\eb1b"}.ri-cake-fill:before{content:"\eb1c"}.ri-cake-line:before{content:"\eb1d"}.ri-calculator-fill:before{content:"\eb1e"}.ri-calculator-line:before{content:"\eb1f"}.ri-calendar-2-fill:before{content:"\eb20"}.ri-calendar-2-line:before{content:"\eb21"}.ri-calendar-check-fill:before{content:"\eb22"}.ri-calendar-check-line:before{content:"\eb23"}.ri-calendar-event-fill:before{content:"\eb24"}.ri-calendar-event-line:before{content:"\eb25"}.ri-calendar-fill:before{content:"\eb26"}.ri-calendar-line:before{content:"\eb27"}.ri-calendar-todo-fill:before{content:"\eb28"}.ri-calendar-todo-line:before{content:"\eb29"}.ri-camera-2-fill:before{content:"\eb2a"}.ri-camera-2-line:before{content:"\eb2b"}.ri-camera-3-fill:before{content:"\eb2c"}.ri-camera-3-line:before{content:"\eb2d"}.ri-camera-fill:before{content:"\eb2e"}.ri-camera-lens-fill:before{content:"\eb2f"}.ri-camera-lens-line:before{content:"\eb30"}.ri-camera-line:before{content:"\eb31"}.ri-camera-off-fill:before{content:"\eb32"}.ri-camera-off-line:before{content:"\eb33"}.ri-camera-switch-fill:before{content:"\eb34"}.ri-camera-switch-line:before{content:"\eb35"}.ri-capsule-fill:before{content:"\eb36"}.ri-capsule-line:before{content:"\eb37"}.ri-car-fill:before{content:"\eb38"}.ri-car-line:before{content:"\eb39"}.ri-car-washing-fill:before{content:"\eb3a"}.ri-car-washing-line:before{content:"\eb3b"}.ri-caravan-fill:before{content:"\eb3c"}.ri-caravan-line:before{content:"\eb3d"}.ri-cast-fill:before{content:"\eb3e"}.ri-cast-line:before{content:"\eb3f"}.ri-cellphone-fill:before{content:"\eb40"}.ri-cellphone-line:before{content:"\eb41"}.ri-celsius-fill:before{content:"\eb42"}.ri-celsius-line:before{content:"\eb43"}.ri-centos-fill:before{content:"\eb44"}.ri-centos-line:before{content:"\eb45"}.ri-character-recognition-fill:before{content:"\eb46"}.ri-character-recognition-line:before{content:"\eb47"}.ri-charging-pile-2-fill:before{content:"\eb48"}.ri-charging-pile-2-line:before{content:"\eb49"}.ri-charging-pile-fill:before{content:"\eb4a"}.ri-charging-pile-line:before{content:"\eb4b"}.ri-chat-1-fill:before{content:"\eb4c"}.ri-chat-1-line:before{content:"\eb4d"}.ri-chat-2-fill:before{content:"\eb4e"}.ri-chat-2-line:before{content:"\eb4f"}.ri-chat-3-fill:before{content:"\eb50"}.ri-chat-3-line:before{content:"\eb51"}.ri-chat-4-fill:before{content:"\eb52"}.ri-chat-4-line:before{content:"\eb53"}.ri-chat-check-fill:before{content:"\eb54"}.ri-chat-check-line:before{content:"\eb55"}.ri-chat-delete-fill:before{content:"\eb56"}.ri-chat-delete-line:before{content:"\eb57"}.ri-chat-download-fill:before{content:"\eb58"}.ri-chat-download-line:before{content:"\eb59"}.ri-chat-follow-up-fill:before{content:"\eb5a"}.ri-chat-follow-up-line:before{content:"\eb5b"}.ri-chat-forward-fill:before{content:"\eb5c"}.ri-chat-forward-line:before{content:"\eb5d"}.ri-chat-heart-fill:before{content:"\eb5e"}.ri-chat-heart-line:before{content:"\eb5f"}.ri-chat-history-fill:before{content:"\eb60"}.ri-chat-history-line:before{content:"\eb61"}.ri-chat-new-fill:before{content:"\eb62"}.ri-chat-new-line:before{content:"\eb63"}.ri-chat-off-fill:before{content:"\eb64"}.ri-chat-off-line:before{content:"\eb65"}.ri-chat-poll-fill:before{content:"\eb66"}.ri-chat-poll-line:before{content:"\eb67"}.ri-chat-private-fill:before{content:"\eb68"}.ri-chat-private-line:before{content:"\eb69"}.ri-chat-quote-fill:before{content:"\eb6a"}.ri-chat-quote-line:before{content:"\eb6b"}.ri-chat-settings-fill:before{content:"\eb6c"}.ri-chat-settings-line:before{content:"\eb6d"}.ri-chat-smile-2-fill:before{content:"\eb6e"}.ri-chat-smile-2-line:before{content:"\eb6f"}.ri-chat-smile-3-fill:before{content:"\eb70"}.ri-chat-smile-3-line:before{content:"\eb71"}.ri-chat-smile-fill:before{content:"\eb72"}.ri-chat-smile-line:before{content:"\eb73"}.ri-chat-upload-fill:before{content:"\eb74"}.ri-chat-upload-line:before{content:"\eb75"}.ri-chat-voice-fill:before{content:"\eb76"}.ri-chat-voice-line:before{content:"\eb77"}.ri-check-double-fill:before{content:"\eb78"}.ri-check-double-line:before{content:"\eb79"}.ri-check-fill:before{content:"\eb7a"}.ri-check-line:before{content:"\eb7b"}.ri-checkbox-blank-circle-fill:before{content:"\eb7c"}.ri-checkbox-blank-circle-line:before{content:"\eb7d"}.ri-checkbox-blank-fill:before{content:"\eb7e"}.ri-checkbox-blank-line:before{content:"\eb7f"}.ri-checkbox-circle-fill:before{content:"\eb80"}.ri-checkbox-circle-line:before{content:"\eb81"}.ri-checkbox-fill:before{content:"\eb82"}.ri-checkbox-indeterminate-fill:before{content:"\eb83"}.ri-checkbox-indeterminate-line:before{content:"\eb84"}.ri-checkbox-line:before{content:"\eb85"}.ri-checkbox-multiple-blank-fill:before{content:"\eb86"}.ri-checkbox-multiple-blank-line:before{content:"\eb87"}.ri-checkbox-multiple-fill:before{content:"\eb88"}.ri-checkbox-multiple-line:before{content:"\eb89"}.ri-china-railway-fill:before{content:"\eb8a"}.ri-china-railway-line:before{content:"\eb8b"}.ri-chrome-fill:before{content:"\eb8c"}.ri-chrome-line:before{content:"\eb8d"}.ri-clapperboard-fill:before{content:"\eb8e"}.ri-clapperboard-line:before{content:"\eb8f"}.ri-clipboard-fill:before{content:"\eb90"}.ri-clipboard-line:before{content:"\eb91"}.ri-clockwise-2-fill:before{content:"\eb92"}.ri-clockwise-2-line:before{content:"\eb93"}.ri-clockwise-fill:before{content:"\eb94"}.ri-clockwise-line:before{content:"\eb95"}.ri-close-circle-fill:before{content:"\eb96"}.ri-close-circle-line:before{content:"\eb97"}.ri-close-fill:before{content:"\eb98"}.ri-close-line:before{content:"\eb99"}.ri-closed-captioning-fill:before{content:"\eb9a"}.ri-closed-captioning-line:before{content:"\eb9b"}.ri-cloud-fill:before{content:"\eb9c"}.ri-cloud-line:before{content:"\eb9d"}.ri-cloud-off-fill:before{content:"\eb9e"}.ri-cloud-off-line:before{content:"\eb9f"}.ri-cloud-windy-fill:before{content:"\eba0"}.ri-cloud-windy-line:before{content:"\eba1"}.ri-cloudy-2-fill:before{content:"\eba2"}.ri-cloudy-2-line:before{content:"\eba3"}.ri-cloudy-fill:before{content:"\eba4"}.ri-cloudy-line:before{content:"\eba5"}.ri-code-box-fill:before{content:"\eba6"}.ri-code-box-line:before{content:"\eba7"}.ri-code-fill:before{content:"\eba8"}.ri-code-line:before{content:"\eba9"}.ri-code-s-fill:before{content:"\ebaa"}.ri-code-s-line:before{content:"\ebab"}.ri-code-s-slash-fill:before{content:"\ebac"}.ri-code-s-slash-line:before{content:"\ebad"}.ri-code-view:before{content:"\ebae"}.ri-codepen-fill:before{content:"\ebaf"}.ri-codepen-line:before{content:"\ebb0"}.ri-coin-fill:before{content:"\ebb1"}.ri-coin-line:before{content:"\ebb2"}.ri-coins-fill:before{content:"\ebb3"}.ri-coins-line:before{content:"\ebb4"}.ri-collage-fill:before{content:"\ebb5"}.ri-collage-line:before{content:"\ebb6"}.ri-command-fill:before{content:"\ebb7"}.ri-command-line:before{content:"\ebb8"}.ri-community-fill:before{content:"\ebb9"}.ri-community-line:before{content:"\ebba"}.ri-compass-2-fill:before{content:"\ebbb"}.ri-compass-2-line:before{content:"\ebbc"}.ri-compass-3-fill:before{content:"\ebbd"}.ri-compass-3-line:before{content:"\ebbe"}.ri-compass-4-fill:before{content:"\ebbf"}.ri-compass-4-line:before{content:"\ebc0"}.ri-compass-discover-fill:before{content:"\ebc1"}.ri-compass-discover-line:before{content:"\ebc2"}.ri-compass-fill:before{content:"\ebc3"}.ri-compass-line:before{content:"\ebc4"}.ri-compasses-2-fill:before{content:"\ebc5"}.ri-compasses-2-line:before{content:"\ebc6"}.ri-compasses-fill:before{content:"\ebc7"}.ri-compasses-line:before{content:"\ebc8"}.ri-computer-fill:before{content:"\ebc9"}.ri-computer-line:before{content:"\ebca"}.ri-contacts-book-2-fill:before{content:"\ebcb"}.ri-contacts-book-2-line:before{content:"\ebcc"}.ri-contacts-book-fill:before{content:"\ebcd"}.ri-contacts-book-line:before{content:"\ebce"}.ri-contacts-book-upload-fill:before{content:"\ebcf"}.ri-contacts-book-upload-line:before{content:"\ebd0"}.ri-contacts-fill:before{content:"\ebd1"}.ri-contacts-line:before{content:"\ebd2"}.ri-contrast-2-fill:before{content:"\ebd3"}.ri-contrast-2-line:before{content:"\ebd4"}.ri-contrast-drop-2-fill:before{content:"\ebd5"}.ri-contrast-drop-2-line:before{content:"\ebd6"}.ri-contrast-drop-fill:before{content:"\ebd7"}.ri-contrast-drop-line:before{content:"\ebd8"}.ri-contrast-fill:before{content:"\ebd9"}.ri-contrast-line:before{content:"\ebda"}.ri-copper-coin-fill:before{content:"\ebdb"}.ri-copper-coin-line:before{content:"\ebdc"}.ri-copper-diamond-fill:before{content:"\ebdd"}.ri-copper-diamond-line:before{content:"\ebde"}.ri-copyleft-fill:before{content:"\ebdf"}.ri-copyleft-line:before{content:"\ebe0"}.ri-copyright-fill:before{content:"\ebe1"}.ri-copyright-line:before{content:"\ebe2"}.ri-coreos-fill:before{content:"\ebe3"}.ri-coreos-line:before{content:"\ebe4"}.ri-coupon-2-fill:before{content:"\ebe5"}.ri-coupon-2-line:before{content:"\ebe6"}.ri-coupon-3-fill:before{content:"\ebe7"}.ri-coupon-3-line:before{content:"\ebe8"}.ri-coupon-4-fill:before{content:"\ebe9"}.ri-coupon-4-line:before{content:"\ebea"}.ri-coupon-5-fill:before{content:"\ebeb"}.ri-coupon-5-line:before{content:"\ebec"}.ri-coupon-fill:before{content:"\ebed"}.ri-coupon-line:before{content:"\ebee"}.ri-cpu-fill:before{content:"\ebef"}.ri-cpu-line:before{content:"\ebf0"}.ri-creative-commons-by-fill:before{content:"\ebf1"}.ri-creative-commons-by-line:before{content:"\ebf2"}.ri-creative-commons-fill:before{content:"\ebf3"}.ri-creative-commons-line:before{content:"\ebf4"}.ri-creative-commons-nc-fill:before{content:"\ebf5"}.ri-creative-commons-nc-line:before{content:"\ebf6"}.ri-creative-commons-nd-fill:before{content:"\ebf7"}.ri-creative-commons-nd-line:before{content:"\ebf8"}.ri-creative-commons-sa-fill:before{content:"\ebf9"}.ri-creative-commons-sa-line:before{content:"\ebfa"}.ri-creative-commons-zero-fill:before{content:"\ebfb"}.ri-creative-commons-zero-line:before{content:"\ebfc"}.ri-criminal-fill:before{content:"\ebfd"}.ri-criminal-line:before{content:"\ebfe"}.ri-crop-2-fill:before{content:"\ebff"}.ri-crop-2-line:before{content:"\ec00"}.ri-crop-fill:before{content:"\ec01"}.ri-crop-line:before{content:"\ec02"}.ri-css3-fill:before{content:"\ec03"}.ri-css3-line:before{content:"\ec04"}.ri-cup-fill:before{content:"\ec05"}.ri-cup-line:before{content:"\ec06"}.ri-currency-fill:before{content:"\ec07"}.ri-currency-line:before{content:"\ec08"}.ri-cursor-fill:before{content:"\ec09"}.ri-cursor-line:before{content:"\ec0a"}.ri-customer-service-2-fill:before{content:"\ec0b"}.ri-customer-service-2-line:before{content:"\ec0c"}.ri-customer-service-fill:before{content:"\ec0d"}.ri-customer-service-line:before{content:"\ec0e"}.ri-dashboard-2-fill:before{content:"\ec0f"}.ri-dashboard-2-line:before{content:"\ec10"}.ri-dashboard-3-fill:before{content:"\ec11"}.ri-dashboard-3-line:before{content:"\ec12"}.ri-dashboard-fill:before{content:"\ec13"}.ri-dashboard-line:before{content:"\ec14"}.ri-database-2-fill:before{content:"\ec15"}.ri-database-2-line:before{content:"\ec16"}.ri-database-fill:before{content:"\ec17"}.ri-database-line:before{content:"\ec18"}.ri-delete-back-2-fill:before{content:"\ec19"}.ri-delete-back-2-line:before{content:"\ec1a"}.ri-delete-back-fill:before{content:"\ec1b"}.ri-delete-back-line:before{content:"\ec1c"}.ri-delete-bin-2-fill:before{content:"\ec1d"}.ri-delete-bin-2-line:before{content:"\ec1e"}.ri-delete-bin-3-fill:before{content:"\ec1f"}.ri-delete-bin-3-line:before{content:"\ec20"}.ri-delete-bin-4-fill:before{content:"\ec21"}.ri-delete-bin-4-line:before{content:"\ec22"}.ri-delete-bin-5-fill:before{content:"\ec23"}.ri-delete-bin-5-line:before{content:"\ec24"}.ri-delete-bin-6-fill:before{content:"\ec25"}.ri-delete-bin-6-line:before{content:"\ec26"}.ri-delete-bin-7-fill:before{content:"\ec27"}.ri-delete-bin-7-line:before{content:"\ec28"}.ri-delete-bin-fill:before{content:"\ec29"}.ri-delete-bin-line:before{content:"\ec2a"}.ri-delete-column:before{content:"\ec2b"}.ri-delete-row:before{content:"\ec2c"}.ri-device-fill:before{content:"\ec2d"}.ri-device-line:before{content:"\ec2e"}.ri-device-recover-fill:before{content:"\ec2f"}.ri-device-recover-line:before{content:"\ec30"}.ri-dingding-fill:before{content:"\ec31"}.ri-dingding-line:before{content:"\ec32"}.ri-direction-fill:before{content:"\ec33"}.ri-direction-line:before{content:"\ec34"}.ri-disc-fill:before{content:"\ec35"}.ri-disc-line:before{content:"\ec36"}.ri-discord-fill:before{content:"\ec37"}.ri-discord-line:before{content:"\ec38"}.ri-discuss-fill:before{content:"\ec39"}.ri-discuss-line:before{content:"\ec3a"}.ri-dislike-fill:before{content:"\ec3b"}.ri-dislike-line:before{content:"\ec3c"}.ri-disqus-fill:before{content:"\ec3d"}.ri-disqus-line:before{content:"\ec3e"}.ri-divide-fill:before{content:"\ec3f"}.ri-divide-line:before{content:"\ec40"}.ri-donut-chart-fill:before{content:"\ec41"}.ri-donut-chart-line:before{content:"\ec42"}.ri-door-closed-fill:before{content:"\ec43"}.ri-door-closed-line:before{content:"\ec44"}.ri-door-fill:before{content:"\ec45"}.ri-door-line:before{content:"\ec46"}.ri-door-lock-box-fill:before{content:"\ec47"}.ri-door-lock-box-line:before{content:"\ec48"}.ri-door-lock-fill:before{content:"\ec49"}.ri-door-lock-line:before{content:"\ec4a"}.ri-door-open-fill:before{content:"\ec4b"}.ri-door-open-line:before{content:"\ec4c"}.ri-dossier-fill:before{content:"\ec4d"}.ri-dossier-line:before{content:"\ec4e"}.ri-douban-fill:before{content:"\ec4f"}.ri-douban-line:before{content:"\ec50"}.ri-double-quotes-l:before{content:"\ec51"}.ri-double-quotes-r:before{content:"\ec52"}.ri-download-2-fill:before{content:"\ec53"}.ri-download-2-line:before{content:"\ec54"}.ri-download-cloud-2-fill:before{content:"\ec55"}.ri-download-cloud-2-line:before{content:"\ec56"}.ri-download-cloud-fill:before{content:"\ec57"}.ri-download-cloud-line:before{content:"\ec58"}.ri-download-fill:before{content:"\ec59"}.ri-download-line:before{content:"\ec5a"}.ri-draft-fill:before{content:"\ec5b"}.ri-draft-line:before{content:"\ec5c"}.ri-drag-drop-fill:before{content:"\ec5d"}.ri-drag-drop-line:before{content:"\ec5e"}.ri-drag-move-2-fill:before{content:"\ec5f"}.ri-drag-move-2-line:before{content:"\ec60"}.ri-drag-move-fill:before{content:"\ec61"}.ri-drag-move-line:before{content:"\ec62"}.ri-dribbble-fill:before{content:"\ec63"}.ri-dribbble-line:before{content:"\ec64"}.ri-drive-fill:before{content:"\ec65"}.ri-drive-line:before{content:"\ec66"}.ri-drizzle-fill:before{content:"\ec67"}.ri-drizzle-line:before{content:"\ec68"}.ri-drop-fill:before{content:"\ec69"}.ri-drop-line:before{content:"\ec6a"}.ri-dropbox-fill:before{content:"\ec6b"}.ri-dropbox-line:before{content:"\ec6c"}.ri-dual-sim-1-fill:before{content:"\ec6d"}.ri-dual-sim-1-line:before{content:"\ec6e"}.ri-dual-sim-2-fill:before{content:"\ec6f"}.ri-dual-sim-2-line:before{content:"\ec70"}.ri-dv-fill:before{content:"\ec71"}.ri-dv-line:before{content:"\ec72"}.ri-dvd-fill:before{content:"\ec73"}.ri-dvd-line:before{content:"\ec74"}.ri-e-bike-2-fill:before{content:"\ec75"}.ri-e-bike-2-line:before{content:"\ec76"}.ri-e-bike-fill:before{content:"\ec77"}.ri-e-bike-line:before{content:"\ec78"}.ri-earth-fill:before{content:"\ec79"}.ri-earth-line:before{content:"\ec7a"}.ri-earthquake-fill:before{content:"\ec7b"}.ri-earthquake-line:before{content:"\ec7c"}.ri-edge-fill:before{content:"\ec7d"}.ri-edge-line:before{content:"\ec7e"}.ri-edit-2-fill:before{content:"\ec7f"}.ri-edit-2-line:before{content:"\ec80"}.ri-edit-box-fill:before{content:"\ec81"}.ri-edit-box-line:before{content:"\ec82"}.ri-edit-circle-fill:before{content:"\ec83"}.ri-edit-circle-line:before{content:"\ec84"}.ri-edit-fill:before{content:"\ec85"}.ri-edit-line:before{content:"\ec86"}.ri-eject-fill:before{content:"\ec87"}.ri-eject-line:before{content:"\ec88"}.ri-emotion-2-fill:before{content:"\ec89"}.ri-emotion-2-line:before{content:"\ec8a"}.ri-emotion-fill:before{content:"\ec8b"}.ri-emotion-happy-fill:before{content:"\ec8c"}.ri-emotion-happy-line:before{content:"\ec8d"}.ri-emotion-laugh-fill:before{content:"\ec8e"}.ri-emotion-laugh-line:before{content:"\ec8f"}.ri-emotion-line:before{content:"\ec90"}.ri-emotion-normal-fill:before{content:"\ec91"}.ri-emotion-normal-line:before{content:"\ec92"}.ri-emotion-sad-fill:before{content:"\ec93"}.ri-emotion-sad-line:before{content:"\ec94"}.ri-emotion-unhappy-fill:before{content:"\ec95"}.ri-emotion-unhappy-line:before{content:"\ec96"}.ri-empathize-fill:before{content:"\ec97"}.ri-empathize-line:before{content:"\ec98"}.ri-emphasis-cn:before{content:"\ec99"}.ri-emphasis:before{content:"\ec9a"}.ri-english-input:before{content:"\ec9b"}.ri-equalizer-fill:before{content:"\ec9c"}.ri-equalizer-line:before{content:"\ec9d"}.ri-eraser-fill:before{content:"\ec9e"}.ri-eraser-line:before{content:"\ec9f"}.ri-error-warning-fill:before{content:"\eca0"}.ri-error-warning-line:before{content:"\eca1"}.ri-evernote-fill:before{content:"\eca2"}.ri-evernote-line:before{content:"\eca3"}.ri-exchange-box-fill:before{content:"\eca4"}.ri-exchange-box-line:before{content:"\eca5"}.ri-exchange-cny-fill:before{content:"\eca6"}.ri-exchange-cny-line:before{content:"\eca7"}.ri-exchange-dollar-fill:before{content:"\eca8"}.ri-exchange-dollar-line:before{content:"\eca9"}.ri-exchange-fill:before{content:"\ecaa"}.ri-exchange-funds-fill:before{content:"\ecab"}.ri-exchange-funds-line:before{content:"\ecac"}.ri-exchange-line:before{content:"\ecad"}.ri-external-link-fill:before{content:"\ecae"}.ri-external-link-line:before{content:"\ecaf"}.ri-eye-2-fill:before{content:"\ecb0"}.ri-eye-2-line:before{content:"\ecb1"}.ri-eye-close-fill:before{content:"\ecb2"}.ri-eye-close-line:before{content:"\ecb3"}.ri-eye-fill:before{content:"\ecb4"}.ri-eye-line:before{content:"\ecb5"}.ri-eye-off-fill:before{content:"\ecb6"}.ri-eye-off-line:before{content:"\ecb7"}.ri-facebook-box-fill:before{content:"\ecb8"}.ri-facebook-box-line:before{content:"\ecb9"}.ri-facebook-circle-fill:before{content:"\ecba"}.ri-facebook-circle-line:before{content:"\ecbb"}.ri-facebook-fill:before{content:"\ecbc"}.ri-facebook-line:before{content:"\ecbd"}.ri-fahrenheit-fill:before{content:"\ecbe"}.ri-fahrenheit-line:before{content:"\ecbf"}.ri-feedback-fill:before{content:"\ecc0"}.ri-feedback-line:before{content:"\ecc1"}.ri-file-2-fill:before{content:"\ecc2"}.ri-file-2-line:before{content:"\ecc3"}.ri-file-3-fill:before{content:"\ecc4"}.ri-file-3-line:before{content:"\ecc5"}.ri-file-4-fill:before{content:"\ecc6"}.ri-file-4-line:before{content:"\ecc7"}.ri-file-add-fill:before{content:"\ecc8"}.ri-file-add-line:before{content:"\ecc9"}.ri-file-chart-2-fill:before{content:"\ecca"}.ri-file-chart-2-line:before{content:"\eccb"}.ri-file-chart-fill:before{content:"\eccc"}.ri-file-chart-line:before{content:"\eccd"}.ri-file-cloud-fill:before{content:"\ecce"}.ri-file-cloud-line:before{content:"\eccf"}.ri-file-code-fill:before{content:"\ecd0"}.ri-file-code-line:before{content:"\ecd1"}.ri-file-copy-2-fill:before{content:"\ecd2"}.ri-file-copy-2-line:before{content:"\ecd3"}.ri-file-copy-fill:before{content:"\ecd4"}.ri-file-copy-line:before{content:"\ecd5"}.ri-file-damage-fill:before{content:"\ecd6"}.ri-file-damage-line:before{content:"\ecd7"}.ri-file-download-fill:before{content:"\ecd8"}.ri-file-download-line:before{content:"\ecd9"}.ri-file-edit-fill:before{content:"\ecda"}.ri-file-edit-line:before{content:"\ecdb"}.ri-file-excel-2-fill:before{content:"\ecdc"}.ri-file-excel-2-line:before{content:"\ecdd"}.ri-file-excel-fill:before{content:"\ecde"}.ri-file-excel-line:before{content:"\ecdf"}.ri-file-fill:before{content:"\ece0"}.ri-file-forbid-fill:before{content:"\ece1"}.ri-file-forbid-line:before{content:"\ece2"}.ri-file-gif-fill:before{content:"\ece3"}.ri-file-gif-line:before{content:"\ece4"}.ri-file-history-fill:before{content:"\ece5"}.ri-file-history-line:before{content:"\ece6"}.ri-file-hwp-fill:before{content:"\ece7"}.ri-file-hwp-line:before{content:"\ece8"}.ri-file-info-fill:before{content:"\ece9"}.ri-file-info-line:before{content:"\ecea"}.ri-file-line:before{content:"\eceb"}.ri-file-list-2-fill:before{content:"\ecec"}.ri-file-list-2-line:before{content:"\eced"}.ri-file-list-3-fill:before{content:"\ecee"}.ri-file-list-3-line:before{content:"\ecef"}.ri-file-list-fill:before{content:"\ecf0"}.ri-file-list-line:before{content:"\ecf1"}.ri-file-lock-fill:before{content:"\ecf2"}.ri-file-lock-line:before{content:"\ecf3"}.ri-file-mark-fill:before{content:"\ecf4"}.ri-file-mark-line:before{content:"\ecf5"}.ri-file-music-fill:before{content:"\ecf6"}.ri-file-music-line:before{content:"\ecf7"}.ri-file-paper-2-fill:before{content:"\ecf8"}.ri-file-paper-2-line:before{content:"\ecf9"}.ri-file-paper-fill:before{content:"\ecfa"}.ri-file-paper-line:before{content:"\ecfb"}.ri-file-pdf-fill:before{content:"\ecfc"}.ri-file-pdf-line:before{content:"\ecfd"}.ri-file-ppt-2-fill:before{content:"\ecfe"}.ri-file-ppt-2-line:before{content:"\ecff"}.ri-file-ppt-fill:before{content:"\ed00"}.ri-file-ppt-line:before{content:"\ed01"}.ri-file-reduce-fill:before{content:"\ed02"}.ri-file-reduce-line:before{content:"\ed03"}.ri-file-search-fill:before{content:"\ed04"}.ri-file-search-line:before{content:"\ed05"}.ri-file-settings-fill:before{content:"\ed06"}.ri-file-settings-line:before{content:"\ed07"}.ri-file-shield-2-fill:before{content:"\ed08"}.ri-file-shield-2-line:before{content:"\ed09"}.ri-file-shield-fill:before{content:"\ed0a"}.ri-file-shield-line:before{content:"\ed0b"}.ri-file-shred-fill:before{content:"\ed0c"}.ri-file-shred-line:before{content:"\ed0d"}.ri-file-text-fill:before{content:"\ed0e"}.ri-file-text-line:before{content:"\ed0f"}.ri-file-transfer-fill:before{content:"\ed10"}.ri-file-transfer-line:before{content:"\ed11"}.ri-file-unknow-fill:before{content:"\ed12"}.ri-file-unknow-line:before{content:"\ed13"}.ri-file-upload-fill:before{content:"\ed14"}.ri-file-upload-line:before{content:"\ed15"}.ri-file-user-fill:before{content:"\ed16"}.ri-file-user-line:before{content:"\ed17"}.ri-file-warning-fill:before{content:"\ed18"}.ri-file-warning-line:before{content:"\ed19"}.ri-file-word-2-fill:before{content:"\ed1a"}.ri-file-word-2-line:before{content:"\ed1b"}.ri-file-word-fill:before{content:"\ed1c"}.ri-file-word-line:before{content:"\ed1d"}.ri-file-zip-fill:before{content:"\ed1e"}.ri-file-zip-line:before{content:"\ed1f"}.ri-film-fill:before{content:"\ed20"}.ri-film-line:before{content:"\ed21"}.ri-filter-2-fill:before{content:"\ed22"}.ri-filter-2-line:before{content:"\ed23"}.ri-filter-3-fill:before{content:"\ed24"}.ri-filter-3-line:before{content:"\ed25"}.ri-filter-fill:before{content:"\ed26"}.ri-filter-line:before{content:"\ed27"}.ri-filter-off-fill:before{content:"\ed28"}.ri-filter-off-line:before{content:"\ed29"}.ri-find-replace-fill:before{content:"\ed2a"}.ri-find-replace-line:before{content:"\ed2b"}.ri-finder-fill:before{content:"\ed2c"}.ri-finder-line:before{content:"\ed2d"}.ri-fingerprint-2-fill:before{content:"\ed2e"}.ri-fingerprint-2-line:before{content:"\ed2f"}.ri-fingerprint-fill:before{content:"\ed30"}.ri-fingerprint-line:before{content:"\ed31"}.ri-fire-fill:before{content:"\ed32"}.ri-fire-line:before{content:"\ed33"}.ri-firefox-fill:before{content:"\ed34"}.ri-firefox-line:before{content:"\ed35"}.ri-first-aid-kit-fill:before{content:"\ed36"}.ri-first-aid-kit-line:before{content:"\ed37"}.ri-flag-2-fill:before{content:"\ed38"}.ri-flag-2-line:before{content:"\ed39"}.ri-flag-fill:before{content:"\ed3a"}.ri-flag-line:before{content:"\ed3b"}.ri-flashlight-fill:before{content:"\ed3c"}.ri-flashlight-line:before{content:"\ed3d"}.ri-flask-fill:before{content:"\ed3e"}.ri-flask-line:before{content:"\ed3f"}.ri-flight-land-fill:before{content:"\ed40"}.ri-flight-land-line:before{content:"\ed41"}.ri-flight-takeoff-fill:before{content:"\ed42"}.ri-flight-takeoff-line:before{content:"\ed43"}.ri-flood-fill:before{content:"\ed44"}.ri-flood-line:before{content:"\ed45"}.ri-flow-chart:before{content:"\ed46"}.ri-flutter-fill:before{content:"\ed47"}.ri-flutter-line:before{content:"\ed48"}.ri-focus-2-fill:before{content:"\ed49"}.ri-focus-2-line:before{content:"\ed4a"}.ri-focus-3-fill:before{content:"\ed4b"}.ri-focus-3-line:before{content:"\ed4c"}.ri-focus-fill:before{content:"\ed4d"}.ri-focus-line:before{content:"\ed4e"}.ri-foggy-fill:before{content:"\ed4f"}.ri-foggy-line:before{content:"\ed50"}.ri-folder-2-fill:before{content:"\ed51"}.ri-folder-2-line:before{content:"\ed52"}.ri-folder-3-fill:before{content:"\ed53"}.ri-folder-3-line:before{content:"\ed54"}.ri-folder-4-fill:before{content:"\ed55"}.ri-folder-4-line:before{content:"\ed56"}.ri-folder-5-fill:before{content:"\ed57"}.ri-folder-5-line:before{content:"\ed58"}.ri-folder-add-fill:before{content:"\ed59"}.ri-folder-add-line:before{content:"\ed5a"}.ri-folder-chart-2-fill:before{content:"\ed5b"}.ri-folder-chart-2-line:before{content:"\ed5c"}.ri-folder-chart-fill:before{content:"\ed5d"}.ri-folder-chart-line:before{content:"\ed5e"}.ri-folder-download-fill:before{content:"\ed5f"}.ri-folder-download-line:before{content:"\ed60"}.ri-folder-fill:before{content:"\ed61"}.ri-folder-forbid-fill:before{content:"\ed62"}.ri-folder-forbid-line:before{content:"\ed63"}.ri-folder-history-fill:before{content:"\ed64"}.ri-folder-history-line:before{content:"\ed65"}.ri-folder-info-fill:before{content:"\ed66"}.ri-folder-info-line:before{content:"\ed67"}.ri-folder-keyhole-fill:before{content:"\ed68"}.ri-folder-keyhole-line:before{content:"\ed69"}.ri-folder-line:before{content:"\ed6a"}.ri-folder-lock-fill:before{content:"\ed6b"}.ri-folder-lock-line:before{content:"\ed6c"}.ri-folder-music-fill:before{content:"\ed6d"}.ri-folder-music-line:before{content:"\ed6e"}.ri-folder-open-fill:before{content:"\ed6f"}.ri-folder-open-line:before{content:"\ed70"}.ri-folder-received-fill:before{content:"\ed71"}.ri-folder-received-line:before{content:"\ed72"}.ri-folder-reduce-fill:before{content:"\ed73"}.ri-folder-reduce-line:before{content:"\ed74"}.ri-folder-settings-fill:before{content:"\ed75"}.ri-folder-settings-line:before{content:"\ed76"}.ri-folder-shared-fill:before{content:"\ed77"}.ri-folder-shared-line:before{content:"\ed78"}.ri-folder-shield-2-fill:before{content:"\ed79"}.ri-folder-shield-2-line:before{content:"\ed7a"}.ri-folder-shield-fill:before{content:"\ed7b"}.ri-folder-shield-line:before{content:"\ed7c"}.ri-folder-transfer-fill:before{content:"\ed7d"}.ri-folder-transfer-line:before{content:"\ed7e"}.ri-folder-unknow-fill:before{content:"\ed7f"}.ri-folder-unknow-line:before{content:"\ed80"}.ri-folder-upload-fill:before{content:"\ed81"}.ri-folder-upload-line:before{content:"\ed82"}.ri-folder-user-fill:before{content:"\ed83"}.ri-folder-user-line:before{content:"\ed84"}.ri-folder-warning-fill:before{content:"\ed85"}.ri-folder-warning-line:before{content:"\ed86"}.ri-folder-zip-fill:before{content:"\ed87"}.ri-folder-zip-line:before{content:"\ed88"}.ri-folders-fill:before{content:"\ed89"}.ri-folders-line:before{content:"\ed8a"}.ri-font-color:before{content:"\ed8b"}.ri-font-size-2:before{content:"\ed8c"}.ri-font-size:before{content:"\ed8d"}.ri-football-fill:before{content:"\ed8e"}.ri-football-line:before{content:"\ed8f"}.ri-footprint-fill:before{content:"\ed90"}.ri-footprint-line:before{content:"\ed91"}.ri-forbid-2-fill:before{content:"\ed92"}.ri-forbid-2-line:before{content:"\ed93"}.ri-forbid-fill:before{content:"\ed94"}.ri-forbid-line:before{content:"\ed95"}.ri-format-clear:before{content:"\ed96"}.ri-fridge-fill:before{content:"\ed97"}.ri-fridge-line:before{content:"\ed98"}.ri-fullscreen-exit-fill:before{content:"\ed99"}.ri-fullscreen-exit-line:before{content:"\ed9a"}.ri-fullscreen-fill:before{content:"\ed9b"}.ri-fullscreen-line:before{content:"\ed9c"}.ri-function-fill:before{content:"\ed9d"}.ri-function-line:before{content:"\ed9e"}.ri-functions:before{content:"\ed9f"}.ri-funds-box-fill:before{content:"\eda0"}.ri-funds-box-line:before{content:"\eda1"}.ri-funds-fill:before{content:"\eda2"}.ri-funds-line:before{content:"\eda3"}.ri-gallery-fill:before{content:"\eda4"}.ri-gallery-line:before{content:"\eda5"}.ri-gallery-upload-fill:before{content:"\eda6"}.ri-gallery-upload-line:before{content:"\eda7"}.ri-game-fill:before{content:"\eda8"}.ri-game-line:before{content:"\eda9"}.ri-gamepad-fill:before{content:"\edaa"}.ri-gamepad-line:before{content:"\edab"}.ri-gas-station-fill:before{content:"\edac"}.ri-gas-station-line:before{content:"\edad"}.ri-gatsby-fill:before{content:"\edae"}.ri-gatsby-line:before{content:"\edaf"}.ri-genderless-fill:before{content:"\edb0"}.ri-genderless-line:before{content:"\edb1"}.ri-ghost-2-fill:before{content:"\edb2"}.ri-ghost-2-line:before{content:"\edb3"}.ri-ghost-fill:before{content:"\edb4"}.ri-ghost-line:before{content:"\edb5"}.ri-ghost-smile-fill:before{content:"\edb6"}.ri-ghost-smile-line:before{content:"\edb7"}.ri-gift-2-fill:before{content:"\edb8"}.ri-gift-2-line:before{content:"\edb9"}.ri-gift-fill:before{content:"\edba"}.ri-gift-line:before{content:"\edbb"}.ri-git-branch-fill:before{content:"\edbc"}.ri-git-branch-line:before{content:"\edbd"}.ri-git-commit-fill:before{content:"\edbe"}.ri-git-commit-line:before{content:"\edbf"}.ri-git-merge-fill:before{content:"\edc0"}.ri-git-merge-line:before{content:"\edc1"}.ri-git-pull-request-fill:before{content:"\edc2"}.ri-git-pull-request-line:before{content:"\edc3"}.ri-git-repository-commits-fill:before{content:"\edc4"}.ri-git-repository-commits-line:before{content:"\edc5"}.ri-git-repository-fill:before{content:"\edc6"}.ri-git-repository-line:before{content:"\edc7"}.ri-git-repository-private-fill:before{content:"\edc8"}.ri-git-repository-private-line:before{content:"\edc9"}.ri-github-fill:before{content:"\edca"}.ri-github-line:before{content:"\edcb"}.ri-gitlab-fill:before{content:"\edcc"}.ri-gitlab-line:before{content:"\edcd"}.ri-global-fill:before{content:"\edce"}.ri-global-line:before{content:"\edcf"}.ri-globe-fill:before{content:"\edd0"}.ri-globe-line:before{content:"\edd1"}.ri-goblet-fill:before{content:"\edd2"}.ri-goblet-line:before{content:"\edd3"}.ri-google-fill:before{content:"\edd4"}.ri-google-line:before{content:"\edd5"}.ri-google-play-fill:before{content:"\edd6"}.ri-google-play-line:before{content:"\edd7"}.ri-government-fill:before{content:"\edd8"}.ri-government-line:before{content:"\edd9"}.ri-gps-fill:before{content:"\edda"}.ri-gps-line:before{content:"\eddb"}.ri-gradienter-fill:before{content:"\eddc"}.ri-gradienter-line:before{content:"\eddd"}.ri-grid-fill:before{content:"\edde"}.ri-grid-line:before{content:"\eddf"}.ri-group-2-fill:before{content:"\ede0"}.ri-group-2-line:before{content:"\ede1"}.ri-group-fill:before{content:"\ede2"}.ri-group-line:before{content:"\ede3"}.ri-guide-fill:before{content:"\ede4"}.ri-guide-line:before{content:"\ede5"}.ri-h-1:before{content:"\ede6"}.ri-h-2:before{content:"\ede7"}.ri-h-3:before{content:"\ede8"}.ri-h-4:before{content:"\ede9"}.ri-h-5:before{content:"\edea"}.ri-h-6:before{content:"\edeb"}.ri-hail-fill:before{content:"\edec"}.ri-hail-line:before{content:"\eded"}.ri-hammer-fill:before{content:"\edee"}.ri-hammer-line:before{content:"\edef"}.ri-hand-coin-fill:before{content:"\edf0"}.ri-hand-coin-line:before{content:"\edf1"}.ri-hand-heart-fill:before{content:"\edf2"}.ri-hand-heart-line:before{content:"\edf3"}.ri-hand-sanitizer-fill:before{content:"\edf4"}.ri-hand-sanitizer-line:before{content:"\edf5"}.ri-handbag-fill:before{content:"\edf6"}.ri-handbag-line:before{content:"\edf7"}.ri-hard-drive-2-fill:before{content:"\edf8"}.ri-hard-drive-2-line:before{content:"\edf9"}.ri-hard-drive-fill:before{content:"\edfa"}.ri-hard-drive-line:before{content:"\edfb"}.ri-hashtag:before{content:"\edfc"}.ri-haze-2-fill:before{content:"\edfd"}.ri-haze-2-line:before{content:"\edfe"}.ri-haze-fill:before{content:"\edff"}.ri-haze-line:before{content:"\ee00"}.ri-hd-fill:before{content:"\ee01"}.ri-hd-line:before{content:"\ee02"}.ri-heading:before{content:"\ee03"}.ri-headphone-fill:before{content:"\ee04"}.ri-headphone-line:before{content:"\ee05"}.ri-health-book-fill:before{content:"\ee06"}.ri-health-book-line:before{content:"\ee07"}.ri-heart-2-fill:before{content:"\ee08"}.ri-heart-2-line:before{content:"\ee09"}.ri-heart-3-fill:before{content:"\ee0a"}.ri-heart-3-line:before{content:"\ee0b"}.ri-heart-add-fill:before{content:"\ee0c"}.ri-heart-add-line:before{content:"\ee0d"}.ri-heart-fill:before{content:"\ee0e"}.ri-heart-line:before{content:"\ee0f"}.ri-heart-pulse-fill:before{content:"\ee10"}.ri-heart-pulse-line:before{content:"\ee11"}.ri-hearts-fill:before{content:"\ee12"}.ri-hearts-line:before{content:"\ee13"}.ri-heavy-showers-fill:before{content:"\ee14"}.ri-heavy-showers-line:before{content:"\ee15"}.ri-history-fill:before{content:"\ee16"}.ri-history-line:before{content:"\ee17"}.ri-home-2-fill:before{content:"\ee18"}.ri-home-2-line:before{content:"\ee19"}.ri-home-3-fill:before{content:"\ee1a"}.ri-home-3-line:before{content:"\ee1b"}.ri-home-4-fill:before{content:"\ee1c"}.ri-home-4-line:before{content:"\ee1d"}.ri-home-5-fill:before{content:"\ee1e"}.ri-home-5-line:before{content:"\ee1f"}.ri-home-6-fill:before{content:"\ee20"}.ri-home-6-line:before{content:"\ee21"}.ri-home-7-fill:before{content:"\ee22"}.ri-home-7-line:before{content:"\ee23"}.ri-home-8-fill:before{content:"\ee24"}.ri-home-8-line:before{content:"\ee25"}.ri-home-fill:before{content:"\ee26"}.ri-home-gear-fill:before{content:"\ee27"}.ri-home-gear-line:before{content:"\ee28"}.ri-home-heart-fill:before{content:"\ee29"}.ri-home-heart-line:before{content:"\ee2a"}.ri-home-line:before{content:"\ee2b"}.ri-home-smile-2-fill:before{content:"\ee2c"}.ri-home-smile-2-line:before{content:"\ee2d"}.ri-home-smile-fill:before{content:"\ee2e"}.ri-home-smile-line:before{content:"\ee2f"}.ri-home-wifi-fill:before{content:"\ee30"}.ri-home-wifi-line:before{content:"\ee31"}.ri-honor-of-kings-fill:before{content:"\ee32"}.ri-honor-of-kings-line:before{content:"\ee33"}.ri-honour-fill:before{content:"\ee34"}.ri-honour-line:before{content:"\ee35"}.ri-hospital-fill:before{content:"\ee36"}.ri-hospital-line:before{content:"\ee37"}.ri-hotel-bed-fill:before{content:"\ee38"}.ri-hotel-bed-line:before{content:"\ee39"}.ri-hotel-fill:before{content:"\ee3a"}.ri-hotel-line:before{content:"\ee3b"}.ri-hotspot-fill:before{content:"\ee3c"}.ri-hotspot-line:before{content:"\ee3d"}.ri-hq-fill:before{content:"\ee3e"}.ri-hq-line:before{content:"\ee3f"}.ri-html5-fill:before{content:"\ee40"}.ri-html5-line:before{content:"\ee41"}.ri-ie-fill:before{content:"\ee42"}.ri-ie-line:before{content:"\ee43"}.ri-image-2-fill:before{content:"\ee44"}.ri-image-2-line:before{content:"\ee45"}.ri-image-add-fill:before{content:"\ee46"}.ri-image-add-line:before{content:"\ee47"}.ri-image-edit-fill:before{content:"\ee48"}.ri-image-edit-line:before{content:"\ee49"}.ri-image-fill:before{content:"\ee4a"}.ri-image-line:before{content:"\ee4b"}.ri-inbox-archive-fill:before{content:"\ee4c"}.ri-inbox-archive-line:before{content:"\ee4d"}.ri-inbox-fill:before{content:"\ee4e"}.ri-inbox-line:before{content:"\ee4f"}.ri-inbox-unarchive-fill:before{content:"\ee50"}.ri-inbox-unarchive-line:before{content:"\ee51"}.ri-increase-decrease-fill:before{content:"\ee52"}.ri-increase-decrease-line:before{content:"\ee53"}.ri-indent-decrease:before{content:"\ee54"}.ri-indent-increase:before{content:"\ee55"}.ri-indeterminate-circle-fill:before{content:"\ee56"}.ri-indeterminate-circle-line:before{content:"\ee57"}.ri-information-fill:before{content:"\ee58"}.ri-information-line:before{content:"\ee59"}.ri-infrared-thermometer-fill:before{content:"\ee5a"}.ri-infrared-thermometer-line:before{content:"\ee5b"}.ri-ink-bottle-fill:before{content:"\ee5c"}.ri-ink-bottle-line:before{content:"\ee5d"}.ri-input-cursor-move:before{content:"\ee5e"}.ri-input-method-fill:before{content:"\ee5f"}.ri-input-method-line:before{content:"\ee60"}.ri-insert-column-left:before{content:"\ee61"}.ri-insert-column-right:before{content:"\ee62"}.ri-insert-row-bottom:before{content:"\ee63"}.ri-insert-row-top:before{content:"\ee64"}.ri-instagram-fill:before{content:"\ee65"}.ri-instagram-line:before{content:"\ee66"}.ri-install-fill:before{content:"\ee67"}.ri-install-line:before{content:"\ee68"}.ri-invision-fill:before{content:"\ee69"}.ri-invision-line:before{content:"\ee6a"}.ri-italic:before{content:"\ee6b"}.ri-kakao-talk-fill:before{content:"\ee6c"}.ri-kakao-talk-line:before{content:"\ee6d"}.ri-key-2-fill:before{content:"\ee6e"}.ri-key-2-line:before{content:"\ee6f"}.ri-key-fill:before{content:"\ee70"}.ri-key-line:before{content:"\ee71"}.ri-keyboard-box-fill:before{content:"\ee72"}.ri-keyboard-box-line:before{content:"\ee73"}.ri-keyboard-fill:before{content:"\ee74"}.ri-keyboard-line:before{content:"\ee75"}.ri-keynote-fill:before{content:"\ee76"}.ri-keynote-line:before{content:"\ee77"}.ri-knife-blood-fill:before{content:"\ee78"}.ri-knife-blood-line:before{content:"\ee79"}.ri-knife-fill:before{content:"\ee7a"}.ri-knife-line:before{content:"\ee7b"}.ri-landscape-fill:before{content:"\ee7c"}.ri-landscape-line:before{content:"\ee7d"}.ri-layout-2-fill:before{content:"\ee7e"}.ri-layout-2-line:before{content:"\ee7f"}.ri-layout-3-fill:before{content:"\ee80"}.ri-layout-3-line:before{content:"\ee81"}.ri-layout-4-fill:before{content:"\ee82"}.ri-layout-4-line:before{content:"\ee83"}.ri-layout-5-fill:before{content:"\ee84"}.ri-layout-5-line:before{content:"\ee85"}.ri-layout-6-fill:before{content:"\ee86"}.ri-layout-6-line:before{content:"\ee87"}.ri-layout-bottom-2-fill:before{content:"\ee88"}.ri-layout-bottom-2-line:before{content:"\ee89"}.ri-layout-bottom-fill:before{content:"\ee8a"}.ri-layout-bottom-line:before{content:"\ee8b"}.ri-layout-column-fill:before{content:"\ee8c"}.ri-layout-column-line:before{content:"\ee8d"}.ri-layout-fill:before{content:"\ee8e"}.ri-layout-grid-fill:before{content:"\ee8f"}.ri-layout-grid-line:before{content:"\ee90"}.ri-layout-left-2-fill:before{content:"\ee91"}.ri-layout-left-2-line:before{content:"\ee92"}.ri-layout-left-fill:before{content:"\ee93"}.ri-layout-left-line:before{content:"\ee94"}.ri-layout-line:before{content:"\ee95"}.ri-layout-masonry-fill:before{content:"\ee96"}.ri-layout-masonry-line:before{content:"\ee97"}.ri-layout-right-2-fill:before{content:"\ee98"}.ri-layout-right-2-line:before{content:"\ee99"}.ri-layout-right-fill:before{content:"\ee9a"}.ri-layout-right-line:before{content:"\ee9b"}.ri-layout-row-fill:before{content:"\ee9c"}.ri-layout-row-line:before{content:"\ee9d"}.ri-layout-top-2-fill:before{content:"\ee9e"}.ri-layout-top-2-line:before{content:"\ee9f"}.ri-layout-top-fill:before{content:"\eea0"}.ri-layout-top-line:before{content:"\eea1"}.ri-leaf-fill:before{content:"\eea2"}.ri-leaf-line:before{content:"\eea3"}.ri-lifebuoy-fill:before{content:"\eea4"}.ri-lifebuoy-line:before{content:"\eea5"}.ri-lightbulb-fill:before{content:"\eea6"}.ri-lightbulb-flash-fill:before{content:"\eea7"}.ri-lightbulb-flash-line:before{content:"\eea8"}.ri-lightbulb-line:before{content:"\eea9"}.ri-line-chart-fill:before{content:"\eeaa"}.ri-line-chart-line:before{content:"\eeab"}.ri-line-fill:before{content:"\eeac"}.ri-line-height:before{content:"\eead"}.ri-line-line:before{content:"\eeae"}.ri-link-m:before{content:"\eeaf"}.ri-link-unlink-m:before{content:"\eeb0"}.ri-link-unlink:before{content:"\eeb1"}.ri-link:before{content:"\eeb2"}.ri-linkedin-box-fill:before{content:"\eeb3"}.ri-linkedin-box-line:before{content:"\eeb4"}.ri-linkedin-fill:before{content:"\eeb5"}.ri-linkedin-line:before{content:"\eeb6"}.ri-links-fill:before{content:"\eeb7"}.ri-links-line:before{content:"\eeb8"}.ri-list-check-2:before{content:"\eeb9"}.ri-list-check:before{content:"\eeba"}.ri-list-ordered:before{content:"\eebb"}.ri-list-settings-fill:before{content:"\eebc"}.ri-list-settings-line:before{content:"\eebd"}.ri-list-unordered:before{content:"\eebe"}.ri-live-fill:before{content:"\eebf"}.ri-live-line:before{content:"\eec0"}.ri-loader-2-fill:before{content:"\eec1"}.ri-loader-2-line:before{content:"\eec2"}.ri-loader-3-fill:before{content:"\eec3"}.ri-loader-3-line:before{content:"\eec4"}.ri-loader-4-fill:before{content:"\eec5"}.ri-loader-4-line:before{content:"\eec6"}.ri-loader-5-fill:before{content:"\eec7"}.ri-loader-5-line:before{content:"\eec8"}.ri-loader-fill:before{content:"\eec9"}.ri-loader-line:before{content:"\eeca"}.ri-lock-2-fill:before{content:"\eecb"}.ri-lock-2-line:before{content:"\eecc"}.ri-lock-fill:before{content:"\eecd"}.ri-lock-line:before{content:"\eece"}.ri-lock-password-fill:before{content:"\eecf"}.ri-lock-password-line:before{content:"\eed0"}.ri-lock-unlock-fill:before{content:"\eed1"}.ri-lock-unlock-line:before{content:"\eed2"}.ri-login-box-fill:before{content:"\eed3"}.ri-login-box-line:before{content:"\eed4"}.ri-login-circle-fill:before{content:"\eed5"}.ri-login-circle-line:before{content:"\eed6"}.ri-logout-box-fill:before{content:"\eed7"}.ri-logout-box-line:before{content:"\eed8"}.ri-logout-box-r-fill:before{content:"\eed9"}.ri-logout-box-r-line:before{content:"\eeda"}.ri-logout-circle-fill:before{content:"\eedb"}.ri-logout-circle-line:before{content:"\eedc"}.ri-logout-circle-r-fill:before{content:"\eedd"}.ri-logout-circle-r-line:before{content:"\eede"}.ri-luggage-cart-fill:before{content:"\eedf"}.ri-luggage-cart-line:before{content:"\eee0"}.ri-luggage-deposit-fill:before{content:"\eee1"}.ri-luggage-deposit-line:before{content:"\eee2"}.ri-lungs-fill:before{content:"\eee3"}.ri-lungs-line:before{content:"\eee4"}.ri-mac-fill:before{content:"\eee5"}.ri-mac-line:before{content:"\eee6"}.ri-macbook-fill:before{content:"\eee7"}.ri-macbook-line:before{content:"\eee8"}.ri-magic-fill:before{content:"\eee9"}.ri-magic-line:before{content:"\eeea"}.ri-mail-add-fill:before{content:"\eeeb"}.ri-mail-add-line:before{content:"\eeec"}.ri-mail-check-fill:before{content:"\eeed"}.ri-mail-check-line:before{content:"\eeee"}.ri-mail-close-fill:before{content:"\eeef"}.ri-mail-close-line:before{content:"\eef0"}.ri-mail-download-fill:before{content:"\eef1"}.ri-mail-download-line:before{content:"\eef2"}.ri-mail-fill:before{content:"\eef3"}.ri-mail-forbid-fill:before{content:"\eef4"}.ri-mail-forbid-line:before{content:"\eef5"}.ri-mail-line:before{content:"\eef6"}.ri-mail-lock-fill:before{content:"\eef7"}.ri-mail-lock-line:before{content:"\eef8"}.ri-mail-open-fill:before{content:"\eef9"}.ri-mail-open-line:before{content:"\eefa"}.ri-mail-send-fill:before{content:"\eefb"}.ri-mail-send-line:before{content:"\eefc"}.ri-mail-settings-fill:before{content:"\eefd"}.ri-mail-settings-line:before{content:"\eefe"}.ri-mail-star-fill:before{content:"\eeff"}.ri-mail-star-line:before{content:"\ef00"}.ri-mail-unread-fill:before{content:"\ef01"}.ri-mail-unread-line:before{content:"\ef02"}.ri-mail-volume-fill:before{content:"\ef03"}.ri-mail-volume-line:before{content:"\ef04"}.ri-map-2-fill:before{content:"\ef05"}.ri-map-2-line:before{content:"\ef06"}.ri-map-fill:before{content:"\ef07"}.ri-map-line:before{content:"\ef08"}.ri-map-pin-2-fill:before{content:"\ef09"}.ri-map-pin-2-line:before{content:"\ef0a"}.ri-map-pin-3-fill:before{content:"\ef0b"}.ri-map-pin-3-line:before{content:"\ef0c"}.ri-map-pin-4-fill:before{content:"\ef0d"}.ri-map-pin-4-line:before{content:"\ef0e"}.ri-map-pin-5-fill:before{content:"\ef0f"}.ri-map-pin-5-line:before{content:"\ef10"}.ri-map-pin-add-fill:before{content:"\ef11"}.ri-map-pin-add-line:before{content:"\ef12"}.ri-map-pin-fill:before{content:"\ef13"}.ri-map-pin-line:before{content:"\ef14"}.ri-map-pin-range-fill:before{content:"\ef15"}.ri-map-pin-range-line:before{content:"\ef16"}.ri-map-pin-time-fill:before{content:"\ef17"}.ri-map-pin-time-line:before{content:"\ef18"}.ri-map-pin-user-fill:before{content:"\ef19"}.ri-map-pin-user-line:before{content:"\ef1a"}.ri-mark-pen-fill:before{content:"\ef1b"}.ri-mark-pen-line:before{content:"\ef1c"}.ri-markdown-fill:before{content:"\ef1d"}.ri-markdown-line:before{content:"\ef1e"}.ri-markup-fill:before{content:"\ef1f"}.ri-markup-line:before{content:"\ef20"}.ri-mastercard-fill:before{content:"\ef21"}.ri-mastercard-line:before{content:"\ef22"}.ri-mastodon-fill:before{content:"\ef23"}.ri-mastodon-line:before{content:"\ef24"}.ri-medal-2-fill:before{content:"\ef25"}.ri-medal-2-line:before{content:"\ef26"}.ri-medal-fill:before{content:"\ef27"}.ri-medal-line:before{content:"\ef28"}.ri-medicine-bottle-fill:before{content:"\ef29"}.ri-medicine-bottle-line:before{content:"\ef2a"}.ri-medium-fill:before{content:"\ef2b"}.ri-medium-line:before{content:"\ef2c"}.ri-men-fill:before{content:"\ef2d"}.ri-men-line:before{content:"\ef2e"}.ri-mental-health-fill:before{content:"\ef2f"}.ri-mental-health-line:before{content:"\ef30"}.ri-menu-2-fill:before{content:"\ef31"}.ri-menu-2-line:before{content:"\ef32"}.ri-menu-3-fill:before{content:"\ef33"}.ri-menu-3-line:before{content:"\ef34"}.ri-menu-4-fill:before{content:"\ef35"}.ri-menu-4-line:before{content:"\ef36"}.ri-menu-5-fill:before{content:"\ef37"}.ri-menu-5-line:before{content:"\ef38"}.ri-menu-add-fill:before{content:"\ef39"}.ri-menu-add-line:before{content:"\ef3a"}.ri-menu-fill:before{content:"\ef3b"}.ri-menu-fold-fill:before{content:"\ef3c"}.ri-menu-fold-line:before{content:"\ef3d"}.ri-menu-line:before{content:"\ef3e"}.ri-menu-unfold-fill:before{content:"\ef3f"}.ri-menu-unfold-line:before{content:"\ef40"}.ri-merge-cells-horizontal:before{content:"\ef41"}.ri-merge-cells-vertical:before{content:"\ef42"}.ri-message-2-fill:before{content:"\ef43"}.ri-message-2-line:before{content:"\ef44"}.ri-message-3-fill:before{content:"\ef45"}.ri-message-3-line:before{content:"\ef46"}.ri-message-fill:before{content:"\ef47"}.ri-message-line:before{content:"\ef48"}.ri-messenger-fill:before{content:"\ef49"}.ri-messenger-line:before{content:"\ef4a"}.ri-meteor-fill:before{content:"\ef4b"}.ri-meteor-line:before{content:"\ef4c"}.ri-mic-2-fill:before{content:"\ef4d"}.ri-mic-2-line:before{content:"\ef4e"}.ri-mic-fill:before{content:"\ef4f"}.ri-mic-line:before{content:"\ef50"}.ri-mic-off-fill:before{content:"\ef51"}.ri-mic-off-line:before{content:"\ef52"}.ri-mickey-fill:before{content:"\ef53"}.ri-mickey-line:before{content:"\ef54"}.ri-microscope-fill:before{content:"\ef55"}.ri-microscope-line:before{content:"\ef56"}.ri-microsoft-fill:before{content:"\ef57"}.ri-microsoft-line:before{content:"\ef58"}.ri-mind-map:before{content:"\ef59"}.ri-mini-program-fill:before{content:"\ef5a"}.ri-mini-program-line:before{content:"\ef5b"}.ri-mist-fill:before{content:"\ef5c"}.ri-mist-line:before{content:"\ef5d"}.ri-money-cny-box-fill:before{content:"\ef5e"}.ri-money-cny-box-line:before{content:"\ef5f"}.ri-money-cny-circle-fill:before{content:"\ef60"}.ri-money-cny-circle-line:before{content:"\ef61"}.ri-money-dollar-box-fill:before{content:"\ef62"}.ri-money-dollar-box-line:before{content:"\ef63"}.ri-money-dollar-circle-fill:before{content:"\ef64"}.ri-money-dollar-circle-line:before{content:"\ef65"}.ri-money-euro-box-fill:before{content:"\ef66"}.ri-money-euro-box-line:before{content:"\ef67"}.ri-money-euro-circle-fill:before{content:"\ef68"}.ri-money-euro-circle-line:before{content:"\ef69"}.ri-money-pound-box-fill:before{content:"\ef6a"}.ri-money-pound-box-line:before{content:"\ef6b"}.ri-money-pound-circle-fill:before{content:"\ef6c"}.ri-money-pound-circle-line:before{content:"\ef6d"}.ri-moon-clear-fill:before{content:"\ef6e"}.ri-moon-clear-line:before{content:"\ef6f"}.ri-moon-cloudy-fill:before{content:"\ef70"}.ri-moon-cloudy-line:before{content:"\ef71"}.ri-moon-fill:before{content:"\ef72"}.ri-moon-foggy-fill:before{content:"\ef73"}.ri-moon-foggy-line:before{content:"\ef74"}.ri-moon-line:before{content:"\ef75"}.ri-more-2-fill:before{content:"\ef76"}.ri-more-2-line:before{content:"\ef77"}.ri-more-fill:before{content:"\ef78"}.ri-more-line:before{content:"\ef79"}.ri-motorbike-fill:before{content:"\ef7a"}.ri-motorbike-line:before{content:"\ef7b"}.ri-mouse-fill:before{content:"\ef7c"}.ri-mouse-line:before{content:"\ef7d"}.ri-movie-2-fill:before{content:"\ef7e"}.ri-movie-2-line:before{content:"\ef7f"}.ri-movie-fill:before{content:"\ef80"}.ri-movie-line:before{content:"\ef81"}.ri-music-2-fill:before{content:"\ef82"}.ri-music-2-line:before{content:"\ef83"}.ri-music-fill:before{content:"\ef84"}.ri-music-line:before{content:"\ef85"}.ri-mv-fill:before{content:"\ef86"}.ri-mv-line:before{content:"\ef87"}.ri-navigation-fill:before{content:"\ef88"}.ri-navigation-line:before{content:"\ef89"}.ri-netease-cloud-music-fill:before{content:"\ef8a"}.ri-netease-cloud-music-line:before{content:"\ef8b"}.ri-netflix-fill:before{content:"\ef8c"}.ri-netflix-line:before{content:"\ef8d"}.ri-newspaper-fill:before{content:"\ef8e"}.ri-newspaper-line:before{content:"\ef8f"}.ri-node-tree:before{content:"\ef90"}.ri-notification-2-fill:before{content:"\ef91"}.ri-notification-2-line:before{content:"\ef92"}.ri-notification-3-fill:before{content:"\ef93"}.ri-notification-3-line:before{content:"\ef94"}.ri-notification-4-fill:before{content:"\ef95"}.ri-notification-4-line:before{content:"\ef96"}.ri-notification-badge-fill:before{content:"\ef97"}.ri-notification-badge-line:before{content:"\ef98"}.ri-notification-fill:before{content:"\ef99"}.ri-notification-line:before{content:"\ef9a"}.ri-notification-off-fill:before{content:"\ef9b"}.ri-notification-off-line:before{content:"\ef9c"}.ri-npmjs-fill:before{content:"\ef9d"}.ri-npmjs-line:before{content:"\ef9e"}.ri-number-0:before{content:"\ef9f"}.ri-number-1:before{content:"\efa0"}.ri-number-2:before{content:"\efa1"}.ri-number-3:before{content:"\efa2"}.ri-number-4:before{content:"\efa3"}.ri-number-5:before{content:"\efa4"}.ri-number-6:before{content:"\efa5"}.ri-number-7:before{content:"\efa6"}.ri-number-8:before{content:"\efa7"}.ri-number-9:before{content:"\efa8"}.ri-numbers-fill:before{content:"\efa9"}.ri-numbers-line:before{content:"\efaa"}.ri-nurse-fill:before{content:"\efab"}.ri-nurse-line:before{content:"\efac"}.ri-oil-fill:before{content:"\efad"}.ri-oil-line:before{content:"\efae"}.ri-omega:before{content:"\efaf"}.ri-open-arm-fill:before{content:"\efb0"}.ri-open-arm-line:before{content:"\efb1"}.ri-open-source-fill:before{content:"\efb2"}.ri-open-source-line:before{content:"\efb3"}.ri-opera-fill:before{content:"\efb4"}.ri-opera-line:before{content:"\efb5"}.ri-order-play-fill:before{content:"\efb6"}.ri-order-play-line:before{content:"\efb7"}.ri-organization-chart:before{content:"\efb8"}.ri-outlet-2-fill:before{content:"\efb9"}.ri-outlet-2-line:before{content:"\efba"}.ri-outlet-fill:before{content:"\efbb"}.ri-outlet-line:before{content:"\efbc"}.ri-page-separator:before{content:"\efbd"}.ri-pages-fill:before{content:"\efbe"}.ri-pages-line:before{content:"\efbf"}.ri-paint-brush-fill:before{content:"\efc0"}.ri-paint-brush-line:before{content:"\efc1"}.ri-paint-fill:before{content:"\efc2"}.ri-paint-line:before{content:"\efc3"}.ri-palette-fill:before{content:"\efc4"}.ri-palette-line:before{content:"\efc5"}.ri-pantone-fill:before{content:"\efc6"}.ri-pantone-line:before{content:"\efc7"}.ri-paragraph:before{content:"\efc8"}.ri-parent-fill:before{content:"\efc9"}.ri-parent-line:before{content:"\efca"}.ri-parentheses-fill:before{content:"\efcb"}.ri-parentheses-line:before{content:"\efcc"}.ri-parking-box-fill:before{content:"\efcd"}.ri-parking-box-line:before{content:"\efce"}.ri-parking-fill:before{content:"\efcf"}.ri-parking-line:before{content:"\efd0"}.ri-passport-fill:before{content:"\efd1"}.ri-passport-line:before{content:"\efd2"}.ri-patreon-fill:before{content:"\efd3"}.ri-patreon-line:before{content:"\efd4"}.ri-pause-circle-fill:before{content:"\efd5"}.ri-pause-circle-line:before{content:"\efd6"}.ri-pause-fill:before{content:"\efd7"}.ri-pause-line:before{content:"\efd8"}.ri-pause-mini-fill:before{content:"\efd9"}.ri-pause-mini-line:before{content:"\efda"}.ri-paypal-fill:before{content:"\efdb"}.ri-paypal-line:before{content:"\efdc"}.ri-pen-nib-fill:before{content:"\efdd"}.ri-pen-nib-line:before{content:"\efde"}.ri-pencil-fill:before{content:"\efdf"}.ri-pencil-line:before{content:"\efe0"}.ri-pencil-ruler-2-fill:before{content:"\efe1"}.ri-pencil-ruler-2-line:before{content:"\efe2"}.ri-pencil-ruler-fill:before{content:"\efe3"}.ri-pencil-ruler-line:before{content:"\efe4"}.ri-percent-fill:before{content:"\efe5"}.ri-percent-line:before{content:"\efe6"}.ri-phone-camera-fill:before{content:"\efe7"}.ri-phone-camera-line:before{content:"\efe8"}.ri-phone-fill:before{content:"\efe9"}.ri-phone-find-fill:before{content:"\efea"}.ri-phone-find-line:before{content:"\efeb"}.ri-phone-line:before{content:"\efec"}.ri-phone-lock-fill:before{content:"\efed"}.ri-phone-lock-line:before{content:"\efee"}.ri-picture-in-picture-2-fill:before{content:"\efef"}.ri-picture-in-picture-2-line:before{content:"\eff0"}.ri-picture-in-picture-exit-fill:before{content:"\eff1"}.ri-picture-in-picture-exit-line:before{content:"\eff2"}.ri-picture-in-picture-fill:before{content:"\eff3"}.ri-picture-in-picture-line:before{content:"\eff4"}.ri-pie-chart-2-fill:before{content:"\eff5"}.ri-pie-chart-2-line:before{content:"\eff6"}.ri-pie-chart-box-fill:before{content:"\eff7"}.ri-pie-chart-box-line:before{content:"\eff8"}.ri-pie-chart-fill:before{content:"\eff9"}.ri-pie-chart-line:before{content:"\effa"}.ri-pin-distance-fill:before{content:"\effb"}.ri-pin-distance-line:before{content:"\effc"}.ri-ping-pong-fill:before{content:"\effd"}.ri-ping-pong-line:before{content:"\effe"}.ri-pinterest-fill:before{content:"\efff"}.ri-pinterest-line:before{content:"\f000"}.ri-pinyin-input:before{content:"\f001"}.ri-pixelfed-fill:before{content:"\f002"}.ri-pixelfed-line:before{content:"\f003"}.ri-plane-fill:before{content:"\f004"}.ri-plane-line:before{content:"\f005"}.ri-plant-fill:before{content:"\f006"}.ri-plant-line:before{content:"\f007"}.ri-play-circle-fill:before{content:"\f008"}.ri-play-circle-line:before{content:"\f009"}.ri-play-fill:before{content:"\f00a"}.ri-play-line:before{content:"\f00b"}.ri-play-list-2-fill:before{content:"\f00c"}.ri-play-list-2-line:before{content:"\f00d"}.ri-play-list-add-fill:before{content:"\f00e"}.ri-play-list-add-line:before{content:"\f00f"}.ri-play-list-fill:before{content:"\f010"}.ri-play-list-line:before{content:"\f011"}.ri-play-mini-fill:before{content:"\f012"}.ri-play-mini-line:before{content:"\f013"}.ri-playstation-fill:before{content:"\f014"}.ri-playstation-line:before{content:"\f015"}.ri-plug-2-fill:before{content:"\f016"}.ri-plug-2-line:before{content:"\f017"}.ri-plug-fill:before{content:"\f018"}.ri-plug-line:before{content:"\f019"}.ri-polaroid-2-fill:before{content:"\f01a"}.ri-polaroid-2-line:before{content:"\f01b"}.ri-polaroid-fill:before{content:"\f01c"}.ri-polaroid-line:before{content:"\f01d"}.ri-police-car-fill:before{content:"\f01e"}.ri-police-car-line:before{content:"\f01f"}.ri-price-tag-2-fill:before{content:"\f020"}.ri-price-tag-2-line:before{content:"\f021"}.ri-price-tag-3-fill:before{content:"\f022"}.ri-price-tag-3-line:before{content:"\f023"}.ri-price-tag-fill:before{content:"\f024"}.ri-price-tag-line:before{content:"\f025"}.ri-printer-cloud-fill:before{content:"\f026"}.ri-printer-cloud-line:before{content:"\f027"}.ri-printer-fill:before{content:"\f028"}.ri-printer-line:before{content:"\f029"}.ri-product-hunt-fill:before{content:"\f02a"}.ri-product-hunt-line:before{content:"\f02b"}.ri-profile-fill:before{content:"\f02c"}.ri-profile-line:before{content:"\f02d"}.ri-projector-2-fill:before{content:"\f02e"}.ri-projector-2-line:before{content:"\f02f"}.ri-projector-fill:before{content:"\f030"}.ri-projector-line:before{content:"\f031"}.ri-psychotherapy-fill:before{content:"\f032"}.ri-psychotherapy-line:before{content:"\f033"}.ri-pulse-fill:before{content:"\f034"}.ri-pulse-line:before{content:"\f035"}.ri-pushpin-2-fill:before{content:"\f036"}.ri-pushpin-2-line:before{content:"\f037"}.ri-pushpin-fill:before{content:"\f038"}.ri-pushpin-line:before{content:"\f039"}.ri-qq-fill:before{content:"\f03a"}.ri-qq-line:before{content:"\f03b"}.ri-qr-code-fill:before{content:"\f03c"}.ri-qr-code-line:before{content:"\f03d"}.ri-qr-scan-2-fill:before{content:"\f03e"}.ri-qr-scan-2-line:before{content:"\f03f"}.ri-qr-scan-fill:before{content:"\f040"}.ri-qr-scan-line:before{content:"\f041"}.ri-question-answer-fill:before{content:"\f042"}.ri-question-answer-line:before{content:"\f043"}.ri-question-fill:before{content:"\f044"}.ri-question-line:before{content:"\f045"}.ri-question-mark:before{content:"\f046"}.ri-questionnaire-fill:before{content:"\f047"}.ri-questionnaire-line:before{content:"\f048"}.ri-quill-pen-fill:before{content:"\f049"}.ri-quill-pen-line:before{content:"\f04a"}.ri-radar-fill:before{content:"\f04b"}.ri-radar-line:before{content:"\f04c"}.ri-radio-2-fill:before{content:"\f04d"}.ri-radio-2-line:before{content:"\f04e"}.ri-radio-button-fill:before{content:"\f04f"}.ri-radio-button-line:before{content:"\f050"}.ri-radio-fill:before{content:"\f051"}.ri-radio-line:before{content:"\f052"}.ri-rainbow-fill:before{content:"\f053"}.ri-rainbow-line:before{content:"\f054"}.ri-rainy-fill:before{content:"\f055"}.ri-rainy-line:before{content:"\f056"}.ri-reactjs-fill:before{content:"\f057"}.ri-reactjs-line:before{content:"\f058"}.ri-record-circle-fill:before{content:"\f059"}.ri-record-circle-line:before{content:"\f05a"}.ri-record-mail-fill:before{content:"\f05b"}.ri-record-mail-line:before{content:"\f05c"}.ri-recycle-fill:before{content:"\f05d"}.ri-recycle-line:before{content:"\f05e"}.ri-red-packet-fill:before{content:"\f05f"}.ri-red-packet-line:before{content:"\f060"}.ri-reddit-fill:before{content:"\f061"}.ri-reddit-line:before{content:"\f062"}.ri-refresh-fill:before{content:"\f063"}.ri-refresh-line:before{content:"\f064"}.ri-refund-2-fill:before{content:"\f065"}.ri-refund-2-line:before{content:"\f066"}.ri-refund-fill:before{content:"\f067"}.ri-refund-line:before{content:"\f068"}.ri-registered-fill:before{content:"\f069"}.ri-registered-line:before{content:"\f06a"}.ri-remixicon-fill:before{content:"\f06b"}.ri-remixicon-line:before{content:"\f06c"}.ri-remote-control-2-fill:before{content:"\f06d"}.ri-remote-control-2-line:before{content:"\f06e"}.ri-remote-control-fill:before{content:"\f06f"}.ri-remote-control-line:before{content:"\f070"}.ri-repeat-2-fill:before{content:"\f071"}.ri-repeat-2-line:before{content:"\f072"}.ri-repeat-fill:before{content:"\f073"}.ri-repeat-line:before{content:"\f074"}.ri-repeat-one-fill:before{content:"\f075"}.ri-repeat-one-line:before{content:"\f076"}.ri-reply-all-fill:before{content:"\f077"}.ri-reply-all-line:before{content:"\f078"}.ri-reply-fill:before{content:"\f079"}.ri-reply-line:before{content:"\f07a"}.ri-reserved-fill:before{content:"\f07b"}.ri-reserved-line:before{content:"\f07c"}.ri-rest-time-fill:before{content:"\f07d"}.ri-rest-time-line:before{content:"\f07e"}.ri-restart-fill:before{content:"\f07f"}.ri-restart-line:before{content:"\f080"}.ri-restaurant-2-fill:before{content:"\f081"}.ri-restaurant-2-line:before{content:"\f082"}.ri-restaurant-fill:before{content:"\f083"}.ri-restaurant-line:before{content:"\f084"}.ri-rewind-fill:before{content:"\f085"}.ri-rewind-line:before{content:"\f086"}.ri-rewind-mini-fill:before{content:"\f087"}.ri-rewind-mini-line:before{content:"\f088"}.ri-rhythm-fill:before{content:"\f089"}.ri-rhythm-line:before{content:"\f08a"}.ri-riding-fill:before{content:"\f08b"}.ri-riding-line:before{content:"\f08c"}.ri-road-map-fill:before{content:"\f08d"}.ri-road-map-line:before{content:"\f08e"}.ri-roadster-fill:before{content:"\f08f"}.ri-roadster-line:before{content:"\f090"}.ri-robot-fill:before{content:"\f091"}.ri-robot-line:before{content:"\f092"}.ri-rocket-2-fill:before{content:"\f093"}.ri-rocket-2-line:before{content:"\f094"}.ri-rocket-fill:before{content:"\f095"}.ri-rocket-line:before{content:"\f096"}.ri-rotate-lock-fill:before{content:"\f097"}.ri-rotate-lock-line:before{content:"\f098"}.ri-rounded-corner:before{content:"\f099"}.ri-route-fill:before{content:"\f09a"}.ri-route-line:before{content:"\f09b"}.ri-router-fill:before{content:"\f09c"}.ri-router-line:before{content:"\f09d"}.ri-rss-fill:before{content:"\f09e"}.ri-rss-line:before{content:"\f09f"}.ri-ruler-2-fill:before{content:"\f0a0"}.ri-ruler-2-line:before{content:"\f0a1"}.ri-ruler-fill:before{content:"\f0a2"}.ri-ruler-line:before{content:"\f0a3"}.ri-run-fill:before{content:"\f0a4"}.ri-run-line:before{content:"\f0a5"}.ri-safari-fill:before{content:"\f0a6"}.ri-safari-line:before{content:"\f0a7"}.ri-safe-2-fill:before{content:"\f0a8"}.ri-safe-2-line:before{content:"\f0a9"}.ri-safe-fill:before{content:"\f0aa"}.ri-safe-line:before{content:"\f0ab"}.ri-sailboat-fill:before{content:"\f0ac"}.ri-sailboat-line:before{content:"\f0ad"}.ri-save-2-fill:before{content:"\f0ae"}.ri-save-2-line:before{content:"\f0af"}.ri-save-3-fill:before{content:"\f0b0"}.ri-save-3-line:before{content:"\f0b1"}.ri-save-fill:before{content:"\f0b2"}.ri-save-line:before{content:"\f0b3"}.ri-scales-2-fill:before{content:"\f0b4"}.ri-scales-2-line:before{content:"\f0b5"}.ri-scales-3-fill:before{content:"\f0b6"}.ri-scales-3-line:before{content:"\f0b7"}.ri-scales-fill:before{content:"\f0b8"}.ri-scales-line:before{content:"\f0b9"}.ri-scan-2-fill:before{content:"\f0ba"}.ri-scan-2-line:before{content:"\f0bb"}.ri-scan-fill:before{content:"\f0bc"}.ri-scan-line:before{content:"\f0bd"}.ri-scissors-2-fill:before{content:"\f0be"}.ri-scissors-2-line:before{content:"\f0bf"}.ri-scissors-cut-fill:before{content:"\f0c0"}.ri-scissors-cut-line:before{content:"\f0c1"}.ri-scissors-fill:before{content:"\f0c2"}.ri-scissors-line:before{content:"\f0c3"}.ri-screenshot-2-fill:before{content:"\f0c4"}.ri-screenshot-2-line:before{content:"\f0c5"}.ri-screenshot-fill:before{content:"\f0c6"}.ri-screenshot-line:before{content:"\f0c7"}.ri-sd-card-fill:before{content:"\f0c8"}.ri-sd-card-line:before{content:"\f0c9"}.ri-sd-card-mini-fill:before{content:"\f0ca"}.ri-sd-card-mini-line:before{content:"\f0cb"}.ri-search-2-fill:before{content:"\f0cc"}.ri-search-2-line:before{content:"\f0cd"}.ri-search-eye-fill:before{content:"\f0ce"}.ri-search-eye-line:before{content:"\f0cf"}.ri-search-fill:before{content:"\f0d0"}.ri-search-line:before{content:"\f0d1"}.ri-secure-payment-fill:before{content:"\f0d2"}.ri-secure-payment-line:before{content:"\f0d3"}.ri-seedling-fill:before{content:"\f0d4"}.ri-seedling-line:before{content:"\f0d5"}.ri-send-backward:before{content:"\f0d6"}.ri-send-plane-2-fill:before{content:"\f0d7"}.ri-send-plane-2-line:before{content:"\f0d8"}.ri-send-plane-fill:before{content:"\f0d9"}.ri-send-plane-line:before{content:"\f0da"}.ri-send-to-back:before{content:"\f0db"}.ri-sensor-fill:before{content:"\f0dc"}.ri-sensor-line:before{content:"\f0dd"}.ri-separator:before{content:"\f0de"}.ri-server-fill:before{content:"\f0df"}.ri-server-line:before{content:"\f0e0"}.ri-service-fill:before{content:"\f0e1"}.ri-service-line:before{content:"\f0e2"}.ri-settings-2-fill:before{content:"\f0e3"}.ri-settings-2-line:before{content:"\f0e4"}.ri-settings-3-fill:before{content:"\f0e5"}.ri-settings-3-line:before{content:"\f0e6"}.ri-settings-4-fill:before{content:"\f0e7"}.ri-settings-4-line:before{content:"\f0e8"}.ri-settings-5-fill:before{content:"\f0e9"}.ri-settings-5-line:before{content:"\f0ea"}.ri-settings-6-fill:before{content:"\f0eb"}.ri-settings-6-line:before{content:"\f0ec"}.ri-settings-fill:before{content:"\f0ed"}.ri-settings-line:before{content:"\f0ee"}.ri-shape-2-fill:before{content:"\f0ef"}.ri-shape-2-line:before{content:"\f0f0"}.ri-shape-fill:before{content:"\f0f1"}.ri-shape-line:before{content:"\f0f2"}.ri-share-box-fill:before{content:"\f0f3"}.ri-share-box-line:before{content:"\f0f4"}.ri-share-circle-fill:before{content:"\f0f5"}.ri-share-circle-line:before{content:"\f0f6"}.ri-share-fill:before{content:"\f0f7"}.ri-share-forward-2-fill:before{content:"\f0f8"}.ri-share-forward-2-line:before{content:"\f0f9"}.ri-share-forward-box-fill:before{content:"\f0fa"}.ri-share-forward-box-line:before{content:"\f0fb"}.ri-share-forward-fill:before{content:"\f0fc"}.ri-share-forward-line:before{content:"\f0fd"}.ri-share-line:before{content:"\f0fe"}.ri-shield-check-fill:before{content:"\f0ff"}.ri-shield-check-line:before{content:"\f100"}.ri-shield-cross-fill:before{content:"\f101"}.ri-shield-cross-line:before{content:"\f102"}.ri-shield-fill:before{content:"\f103"}.ri-shield-flash-fill:before{content:"\f104"}.ri-shield-flash-line:before{content:"\f105"}.ri-shield-keyhole-fill:before{content:"\f106"}.ri-shield-keyhole-line:before{content:"\f107"}.ri-shield-line:before{content:"\f108"}.ri-shield-star-fill:before{content:"\f109"}.ri-shield-star-line:before{content:"\f10a"}.ri-shield-user-fill:before{content:"\f10b"}.ri-shield-user-line:before{content:"\f10c"}.ri-ship-2-fill:before{content:"\f10d"}.ri-ship-2-line:before{content:"\f10e"}.ri-ship-fill:before{content:"\f10f"}.ri-ship-line:before{content:"\f110"}.ri-shirt-fill:before{content:"\f111"}.ri-shirt-line:before{content:"\f112"}.ri-shopping-bag-2-fill:before{content:"\f113"}.ri-shopping-bag-2-line:before{content:"\f114"}.ri-shopping-bag-3-fill:before{content:"\f115"}.ri-shopping-bag-3-line:before{content:"\f116"}.ri-shopping-bag-fill:before{content:"\f117"}.ri-shopping-bag-line:before{content:"\f118"}.ri-shopping-basket-2-fill:before{content:"\f119"}.ri-shopping-basket-2-line:before{content:"\f11a"}.ri-shopping-basket-fill:before{content:"\f11b"}.ri-shopping-basket-line:before{content:"\f11c"}.ri-shopping-cart-2-fill:before{content:"\f11d"}.ri-shopping-cart-2-line:before{content:"\f11e"}.ri-shopping-cart-fill:before{content:"\f11f"}.ri-shopping-cart-line:before{content:"\f120"}.ri-showers-fill:before{content:"\f121"}.ri-showers-line:before{content:"\f122"}.ri-shuffle-fill:before{content:"\f123"}.ri-shuffle-line:before{content:"\f124"}.ri-shut-down-fill:before{content:"\f125"}.ri-shut-down-line:before{content:"\f126"}.ri-side-bar-fill:before{content:"\f127"}.ri-side-bar-line:before{content:"\f128"}.ri-signal-tower-fill:before{content:"\f129"}.ri-signal-tower-line:before{content:"\f12a"}.ri-signal-wifi-1-fill:before{content:"\f12b"}.ri-signal-wifi-1-line:before{content:"\f12c"}.ri-signal-wifi-2-fill:before{content:"\f12d"}.ri-signal-wifi-2-line:before{content:"\f12e"}.ri-signal-wifi-3-fill:before{content:"\f12f"}.ri-signal-wifi-3-line:before{content:"\f130"}.ri-signal-wifi-error-fill:before{content:"\f131"}.ri-signal-wifi-error-line:before{content:"\f132"}.ri-signal-wifi-fill:before{content:"\f133"}.ri-signal-wifi-line:before{content:"\f134"}.ri-signal-wifi-off-fill:before{content:"\f135"}.ri-signal-wifi-off-line:before{content:"\f136"}.ri-sim-card-2-fill:before{content:"\f137"}.ri-sim-card-2-line:before{content:"\f138"}.ri-sim-card-fill:before{content:"\f139"}.ri-sim-card-line:before{content:"\f13a"}.ri-single-quotes-l:before{content:"\f13b"}.ri-single-quotes-r:before{content:"\f13c"}.ri-sip-fill:before{content:"\f13d"}.ri-sip-line:before{content:"\f13e"}.ri-skip-back-fill:before{content:"\f13f"}.ri-skip-back-line:before{content:"\f140"}.ri-skip-back-mini-fill:before{content:"\f141"}.ri-skip-back-mini-line:before{content:"\f142"}.ri-skip-forward-fill:before{content:"\f143"}.ri-skip-forward-line:before{content:"\f144"}.ri-skip-forward-mini-fill:before{content:"\f145"}.ri-skip-forward-mini-line:before{content:"\f146"}.ri-skull-2-fill:before{content:"\f147"}.ri-skull-2-line:before{content:"\f148"}.ri-skull-fill:before{content:"\f149"}.ri-skull-line:before{content:"\f14a"}.ri-skype-fill:before{content:"\f14b"}.ri-skype-line:before{content:"\f14c"}.ri-slack-fill:before{content:"\f14d"}.ri-slack-line:before{content:"\f14e"}.ri-slice-fill:before{content:"\f14f"}.ri-slice-line:before{content:"\f150"}.ri-slideshow-2-fill:before{content:"\f151"}.ri-slideshow-2-line:before{content:"\f152"}.ri-slideshow-3-fill:before{content:"\f153"}.ri-slideshow-3-line:before{content:"\f154"}.ri-slideshow-4-fill:before{content:"\f155"}.ri-slideshow-4-line:before{content:"\f156"}.ri-slideshow-fill:before{content:"\f157"}.ri-slideshow-line:before{content:"\f158"}.ri-smartphone-fill:before{content:"\f159"}.ri-smartphone-line:before{content:"\f15a"}.ri-snapchat-fill:before{content:"\f15b"}.ri-snapchat-line:before{content:"\f15c"}.ri-snowy-fill:before{content:"\f15d"}.ri-snowy-line:before{content:"\f15e"}.ri-sort-asc:before{content:"\f15f"}.ri-sort-desc:before{content:"\f160"}.ri-sound-module-fill:before{content:"\f161"}.ri-sound-module-line:before{content:"\f162"}.ri-soundcloud-fill:before{content:"\f163"}.ri-soundcloud-line:before{content:"\f164"}.ri-space-ship-fill:before{content:"\f165"}.ri-space-ship-line:before{content:"\f166"}.ri-space:before{content:"\f167"}.ri-spam-2-fill:before{content:"\f168"}.ri-spam-2-line:before{content:"\f169"}.ri-spam-3-fill:before{content:"\f16a"}.ri-spam-3-line:before{content:"\f16b"}.ri-spam-fill:before{content:"\f16c"}.ri-spam-line:before{content:"\f16d"}.ri-speaker-2-fill:before{content:"\f16e"}.ri-speaker-2-line:before{content:"\f16f"}.ri-speaker-3-fill:before{content:"\f170"}.ri-speaker-3-line:before{content:"\f171"}.ri-speaker-fill:before{content:"\f172"}.ri-speaker-line:before{content:"\f173"}.ri-spectrum-fill:before{content:"\f174"}.ri-spectrum-line:before{content:"\f175"}.ri-speed-fill:before{content:"\f176"}.ri-speed-line:before{content:"\f177"}.ri-speed-mini-fill:before{content:"\f178"}.ri-speed-mini-line:before{content:"\f179"}.ri-split-cells-horizontal:before{content:"\f17a"}.ri-split-cells-vertical:before{content:"\f17b"}.ri-spotify-fill:before{content:"\f17c"}.ri-spotify-line:before{content:"\f17d"}.ri-spy-fill:before{content:"\f17e"}.ri-spy-line:before{content:"\f17f"}.ri-stack-fill:before{content:"\f180"}.ri-stack-line:before{content:"\f181"}.ri-stack-overflow-fill:before{content:"\f182"}.ri-stack-overflow-line:before{content:"\f183"}.ri-stackshare-fill:before{content:"\f184"}.ri-stackshare-line:before{content:"\f185"}.ri-star-fill:before{content:"\f186"}.ri-star-half-fill:before{content:"\f187"}.ri-star-half-line:before{content:"\f188"}.ri-star-half-s-fill:before{content:"\f189"}.ri-star-half-s-line:before{content:"\f18a"}.ri-star-line:before{content:"\f18b"}.ri-star-s-fill:before{content:"\f18c"}.ri-star-s-line:before{content:"\f18d"}.ri-star-smile-fill:before{content:"\f18e"}.ri-star-smile-line:before{content:"\f18f"}.ri-steam-fill:before{content:"\f190"}.ri-steam-line:before{content:"\f191"}.ri-steering-2-fill:before{content:"\f192"}.ri-steering-2-line:before{content:"\f193"}.ri-steering-fill:before{content:"\f194"}.ri-steering-line:before{content:"\f195"}.ri-stethoscope-fill:before{content:"\f196"}.ri-stethoscope-line:before{content:"\f197"}.ri-sticky-note-2-fill:before{content:"\f198"}.ri-sticky-note-2-line:before{content:"\f199"}.ri-sticky-note-fill:before{content:"\f19a"}.ri-sticky-note-line:before{content:"\f19b"}.ri-stock-fill:before{content:"\f19c"}.ri-stock-line:before{content:"\f19d"}.ri-stop-circle-fill:before{content:"\f19e"}.ri-stop-circle-line:before{content:"\f19f"}.ri-stop-fill:before{content:"\f1a0"}.ri-stop-line:before{content:"\f1a1"}.ri-stop-mini-fill:before{content:"\f1a2"}.ri-stop-mini-line:before{content:"\f1a3"}.ri-store-2-fill:before{content:"\f1a4"}.ri-store-2-line:before{content:"\f1a5"}.ri-store-3-fill:before{content:"\f1a6"}.ri-store-3-line:before{content:"\f1a7"}.ri-store-fill:before{content:"\f1a8"}.ri-store-line:before{content:"\f1a9"}.ri-strikethrough-2:before{content:"\f1aa"}.ri-strikethrough:before{content:"\f1ab"}.ri-subscript-2:before{content:"\f1ac"}.ri-subscript:before{content:"\f1ad"}.ri-subtract-fill:before{content:"\f1ae"}.ri-subtract-line:before{content:"\f1af"}.ri-subway-fill:before{content:"\f1b0"}.ri-subway-line:before{content:"\f1b1"}.ri-subway-wifi-fill:before{content:"\f1b2"}.ri-subway-wifi-line:before{content:"\f1b3"}.ri-suitcase-2-fill:before{content:"\f1b4"}.ri-suitcase-2-line:before{content:"\f1b5"}.ri-suitcase-3-fill:before{content:"\f1b6"}.ri-suitcase-3-line:before{content:"\f1b7"}.ri-suitcase-fill:before{content:"\f1b8"}.ri-suitcase-line:before{content:"\f1b9"}.ri-sun-cloudy-fill:before{content:"\f1ba"}.ri-sun-cloudy-line:before{content:"\f1bb"}.ri-sun-fill:before{content:"\f1bc"}.ri-sun-foggy-fill:before{content:"\f1bd"}.ri-sun-foggy-line:before{content:"\f1be"}.ri-sun-line:before{content:"\f1bf"}.ri-superscript-2:before{content:"\f1c0"}.ri-superscript:before{content:"\f1c1"}.ri-surgical-mask-fill:before{content:"\f1c2"}.ri-surgical-mask-line:before{content:"\f1c3"}.ri-surround-sound-fill:before{content:"\f1c4"}.ri-surround-sound-line:before{content:"\f1c5"}.ri-survey-fill:before{content:"\f1c6"}.ri-survey-line:before{content:"\f1c7"}.ri-swap-box-fill:before{content:"\f1c8"}.ri-swap-box-line:before{content:"\f1c9"}.ri-swap-fill:before{content:"\f1ca"}.ri-swap-line:before{content:"\f1cb"}.ri-switch-fill:before{content:"\f1cc"}.ri-switch-line:before{content:"\f1cd"}.ri-sword-fill:before{content:"\f1ce"}.ri-sword-line:before{content:"\f1cf"}.ri-syringe-fill:before{content:"\f1d0"}.ri-syringe-line:before{content:"\f1d1"}.ri-t-box-fill:before{content:"\f1d2"}.ri-t-box-line:before{content:"\f1d3"}.ri-t-shirt-2-fill:before{content:"\f1d4"}.ri-t-shirt-2-line:before{content:"\f1d5"}.ri-t-shirt-air-fill:before{content:"\f1d6"}.ri-t-shirt-air-line:before{content:"\f1d7"}.ri-t-shirt-fill:before{content:"\f1d8"}.ri-t-shirt-line:before{content:"\f1d9"}.ri-table-2:before{content:"\f1da"}.ri-table-alt-fill:before{content:"\f1db"}.ri-table-alt-line:before{content:"\f1dc"}.ri-table-fill:before{content:"\f1dd"}.ri-table-line:before{content:"\f1de"}.ri-tablet-fill:before{content:"\f1df"}.ri-tablet-line:before{content:"\f1e0"}.ri-takeaway-fill:before{content:"\f1e1"}.ri-takeaway-line:before{content:"\f1e2"}.ri-taobao-fill:before{content:"\f1e3"}.ri-taobao-line:before{content:"\f1e4"}.ri-tape-fill:before{content:"\f1e5"}.ri-tape-line:before{content:"\f1e6"}.ri-task-fill:before{content:"\f1e7"}.ri-task-line:before{content:"\f1e8"}.ri-taxi-fill:before{content:"\f1e9"}.ri-taxi-line:before{content:"\f1ea"}.ri-taxi-wifi-fill:before{content:"\f1eb"}.ri-taxi-wifi-line:before{content:"\f1ec"}.ri-team-fill:before{content:"\f1ed"}.ri-team-line:before{content:"\f1ee"}.ri-telegram-fill:before{content:"\f1ef"}.ri-telegram-line:before{content:"\f1f0"}.ri-temp-cold-fill:before{content:"\f1f1"}.ri-temp-cold-line:before{content:"\f1f2"}.ri-temp-hot-fill:before{content:"\f1f3"}.ri-temp-hot-line:before{content:"\f1f4"}.ri-terminal-box-fill:before{content:"\f1f5"}.ri-terminal-box-line:before{content:"\f1f6"}.ri-terminal-fill:before{content:"\f1f7"}.ri-terminal-line:before{content:"\f1f8"}.ri-terminal-window-fill:before{content:"\f1f9"}.ri-terminal-window-line:before{content:"\f1fa"}.ri-test-tube-fill:before{content:"\f1fb"}.ri-test-tube-line:before{content:"\f1fc"}.ri-text-direction-l:before{content:"\f1fd"}.ri-text-direction-r:before{content:"\f1fe"}.ri-text-spacing:before{content:"\f1ff"}.ri-text-wrap:before{content:"\f200"}.ri-text:before{content:"\f201"}.ri-thermometer-fill:before{content:"\f202"}.ri-thermometer-line:before{content:"\f203"}.ri-thumb-down-fill:before{content:"\f204"}.ri-thumb-down-line:before{content:"\f205"}.ri-thumb-up-fill:before{content:"\f206"}.ri-thumb-up-line:before{content:"\f207"}.ri-thunderstorms-fill:before{content:"\f208"}.ri-thunderstorms-line:before{content:"\f209"}.ri-ticket-2-fill:before{content:"\f20a"}.ri-ticket-2-line:before{content:"\f20b"}.ri-ticket-fill:before{content:"\f20c"}.ri-ticket-line:before{content:"\f20d"}.ri-time-fill:before{content:"\f20e"}.ri-time-line:before{content:"\f20f"}.ri-timer-2-fill:before{content:"\f210"}.ri-timer-2-line:before{content:"\f211"}.ri-timer-fill:before{content:"\f212"}.ri-timer-flash-fill:before{content:"\f213"}.ri-timer-flash-line:before{content:"\f214"}.ri-timer-line:before{content:"\f215"}.ri-todo-fill:before{content:"\f216"}.ri-todo-line:before{content:"\f217"}.ri-toggle-fill:before{content:"\f218"}.ri-toggle-line:before{content:"\f219"}.ri-tools-fill:before{content:"\f21a"}.ri-tools-line:before{content:"\f21b"}.ri-tornado-fill:before{content:"\f21c"}.ri-tornado-line:before{content:"\f21d"}.ri-trademark-fill:before{content:"\f21e"}.ri-trademark-line:before{content:"\f21f"}.ri-traffic-light-fill:before{content:"\f220"}.ri-traffic-light-line:before{content:"\f221"}.ri-train-fill:before{content:"\f222"}.ri-train-line:before{content:"\f223"}.ri-train-wifi-fill:before{content:"\f224"}.ri-train-wifi-line:before{content:"\f225"}.ri-translate-2:before{content:"\f226"}.ri-translate:before{content:"\f227"}.ri-travesti-fill:before{content:"\f228"}.ri-travesti-line:before{content:"\f229"}.ri-treasure-map-fill:before{content:"\f22a"}.ri-treasure-map-line:before{content:"\f22b"}.ri-trello-fill:before{content:"\f22c"}.ri-trello-line:before{content:"\f22d"}.ri-trophy-fill:before{content:"\f22e"}.ri-trophy-line:before{content:"\f22f"}.ri-truck-fill:before{content:"\f230"}.ri-truck-line:before{content:"\f231"}.ri-tumblr-fill:before{content:"\f232"}.ri-tumblr-line:before{content:"\f233"}.ri-tv-2-fill:before{content:"\f234"}.ri-tv-2-line:before{content:"\f235"}.ri-tv-fill:before{content:"\f236"}.ri-tv-line:before{content:"\f237"}.ri-twitch-fill:before{content:"\f238"}.ri-twitch-line:before{content:"\f239"}.ri-twitter-fill:before{content:"\f23a"}.ri-twitter-line:before{content:"\f23b"}.ri-typhoon-fill:before{content:"\f23c"}.ri-typhoon-line:before{content:"\f23d"}.ri-u-disk-fill:before{content:"\f23e"}.ri-u-disk-line:before{content:"\f23f"}.ri-ubuntu-fill:before{content:"\f240"}.ri-ubuntu-line:before{content:"\f241"}.ri-umbrella-fill:before{content:"\f242"}.ri-umbrella-line:before{content:"\f243"}.ri-underline:before{content:"\f244"}.ri-uninstall-fill:before{content:"\f245"}.ri-uninstall-line:before{content:"\f246"}.ri-unsplash-fill:before{content:"\f247"}.ri-unsplash-line:before{content:"\f248"}.ri-upload-2-fill:before{content:"\f249"}.ri-upload-2-line:before{content:"\f24a"}.ri-upload-cloud-2-fill:before{content:"\f24b"}.ri-upload-cloud-2-line:before{content:"\f24c"}.ri-upload-cloud-fill:before{content:"\f24d"}.ri-upload-cloud-line:before{content:"\f24e"}.ri-upload-fill:before{content:"\f24f"}.ri-upload-line:before{content:"\f250"}.ri-usb-fill:before{content:"\f251"}.ri-usb-line:before{content:"\f252"}.ri-user-2-fill:before{content:"\f253"}.ri-user-2-line:before{content:"\f254"}.ri-user-3-fill:before{content:"\f255"}.ri-user-3-line:before{content:"\f256"}.ri-user-4-fill:before{content:"\f257"}.ri-user-4-line:before{content:"\f258"}.ri-user-5-fill:before{content:"\f259"}.ri-user-5-line:before{content:"\f25a"}.ri-user-6-fill:before{content:"\f25b"}.ri-user-6-line:before{content:"\f25c"}.ri-user-add-fill:before{content:"\f25d"}.ri-user-add-line:before{content:"\f25e"}.ri-user-fill:before{content:"\f25f"}.ri-user-follow-fill:before{content:"\f260"}.ri-user-follow-line:before{content:"\f261"}.ri-user-heart-fill:before{content:"\f262"}.ri-user-heart-line:before{content:"\f263"}.ri-user-line:before{content:"\f264"}.ri-user-location-fill:before{content:"\f265"}.ri-user-location-line:before{content:"\f266"}.ri-user-received-2-fill:before{content:"\f267"}.ri-user-received-2-line:before{content:"\f268"}.ri-user-received-fill:before{content:"\f269"}.ri-user-received-line:before{content:"\f26a"}.ri-user-search-fill:before{content:"\f26b"}.ri-user-search-line:before{content:"\f26c"}.ri-user-settings-fill:before{content:"\f26d"}.ri-user-settings-line:before{content:"\f26e"}.ri-user-shared-2-fill:before{content:"\f26f"}.ri-user-shared-2-line:before{content:"\f270"}.ri-user-shared-fill:before{content:"\f271"}.ri-user-shared-line:before{content:"\f272"}.ri-user-smile-fill:before{content:"\f273"}.ri-user-smile-line:before{content:"\f274"}.ri-user-star-fill:before{content:"\f275"}.ri-user-star-line:before{content:"\f276"}.ri-user-unfollow-fill:before{content:"\f277"}.ri-user-unfollow-line:before{content:"\f278"}.ri-user-voice-fill:before{content:"\f279"}.ri-user-voice-line:before{content:"\f27a"}.ri-video-add-fill:before{content:"\f27b"}.ri-video-add-line:before{content:"\f27c"}.ri-video-chat-fill:before{content:"\f27d"}.ri-video-chat-line:before{content:"\f27e"}.ri-video-download-fill:before{content:"\f27f"}.ri-video-download-line:before{content:"\f280"}.ri-video-fill:before{content:"\f281"}.ri-video-line:before{content:"\f282"}.ri-video-upload-fill:before{content:"\f283"}.ri-video-upload-line:before{content:"\f284"}.ri-vidicon-2-fill:before{content:"\f285"}.ri-vidicon-2-line:before{content:"\f286"}.ri-vidicon-fill:before{content:"\f287"}.ri-vidicon-line:before{content:"\f288"}.ri-vimeo-fill:before{content:"\f289"}.ri-vimeo-line:before{content:"\f28a"}.ri-vip-crown-2-fill:before{content:"\f28b"}.ri-vip-crown-2-line:before{content:"\f28c"}.ri-vip-crown-fill:before{content:"\f28d"}.ri-vip-crown-line:before{content:"\f28e"}.ri-vip-diamond-fill:before{content:"\f28f"}.ri-vip-diamond-line:before{content:"\f290"}.ri-vip-fill:before{content:"\f291"}.ri-vip-line:before{content:"\f292"}.ri-virus-fill:before{content:"\f293"}.ri-virus-line:before{content:"\f294"}.ri-visa-fill:before{content:"\f295"}.ri-visa-line:before{content:"\f296"}.ri-voice-recognition-fill:before{content:"\f297"}.ri-voice-recognition-line:before{content:"\f298"}.ri-voiceprint-fill:before{content:"\f299"}.ri-voiceprint-line:before{content:"\f29a"}.ri-volume-down-fill:before{content:"\f29b"}.ri-volume-down-line:before{content:"\f29c"}.ri-volume-mute-fill:before{content:"\f29d"}.ri-volume-mute-line:before{content:"\f29e"}.ri-volume-off-vibrate-fill:before{content:"\f29f"}.ri-volume-off-vibrate-line:before{content:"\f2a0"}.ri-volume-up-fill:before{content:"\f2a1"}.ri-volume-up-line:before{content:"\f2a2"}.ri-volume-vibrate-fill:before{content:"\f2a3"}.ri-volume-vibrate-line:before{content:"\f2a4"}.ri-vuejs-fill:before{content:"\f2a5"}.ri-vuejs-line:before{content:"\f2a6"}.ri-walk-fill:before{content:"\f2a7"}.ri-walk-line:before{content:"\f2a8"}.ri-wallet-2-fill:before{content:"\f2a9"}.ri-wallet-2-line:before{content:"\f2aa"}.ri-wallet-3-fill:before{content:"\f2ab"}.ri-wallet-3-line:before{content:"\f2ac"}.ri-wallet-fill:before{content:"\f2ad"}.ri-wallet-line:before{content:"\f2ae"}.ri-water-flash-fill:before{content:"\f2af"}.ri-water-flash-line:before{content:"\f2b0"}.ri-webcam-fill:before{content:"\f2b1"}.ri-webcam-line:before{content:"\f2b2"}.ri-wechat-2-fill:before{content:"\f2b3"}.ri-wechat-2-line:before{content:"\f2b4"}.ri-wechat-fill:before{content:"\f2b5"}.ri-wechat-line:before{content:"\f2b6"}.ri-wechat-pay-fill:before{content:"\f2b7"}.ri-wechat-pay-line:before{content:"\f2b8"}.ri-weibo-fill:before{content:"\f2b9"}.ri-weibo-line:before{content:"\f2ba"}.ri-whatsapp-fill:before{content:"\f2bb"}.ri-whatsapp-line:before{content:"\f2bc"}.ri-wheelchair-fill:before{content:"\f2bd"}.ri-wheelchair-line:before{content:"\f2be"}.ri-wifi-fill:before{content:"\f2bf"}.ri-wifi-line:before{content:"\f2c0"}.ri-wifi-off-fill:before{content:"\f2c1"}.ri-wifi-off-line:before{content:"\f2c2"}.ri-window-2-fill:before{content:"\f2c3"}.ri-window-2-line:before{content:"\f2c4"}.ri-window-fill:before{content:"\f2c5"}.ri-window-line:before{content:"\f2c6"}.ri-windows-fill:before{content:"\f2c7"}.ri-windows-line:before{content:"\f2c8"}.ri-windy-fill:before{content:"\f2c9"}.ri-windy-line:before{content:"\f2ca"}.ri-wireless-charging-fill:before{content:"\f2cb"}.ri-wireless-charging-line:before{content:"\f2cc"}.ri-women-fill:before{content:"\f2cd"}.ri-women-line:before{content:"\f2ce"}.ri-wubi-input:before{content:"\f2cf"}.ri-xbox-fill:before{content:"\f2d0"}.ri-xbox-line:before{content:"\f2d1"}.ri-xing-fill:before{content:"\f2d2"}.ri-xing-line:before{content:"\f2d3"}.ri-youtube-fill:before{content:"\f2d4"}.ri-youtube-line:before{content:"\f2d5"}.ri-zcool-fill:before{content:"\f2d6"}.ri-zcool-line:before{content:"\f2d7"}.ri-zhihu-fill:before{content:"\f2d8"}.ri-zhihu-line:before{content:"\f2d9"}.ri-zoom-in-fill:before{content:"\f2da"}.ri-zoom-in-line:before{content:"\f2db"}.ri-zoom-out-fill:before{content:"\f2dc"}.ri-zoom-out-line:before{content:"\f2dd"}.ri-zzz-fill:before{content:"\f2de"}.ri-zzz-line:before{content:"\f2df"}@keyframes rotate{to{transform:rotate(360deg)}}@keyframes expand{0%{transform:rotateY(90deg)}to{opacity:1;transform:rotateY(0)}}@keyframes slideIn{0%{opacity:0;transform:translateY(5px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes shine{to{background-position-x:-200%}}@keyframes loaderShow{0%{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}@media screen and (min-width: 550px){::-webkit-scrollbar{width:8px;height:8px;border-radius:var(--baseRadius)}::-webkit-scrollbar-track{background:transparent;border-radius:var(--baseRadius)}::-webkit-scrollbar-thumb{background-color:var(--baseAlt2Color);border-radius:15px;border:2px solid transparent;background-clip:padding-box}::-webkit-scrollbar-thumb:hover,::-webkit-scrollbar-thumb:active{background-color:var(--baseAlt3Color)}html{scrollbar-color:var(--baseAlt2Color) transparent;scrollbar-width:thin;scroll-behavior:smooth}html *{scrollbar-width:inherit}}:focus-visible{outline-color:var(--primaryColor);outline-style:solid}html,body{line-height:var(--baseLineHeight);font-family:var(--baseFontFamily);font-size:var(--baseFontSize);color:var(--txtPrimaryColor);background:var(--bodyColor)}#app{overflow:auto;display:block;width:100%;height:100vh}.flatpickr-inline-container,.accordion .accordion-content,.accordion,.tabs,.tabs-content,.page-sidebar .sidebar-title,.form-field-file .files-list,.select .txt-missing,.skeleton-loader,.clearfix,.content,.form-field .help-block,.overlay-panel .panel-content,.panel,.block,blockquote,p{display:block;width:100%}h1,h2,.breadcrumbs .breadcrumb-item,h3,h4,h5,h6{margin:0;font-weight:400}h1{font-size:22px;line-height:28px}h2,.breadcrumbs .breadcrumb-item{font-size:20px;line-height:26px}h3{font-size:19px;line-height:24px}h4{font-size:18px;line-height:24px}h5{font-size:17px;line-height:24px}h6{font-size:16px;line-height:22px}em{font-style:italic}strong{font-weight:600}small{font-size:var(--smFontSize);line-height:var(--smLineHeight)}sub,sup{position:relative;font-size:.75em;line-height:1}sup{vertical-align:top}sub{vertical-align:bottom}p{margin:5px 0}blockquote{position:relative;padding-left:var(--smSpacing);font-style:italic;color:var(--txtHintColor)}blockquote:before{content:"";position:absolute;top:0;left:0;width:2px;height:100%;background:var(--baseColor)}code{display:inline-block;font-family:var(--monospaceFontFamily);font-size:15px;line-height:1.379rem;padding:0 4px;white-space:nowrap;color:var(--txtPrimaryColor);background:var(--baseAlt2Color);border-radius:var(--baseRadius)}ol,ul{margin:10px 0;list-style:decimal;padding-left:var(--baseSpacing)}ol li,ul li{margin-top:5px;margin-bottom:5px}ul{list-style:disc}img{max-width:100%;vertical-align:top}hr{display:block;border:0;height:1px;width:100%;background:var(--baseAlt1Color);margin:var(--baseSpacing) 0}a{color:inherit}a:hover{text-decoration:none}a i,a .txt{display:inline-block;vertical-align:top}.txt-mono{font-family:var(--monospaceFontFamily)}.txt-nowrap{white-space:nowrap}.txt-ellipsis{display:inline-block;vertical-align:top;flex-shrink:0;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.txt-base{font-size:var(--baseFontSize)!important}.txt-xs{font-size:var(--xsFontSize)!important;line-height:var(--smLineHeight)}.txt-sm{font-size:var(--smFontSize)!important;line-height:var(--smLineHeight)}.txt-lg{font-size:var(--lgFontSize)!important}.txt-xl{font-size:var(--xlFontSize)!important}.txt-bold{font-weight:600!important}.txt-strikethrough{text-decoration:line-through!important}.txt-break{white-space:pre-wrap!important}.txt-center{text-align:center!important}.txt-left{text-align:left!important}.txt-right{text-align:right!important}.txt-main{color:var(--txtPrimaryColor)!important}.txt-hint{color:var(--txtHintColor)!important}.txt-disabled{color:var(--txtDisabledColor)!important}.link-hint{user-select:none;cursor:pointer;color:var(--txtHintColor)!important;text-decoration:none;transition:color var(--baseAnimationSpeed)}.link-hint:hover,.link-hint:focus-visible,.link-hint:active{color:var(--txtPrimaryColor)!important}.link-fade{opacity:1;user-select:none;cursor:pointer;text-decoration:none;color:var(--txtPrimaryColor);transition:opacity var(--baseAnimationSpeed)}.link-fade:focus-visible,.link-fade:hover,.link-fade:active{opacity:.8}.txt-primary{color:var(--primaryColor)!important}.bg-primary{background:var(--primaryColor)!important}.link-primary{cursor:pointer;color:var(--primaryColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-primary:hover{opacity:.8}.txt-info{color:var(--infoColor)!important}.bg-info{background:var(--infoColor)!important}.link-info{cursor:pointer;color:var(--infoColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-info:hover{opacity:.8}.txt-info-alt{color:var(--infoAltColor)!important}.bg-info-alt{background:var(--infoAltColor)!important}.link-info-alt{cursor:pointer;color:var(--infoAltColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-info-alt:hover{opacity:.8}.txt-success{color:var(--successColor)!important}.bg-success{background:var(--successColor)!important}.link-success{cursor:pointer;color:var(--successColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-success:hover{opacity:.8}.txt-success-alt{color:var(--successAltColor)!important}.bg-success-alt{background:var(--successAltColor)!important}.link-success-alt{cursor:pointer;color:var(--successAltColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-success-alt:hover{opacity:.8}.txt-danger{color:var(--dangerColor)!important}.bg-danger{background:var(--dangerColor)!important}.link-danger{cursor:pointer;color:var(--dangerColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-danger:hover{opacity:.8}.txt-danger-alt{color:var(--dangerAltColor)!important}.bg-danger-alt{background:var(--dangerAltColor)!important}.link-danger-alt{cursor:pointer;color:var(--dangerAltColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-danger-alt:hover{opacity:.8}.txt-warning{color:var(--warningColor)!important}.bg-warning{background:var(--warningColor)!important}.link-warning{cursor:pointer;color:var(--warningColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-warning:hover{opacity:.8}.txt-warning-alt{color:var(--warningAltColor)!important}.bg-warning-alt{background:var(--warningAltColor)!important}.link-warning-alt{cursor:pointer;color:var(--warningAltColor)!important;text-decoration:none;user-select:none;transition:opacity var(--baseAnimationSpeed)}.link-warning-alt:hover{opacity:.8}.fade{opacity:.6}a.fade,.btn.fade,[tabindex].fade,[class*=link-].fade,.handle.fade{transition:all var(--baseAnimationSpeed)}a.fade:hover,.btn.fade:hover,[tabindex].fade:hover,[class*=link-].fade:hover,.handle.fade:hover{opacity:1}.noborder{border:0px!important}.hidden{display:none!important}.hidden-empty:empty{display:none!important}.content>:first-child,.form-field .help-block>:first-child,.overlay-panel .panel-content>:first-child,.panel>:first-child{margin-top:0}.content>:last-child,.form-field .help-block>:last-child,.overlay-panel .panel-content>:last-child,.panel>:last-child{margin-bottom:0}.panel{background:var(--baseColor);border-radius:var(--lgRadius);padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing);box-shadow:0 2px 5px 0 var(--shadowColor)}.clearfix{clear:both}.clearfix:after{content:"";display:table;clear:both}.flex{position:relative;display:flex;align-items:center;width:100%;gap:var(--smSpacing)}.flex-fill{flex:1 1 auto!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.inline-flex{position:relative;display:inline-flex;align-items:center;flex-wrap:wrap;min-width:0;gap:10px}.flex-order-0{order:0}.flex-order-1{order:1}.flex-order-2{order:2}.flex-order-3{order:3}.flex-order-4{order:4}.flex-order-5{order:5}.flex-order-6{order:6}.flex-gap-base{gap:var(--baseSpacing)!important}.flex-gap-xs{gap:var(--xsSpacing)!important}.flex-gap-sm{gap:var(--smSpacing)!important}.flex-gap-lg{gap:var(--lgSpacing)!important}.flex-gap-xl{gap:var(--xlSpacing)!important}.flex-gap-0{gap:0px!important}.flex-gap-5{gap:5px!important}.flex-gap-10{gap:10px!important}.flex-gap-15{gap:15px!important}.flex-gap-20{gap:20px!important}.flex-gap-25{gap:25px!important}.flex-gap-30{gap:30px!important}.flex-gap-35{gap:35px!important}.flex-gap-40{gap:40px!important}.flex-gap-45{gap:45px!important}.flex-gap-50{gap:50px!important}.flex-gap-55{gap:55px!important}.flex-gap-60{gap:60px!important}.m-base{margin:var(--baseSpacing)!important}.p-base{padding:var(--baseSpacing)!important}.m-xs{margin:var(--xsSpacing)!important}.p-xs{padding:var(--xsSpacing)!important}.m-sm{margin:var(--smSpacing)!important}.p-sm{padding:var(--smSpacing)!important}.m-lg{margin:var(--lgSpacing)!important}.p-lg{padding:var(--lgSpacing)!important}.m-xl{margin:var(--xlSpacing)!important}.p-xl{padding:var(--xlSpacing)!important}.m-t-auto{margin-top:auto!important}.p-t-auto{padding-top:auto!important}.m-t-base{margin-top:var(--baseSpacing)!important}.p-t-base{padding-top:var(--baseSpacing)!important}.m-t-xs{margin-top:var(--xsSpacing)!important}.p-t-xs{padding-top:var(--xsSpacing)!important}.m-t-sm{margin-top:var(--smSpacing)!important}.p-t-sm{padding-top:var(--smSpacing)!important}.m-t-lg{margin-top:var(--lgSpacing)!important}.p-t-lg{padding-top:var(--lgSpacing)!important}.m-t-xl{margin-top:var(--xlSpacing)!important}.p-t-xl{padding-top:var(--xlSpacing)!important}.m-r-auto{margin-right:auto!important}.p-r-auto{padding-right:auto!important}.m-r-base{margin-right:var(--baseSpacing)!important}.p-r-base{padding-right:var(--baseSpacing)!important}.m-r-xs{margin-right:var(--xsSpacing)!important}.p-r-xs{padding-right:var(--xsSpacing)!important}.m-r-sm{margin-right:var(--smSpacing)!important}.p-r-sm{padding-right:var(--smSpacing)!important}.m-r-lg{margin-right:var(--lgSpacing)!important}.p-r-lg{padding-right:var(--lgSpacing)!important}.m-r-xl{margin-right:var(--xlSpacing)!important}.p-r-xl{padding-right:var(--xlSpacing)!important}.m-b-auto{margin-bottom:auto!important}.p-b-auto{padding-bottom:auto!important}.m-b-base{margin-bottom:var(--baseSpacing)!important}.p-b-base{padding-bottom:var(--baseSpacing)!important}.m-b-xs{margin-bottom:var(--xsSpacing)!important}.p-b-xs{padding-bottom:var(--xsSpacing)!important}.m-b-sm{margin-bottom:var(--smSpacing)!important}.p-b-sm{padding-bottom:var(--smSpacing)!important}.m-b-lg{margin-bottom:var(--lgSpacing)!important}.p-b-lg{padding-bottom:var(--lgSpacing)!important}.m-b-xl{margin-bottom:var(--xlSpacing)!important}.p-b-xl{padding-bottom:var(--xlSpacing)!important}.m-l-auto{margin-left:auto!important}.p-l-auto{padding-left:auto!important}.m-l-base{margin-left:var(--baseSpacing)!important}.p-l-base{padding-left:var(--baseSpacing)!important}.m-l-xs{margin-left:var(--xsSpacing)!important}.p-l-xs{padding-left:var(--xsSpacing)!important}.m-l-sm{margin-left:var(--smSpacing)!important}.p-l-sm{padding-left:var(--smSpacing)!important}.m-l-lg{margin-left:var(--lgSpacing)!important}.p-l-lg{padding-left:var(--lgSpacing)!important}.m-l-xl{margin-left:var(--xlSpacing)!important}.p-l-xl{padding-left:var(--xlSpacing)!important}.m-0{margin:0!important}.p-0{padding:0!important}.m-t-0{margin-top:0!important}.p-t-0{padding-top:0!important}.m-r-0{margin-right:0!important}.p-r-0{padding-right:0!important}.m-b-0{margin-bottom:0!important}.p-b-0{padding-bottom:0!important}.m-l-0{margin-left:0!important}.p-l-0{padding-left:0!important}.m-5{margin:5px!important}.p-5{padding:5px!important}.m-t-5{margin-top:5px!important}.p-t-5{padding-top:5px!important}.m-r-5{margin-right:5px!important}.p-r-5{padding-right:5px!important}.m-b-5{margin-bottom:5px!important}.p-b-5{padding-bottom:5px!important}.m-l-5{margin-left:5px!important}.p-l-5{padding-left:5px!important}.m-10{margin:10px!important}.p-10{padding:10px!important}.m-t-10{margin-top:10px!important}.p-t-10{padding-top:10px!important}.m-r-10{margin-right:10px!important}.p-r-10{padding-right:10px!important}.m-b-10{margin-bottom:10px!important}.p-b-10{padding-bottom:10px!important}.m-l-10{margin-left:10px!important}.p-l-10{padding-left:10px!important}.m-15{margin:15px!important}.p-15{padding:15px!important}.m-t-15{margin-top:15px!important}.p-t-15{padding-top:15px!important}.m-r-15{margin-right:15px!important}.p-r-15{padding-right:15px!important}.m-b-15{margin-bottom:15px!important}.p-b-15{padding-bottom:15px!important}.m-l-15{margin-left:15px!important}.p-l-15{padding-left:15px!important}.m-20{margin:20px!important}.p-20{padding:20px!important}.m-t-20{margin-top:20px!important}.p-t-20{padding-top:20px!important}.m-r-20{margin-right:20px!important}.p-r-20{padding-right:20px!important}.m-b-20{margin-bottom:20px!important}.p-b-20{padding-bottom:20px!important}.m-l-20{margin-left:20px!important}.p-l-20{padding-left:20px!important}.m-25{margin:25px!important}.p-25{padding:25px!important}.m-t-25{margin-top:25px!important}.p-t-25{padding-top:25px!important}.m-r-25{margin-right:25px!important}.p-r-25{padding-right:25px!important}.m-b-25{margin-bottom:25px!important}.p-b-25{padding-bottom:25px!important}.m-l-25{margin-left:25px!important}.p-l-25{padding-left:25px!important}.m-30{margin:30px!important}.p-30{padding:30px!important}.m-t-30{margin-top:30px!important}.p-t-30{padding-top:30px!important}.m-r-30{margin-right:30px!important}.p-r-30{padding-right:30px!important}.m-b-30{margin-bottom:30px!important}.p-b-30{padding-bottom:30px!important}.m-l-30{margin-left:30px!important}.p-l-30{padding-left:30px!important}.m-35{margin:35px!important}.p-35{padding:35px!important}.m-t-35{margin-top:35px!important}.p-t-35{padding-top:35px!important}.m-r-35{margin-right:35px!important}.p-r-35{padding-right:35px!important}.m-b-35{margin-bottom:35px!important}.p-b-35{padding-bottom:35px!important}.m-l-35{margin-left:35px!important}.p-l-35{padding-left:35px!important}.m-40{margin:40px!important}.p-40{padding:40px!important}.m-t-40{margin-top:40px!important}.p-t-40{padding-top:40px!important}.m-r-40{margin-right:40px!important}.p-r-40{padding-right:40px!important}.m-b-40{margin-bottom:40px!important}.p-b-40{padding-bottom:40px!important}.m-l-40{margin-left:40px!important}.p-l-40{padding-left:40px!important}.m-45{margin:45px!important}.p-45{padding:45px!important}.m-t-45{margin-top:45px!important}.p-t-45{padding-top:45px!important}.m-r-45{margin-right:45px!important}.p-r-45{padding-right:45px!important}.m-b-45{margin-bottom:45px!important}.p-b-45{padding-bottom:45px!important}.m-l-45{margin-left:45px!important}.p-l-45{padding-left:45px!important}.m-50{margin:50px!important}.p-50{padding:50px!important}.m-t-50{margin-top:50px!important}.p-t-50{padding-top:50px!important}.m-r-50{margin-right:50px!important}.p-r-50{padding-right:50px!important}.m-b-50{margin-bottom:50px!important}.p-b-50{padding-bottom:50px!important}.m-l-50{margin-left:50px!important}.p-l-50{padding-left:50px!important}.m-55{margin:55px!important}.p-55{padding:55px!important}.m-t-55{margin-top:55px!important}.p-t-55{padding-top:55px!important}.m-r-55{margin-right:55px!important}.p-r-55{padding-right:55px!important}.m-b-55{margin-bottom:55px!important}.p-b-55{padding-bottom:55px!important}.m-l-55{margin-left:55px!important}.p-l-55{padding-left:55px!important}.m-60{margin:60px!important}.p-60{padding:60px!important}.m-t-60{margin-top:60px!important}.p-t-60{padding-top:60px!important}.m-r-60{margin-right:60px!important}.p-r-60{padding-right:60px!important}.m-b-60{margin-bottom:60px!important}.p-b-60{padding-bottom:60px!important}.m-l-60{margin-left:60px!important}.p-l-60{padding-left:60px!important}.wrapper{position:relative;width:var(--wrapperWidth);margin:0 auto;max-width:100%}.wrapper.wrapper-sm{width:var(--smWrapperWidth)}.wrapper.wrapper-lg{width:var(--lgWrapperWidth)}.label{display:inline-flex;align-items:center;justify-content:center;gap:5px;line-height:1;padding:3px 8px;min-height:23px;text-align:center;font-size:var(--smFontSize);border-radius:30px;background:var(--baseAlt2Color);color:var(--txtPrimaryColor);white-space:nowrap}.label.label-primary{color:var(--baseColor);background:var(--primaryColor)}.label.label-info{background:var(--infoAltColor)}.label.label-success{background:var(--successAltColor)}.label.label-danger{background:var(--dangerAltColor)}.label.label-warning{background:var(--warningAltColor)}.thumb{--thumbSize: 44px;flex-shrink:0;position:relative;display:inline-flex;align-items:center;justify-content:center;line-height:1;width:var(--thumbSize);height:var(--thumbSize);background:var(--baseAlt2Color);border-radius:var(--baseRadius);color:var(--txtPrimaryColor);font-size:1.2rem;box-shadow:0 2px 5px 0 var(--shadowColor)}.thumb i{font-size:inherit}.thumb img{width:100%;height:100%;border-radius:inherit}.thumb.thumb-sm{--thumbSize: 32px;font-size:.85rem}.thumb.thumb-lg{--thumbSize: 60px;font-size:1.3rem}.thumb.thumb-xl{--thumbSize: 80px;font-size:1.5rem}.thumb.thumb-circle{border-radius:50%}.thumb.thumb-active{box-shadow:0 0 0 2px var(--primaryColor)}.section-title{display:flex;width:100%;column-gap:10px;row-gap:5px;margin:0 0 var(--xsSpacing);font-weight:600;font-size:var(--smFontSize);line-height:var(--smLineHeight);color:var(--txtHintColor);text-transform:uppercase}.drag-handle{outline:0;cursor:pointer;display:inline-flex;align-items:left;color:var(--txtDisabledColor);transition:color var(--baseAnimationSpeed)}.drag-handle:before,.drag-handle:after{content:"\ef77";font-family:var(--iconFontFamily);font-size:18px;line-height:1;width:7px;text-align:center}.drag-handle:focus-visible,.drag-handle:hover,.drag-handle:active{color:var(--txtPrimaryColor)}.logo{position:relative;vertical-align:top;display:inline-flex;align-items:center;gap:10px;font-size:23px;text-decoration:none;color:inherit;user-select:none}.logo strong{font-weight:700}.logo .version{position:absolute;right:0;top:-5px;line-height:1;font-size:10px;font-weight:400;padding:2px 4px;border-radius:var(--baseRadius);background:var(--dangerAltColor);color:var(--txtPrimaryColor)}.logo.logo-sm{font-size:20px}.loader{--loaderSize: 32px;position:relative;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;row-gap:10px;margin:0;color:var(--txtDisabledColor);text-align:center;font-weight:400}.loader:before{content:"\eec4";display:inline-block;vertical-align:top;clear:both;width:var(--loaderSize);height:var(--loaderSize);line-height:var(--loaderSize);font-size:var(--loaderSize);font-weight:400;font-family:var(--iconFontFamily);color:inherit;text-align:center;animation:loaderShow var(--baseAnimationSpeed),rotate .9s var(--baseAnimationSpeed) infinite linear}.loader.loader-primary{color:var(--primaryColor)}.loader.loader-info{color:var(--infoColor)}.loader.loader-info-alt{color:var(--infoAltColor)}.loader.loader-success{color:var(--successColor)}.loader.loader-success-alt{color:var(--successAltColor)}.loader.loader-danger{color:var(--dangerColor)}.loader.loader-danger-alt{color:var(--dangerAltColor)}.loader.loader-warning{color:var(--warningColor)}.loader.loader-warning-alt{color:var(--warningAltColor)}.loader.loader-sm{--loaderSize: 24px}.loader.loader-lg{--loaderSize: 42px}.skeleton-loader{position:relative;height:12px;margin:5px 0;border-radius:var(--baseRadius);background:var(--baseAlt1Color);animation:fadeIn .4s}.skeleton-loader:before{content:"";width:100%;height:100%;display:block;border-radius:inherit;background:linear-gradient(90deg,var(--baseAlt1Color) 8%,var(--bodyColor) 18%,var(--baseAlt1Color) 33%);background-size:200% 100%;animation:shine 1s linear infinite}.placeholder-section{display:flex;width:100%;align-items:center;justify-content:center;text-align:center;flex-direction:column;gap:var(--smSpacing);color:var(--txtHintColor)}.placeholder-section .icon{font-size:50px;height:50px;line-height:1;opacity:.3}.placeholder-section .icon i{font-size:inherit;vertical-align:top}.grid{--gridGap: var(--baseSpacing);position:relative;display:flex;flex-grow:1;flex-wrap:wrap;row-gap:var(--gridGap);margin:0 calc(-.5 * var(--gridGap))}.grid.grid-center{align-items:center}.grid.grid-sm{--gridGap: var(--smSpacing)}.grid .form-field{margin-bottom:0}.grid>*{margin:0 calc(.5 * var(--gridGap))}.col-xxl-1,.col-xxl-2,.col-xxl-3,.col-xxl-4,.col-xxl-5,.col-xxl-6,.col-xxl-7,.col-xxl-8,.col-xxl-9,.col-xxl-10,.col-xxl-11,.col-xxl-12,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12{position:relative;width:100%;min-height:1px}.col-auto{flex:0 0 auto;width:auto}.col-12{width:calc(100% - var(--gridGap))}.col-11{width:calc(91.6666666667% - var(--gridGap))}.col-10{width:calc(83.3333333333% - var(--gridGap))}.col-9{width:calc(75% - var(--gridGap))}.col-8{width:calc(66.6666666667% - var(--gridGap))}.col-7{width:calc(58.3333333333% - var(--gridGap))}.col-6{width:calc(50% - var(--gridGap))}.col-5{width:calc(41.6666666667% - var(--gridGap))}.col-4{width:calc(33.3333333333% - var(--gridGap))}.col-3{width:calc(25% - var(--gridGap))}.col-2{width:calc(16.6666666667% - var(--gridGap))}.col-1{width:calc(8.3333333333% - var(--gridGap))}@media (min-width: 576px){.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-12{width:calc(100% - var(--gridGap))}.col-sm-11{width:calc(91.6666666667% - var(--gridGap))}.col-sm-10{width:calc(83.3333333333% - var(--gridGap))}.col-sm-9{width:calc(75% - var(--gridGap))}.col-sm-8{width:calc(66.6666666667% - var(--gridGap))}.col-sm-7{width:calc(58.3333333333% - var(--gridGap))}.col-sm-6{width:calc(50% - var(--gridGap))}.col-sm-5{width:calc(41.6666666667% - var(--gridGap))}.col-sm-4{width:calc(33.3333333333% - var(--gridGap))}.col-sm-3{width:calc(25% - var(--gridGap))}.col-sm-2{width:calc(16.6666666667% - var(--gridGap))}.col-sm-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 768px){.col-md-auto{flex:0 0 auto;width:auto}.col-md-12{width:calc(100% - var(--gridGap))}.col-md-11{width:calc(91.6666666667% - var(--gridGap))}.col-md-10{width:calc(83.3333333333% - var(--gridGap))}.col-md-9{width:calc(75% - var(--gridGap))}.col-md-8{width:calc(66.6666666667% - var(--gridGap))}.col-md-7{width:calc(58.3333333333% - var(--gridGap))}.col-md-6{width:calc(50% - var(--gridGap))}.col-md-5{width:calc(41.6666666667% - var(--gridGap))}.col-md-4{width:calc(33.3333333333% - var(--gridGap))}.col-md-3{width:calc(25% - var(--gridGap))}.col-md-2{width:calc(16.6666666667% - var(--gridGap))}.col-md-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 992px){.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-12{width:calc(100% - var(--gridGap))}.col-lg-11{width:calc(91.6666666667% - var(--gridGap))}.col-lg-10{width:calc(83.3333333333% - var(--gridGap))}.col-lg-9{width:calc(75% - var(--gridGap))}.col-lg-8{width:calc(66.6666666667% - var(--gridGap))}.col-lg-7{width:calc(58.3333333333% - var(--gridGap))}.col-lg-6{width:calc(50% - var(--gridGap))}.col-lg-5{width:calc(41.6666666667% - var(--gridGap))}.col-lg-4{width:calc(33.3333333333% - var(--gridGap))}.col-lg-3{width:calc(25% - var(--gridGap))}.col-lg-2{width:calc(16.6666666667% - var(--gridGap))}.col-lg-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 1200px){.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-12{width:calc(100% - var(--gridGap))}.col-xl-11{width:calc(91.6666666667% - var(--gridGap))}.col-xl-10{width:calc(83.3333333333% - var(--gridGap))}.col-xl-9{width:calc(75% - var(--gridGap))}.col-xl-8{width:calc(66.6666666667% - var(--gridGap))}.col-xl-7{width:calc(58.3333333333% - var(--gridGap))}.col-xl-6{width:calc(50% - var(--gridGap))}.col-xl-5{width:calc(41.6666666667% - var(--gridGap))}.col-xl-4{width:calc(33.3333333333% - var(--gridGap))}.col-xl-3{width:calc(25% - var(--gridGap))}.col-xl-2{width:calc(16.6666666667% - var(--gridGap))}.col-xl-1{width:calc(8.3333333333% - var(--gridGap))}}@media (min-width: 1400px){.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-12{width:calc(100% - var(--gridGap))}.col-xxl-11{width:calc(91.6666666667% - var(--gridGap))}.col-xxl-10{width:calc(83.3333333333% - var(--gridGap))}.col-xxl-9{width:calc(75% - var(--gridGap))}.col-xxl-8{width:calc(66.6666666667% - var(--gridGap))}.col-xxl-7{width:calc(58.3333333333% - var(--gridGap))}.col-xxl-6{width:calc(50% - var(--gridGap))}.col-xxl-5{width:calc(41.6666666667% - var(--gridGap))}.col-xxl-4{width:calc(33.3333333333% - var(--gridGap))}.col-xxl-3{width:calc(25% - var(--gridGap))}.col-xxl-2{width:calc(16.6666666667% - var(--gridGap))}.col-xxl-1{width:calc(8.3333333333% - var(--gridGap))}}@keyframes tooltipHide{to{opacity:0;visibility:hidden;transform:scale(.9)}}@keyframes tooltipShow{0%{opacity:0;visibility:hidden;transform:scale(.9)}to{opacity:1;visibility:visible;transform:scale(1)}}.app-tooltip{position:fixed;z-index:999999;top:0;left:0;display:inline-block;vertical-align:top;max-width:275px;padding:3px 5px;color:#fff;text-align:center;font-family:var(--baseFontFamily);font-size:var(--smFontSize);line-height:var(--smLineHeight);border-radius:var(--baseRadius);background:var(--tooltipColor);pointer-events:none;user-select:none;transition:opacity var(--baseAnimationSpeed),visibility var(--baseAnimationSpeed),transform var(--baseAnimationSpeed);transform:scale(.9);white-space:pre-line;opacity:0;visibility:hidden}.app-tooltip.active{transform:scale(1);opacity:1;visibility:visible}.app-tooltip.code{font-family:monospace;white-space:pre-wrap;text-align:left;min-width:150px;max-width:340px}.dropdown{position:absolute;z-index:99;right:0;left:auto;top:100%;cursor:default;display:inline-block;vertical-align:top;padding:5px;margin:10px 0 0;width:auto;min-width:140px;max-width:350px;max-height:330px;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);border-radius:var(--baseRadius);border:1px solid var(--baseAlt2Color);box-shadow:0 2px 5px 0 var(--shadowColor)}.dropdown hr{margin:5px 0}.dropdown .dropdown-item{border:0;background:none;position:relative;outline:0;display:flex;align-items:center;column-gap:8px;width:100%;height:auto;min-height:0;text-align:left;padding:8px 10px;margin:0 0 5px;cursor:pointer;color:var(--txtPrimaryColor);font-weight:400;font-size:var(--baseFontSize);font-family:var(--baseFontFamily);line-height:var(--baseLineHeight);border-radius:var(--baseRadius);text-decoration:none;word-break:break-word;user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.dropdown .dropdown-item:last-child{margin-bottom:0}.dropdown .dropdown-item:focus,.dropdown .dropdown-item:hover{background:var(--baseAlt1Color)}.dropdown .dropdown-item.selected{background:var(--baseAlt2Color)}.dropdown .dropdown-item:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}.dropdown .dropdown-item.disabled{color:var(--txtDisabledColor);background:none;pointer-events:none}.dropdown .dropdown-item.separator{cursor:default;background:none;text-transform:uppercase;padding-top:0;padding-bottom:0;margin-top:15px;color:var(--txtDisabledColor);font-weight:600;font-size:var(--smFontSize)}.dropdown.dropdown-upside{top:auto;bottom:100%;margin:0 0 10px}.dropdown.dropdown-left{right:auto;left:0}.dropdown.dropdown-center{right:auto;left:50%;transform:translate(-50%)}.dropdown.dropdown-sm{margin-top:5px;min-width:100px}.dropdown.dropdown-sm .dropdown-item{column-gap:7px;font-size:var(--smFontSize);margin:0 0 2px;padding:5px 7px}.dropdown.dropdown-sm .dropdown-item:last-child{margin-bottom:0}.dropdown.dropdown-sm.dropdown-upside{margin-top:0;margin-bottom:5px}.dropdown.dropdown-block{width:100%;min-width:130px;max-width:100%}.dropdown.dropdown-nowrap{white-space:nowrap}.overlay-panel{position:relative;z-index:1;display:flex;flex-direction:column;align-self:flex-end;margin-left:auto;background:var(--baseColor);height:100%;width:580px;max-width:100%;word-wrap:break-word;box-shadow:0 2px 5px 0 var(--shadowColor)}.overlay-panel .overlay-panel-section{position:relative;width:100%;margin:0;padding:var(--baseSpacing);transition:box-shadow var(--baseAnimationSpeed)}.overlay-panel .overlay-panel-section:empty{display:none}.overlay-panel .overlay-panel-section:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.overlay-panel .overlay-panel-section:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.overlay-panel .overlay-panel-section .btn{flex-grow:0}.overlay-panel img{max-width:100%}.overlay-panel hr{background:var(--baseAlt2Color)}.overlay-panel .panel-header{position:relative;z-index:2;display:flex;flex-wrap:wrap;align-items:center;column-gap:10px;row-gap:var(--baseSpacing);padding:calc(var(--baseSpacing) - 7px) var(--baseSpacing)}.overlay-panel .panel-header>*{margin-top:0;margin-bottom:0}.overlay-panel .panel-header .btn-back{margin-left:-10px}.overlay-panel .panel-header .overlay-close{z-index:3;outline:0;position:absolute;right:100%;top:20px;margin:0;display:inline-flex;align-items:center;justify-content:center;width:35px;height:35px;cursor:pointer;text-align:center;font-size:1.6rem;line-height:1;border-radius:35px 0 0 35px;color:#fff;background:var(--primaryColor);opacity:.5;transition:opacity var(--baseAnimationSpeed);user-select:none}.overlay-panel .panel-header .overlay-close i{font-size:inherit}.overlay-panel .panel-header .overlay-close:hover,.overlay-panel .panel-header .overlay-close:focus-visible,.overlay-panel .panel-header .overlay-close:active{opacity:.7}.overlay-panel .panel-header .overlay-close:active{transition-duration:var(--activeAnimationSpeed);opacity:1}.overlay-panel .panel-header .btn-close{margin-right:-10px}.overlay-panel .panel-header .tabs-header{margin-bottom:-23px}.overlay-panel .panel-content{z-index:auto;flex-grow:1;overflow-x:hidden;overflow-y:auto;overflow-y:overlay}.overlay-panel .panel-header~.panel-content{padding-top:5px}.overlay-panel .panel-footer{z-index:2;column-gap:var(--smSpacing);display:flex;align-items:center;justify-content:flex-end;border-top:1px solid var(--baseAlt2Color);padding:calc(var(--baseSpacing) - 7px) var(--baseSpacing)}.overlay-panel.scrollable .panel-header{box-shadow:0 4px 5px #0000000d}.overlay-panel.scrollable .panel-footer{box-shadow:0 -4px 5px #0000000d}.overlay-panel.scrollable.scroll-top-reached .panel-header,.overlay-panel.scrollable.scroll-bottom-reached .panel-footer{box-shadow:none}.overlay-panel.overlay-panel-xl{width:850px}.overlay-panel.overlay-panel-lg{width:650px}.overlay-panel.overlay-panel-sm{width:460px}.overlay-panel.popup{height:auto;max-height:100%;align-self:center;border-radius:var(--baseRadius);margin:0 auto}.overlay-panel.popup .panel-footer{background:var(--bodyColor)}.overlay-panel.hide-content .panel-content{display:none}.overlay-panel.colored-header .panel-header{background:var(--bodyColor);border-bottom:1px solid var(--baseAlt1Color)}.overlay-panel.colored-header .panel-header .tabs-header{border-bottom:0}.overlay-panel.colored-header .panel-header~.panel-content{padding-top:calc(var(--baseSpacing) - 5px)}.overlay-panel.image-preview{width:auto;min-width:300px;max-width:70%}.overlay-panel.image-preview .panel-header{position:absolute;z-index:99}.overlay-panel.image-preview .panel-header,.overlay-panel.image-preview .panel-footer{padding:10px 15px}.overlay-panel.image-preview .panel-content{padding:0;text-align:center}.overlay-panel.image-preview img{max-width:100%;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}@media (max-width: 900px){.overlay-panel .overlay-panel-section{padding:var(--smSpacing)}}.overlay-panel-container{display:flex;position:fixed;z-index:1000;flex-direction:row;align-items:center;top:0;left:0;width:100%;height:100%;overflow:hidden;margin:0;padding:0;outline:0}.overlay-panel-container .overlay{position:absolute;z-index:0;left:0;top:0;width:100%;height:100%;user-select:none;background:var(--overlayColor)}.overlay-panel-container.padded{padding:10px}.overlay-panel-wrapper{position:relative;z-index:1000;outline:0}.alert{position:relative;display:flex;column-gap:15px;align-items:center;width:100%;min-height:50px;max-width:100%;word-break:break-word;margin:0 0 var(--baseSpacing);border-radius:var(--baseRadius);padding:12px 15px;background:var(--baseAlt1Color);color:var(--txtAltColor)}.alert .content,.alert .form-field .help-block,.form-field .alert .help-block,.alert .panel,.alert .overlay-panel .panel-content,.overlay-panel .alert .panel-content{flex-grow:1}.alert .icon,.alert .close{display:inline-flex;align-items:center;justify-content:center;flex-grow:0;flex-shrink:0;text-align:center}.alert .icon{align-self:stretch;font-size:1.2em;padding-right:15px;font-weight:400;border-right:1px solid rgba(0,0,0,.05);color:var(--txtHintColor)}.alert .close{display:inline-flex;margin-right:-5px;width:30px;height:30px;outline:0;cursor:pointer;text-align:center;font-size:var(--smFontSize);line-height:30px;border-radius:30px;text-decoration:none;color:inherit;opacity:.5;transition:opacity var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.alert .close:hover,.alert .close:focus{opacity:1;background:rgba(255,255,255,.2)}.alert .close:active{opacity:1;background:rgba(255,255,255,.3);transition-duration:var(--activeAnimationSpeed)}.alert code,.alert hr{background:rgba(0,0,0,.1)}.alert.alert-info{background:var(--infoAltColor)}.alert.alert-info .icon{color:var(--infoColor)}.alert.alert-warning{background:var(--warningAltColor)}.alert.alert-warning .icon{color:var(--warningColor)}.alert.alert-success{background:var(--successAltColor)}.alert.alert-success .icon{color:var(--successColor)}.alert.alert-danger{background:var(--dangerAltColor)}.alert.alert-danger .icon{color:var(--dangerColor)}.toasts-wrapper{position:fixed;z-index:999999;bottom:0;left:0;padding:0 var(--smSpacing);width:100%;display:block;text-align:center;pointer-events:none}.toasts-wrapper .alert{text-align:left;pointer-events:auto;width:var(--smWrapperWidth);margin:var(--baseSpacing) auto;box-shadow:0 2px 5px 0 var(--shadowColor)}button{outline:0;border:0;background:none;padding:0;text-align:left;font-family:inherit;font-size:inherit;font-weight:inherit;line-height:inherit}.btn{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;outline:0;border:0;margin:0;flex-shrink:0;cursor:pointer;padding:5px 20px;column-gap:7px;user-select:none;min-width:var(--btnHeight);min-height:var(--btnHeight);text-align:center;text-decoration:none;line-height:1;font-weight:600;color:#fff;font-size:var(--baseFontSize);font-family:var(--baseFontFamily);border-radius:var(--btnRadius);background:none;transition:color var(--baseAnimationSpeed)}.btn i{font-size:1.1428em;vertical-align:middle;display:inline-block}.btn:before{content:"";border-radius:inherit;position:absolute;left:0;top:0;z-index:-1;width:100%;height:100%;pointer-events:none;user-select:none;background:var(--primaryColor);transition:filter var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed),transform var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.btn:hover:before,.btn:focus-visible:before{opacity:.9}.btn.active,.btn:active{z-index:999}.btn.active:before,.btn:active:before{opacity:.8;transition-duration:var(--activeAnimationSpeed)}.btn.btn-info:before{background:var(--infoColor)}.btn.btn-info:hover:before,.btn.btn-info:focus-visible:before{opacity:.8}.btn.btn-info:active:before{opacity:.7}.btn.btn-success:before{background:var(--successColor)}.btn.btn-success:hover:before,.btn.btn-success:focus-visible:before{opacity:.8}.btn.btn-success:active:before{opacity:.7}.btn.btn-danger:before{background:var(--dangerColor)}.btn.btn-danger:hover:before,.btn.btn-danger:focus-visible:before{opacity:.8}.btn.btn-danger:active:before{opacity:.7}.btn.btn-warning:before{background:var(--warningColor)}.btn.btn-warning:hover:before,.btn.btn-warning:focus-visible:before{opacity:.8}.btn.btn-warning:active:before{opacity:.7}.btn.btn-hint:before{background:var(--baseAlt4Color)}.btn.btn-hint:hover:before,.btn.btn-hint:focus-visible:before{opacity:.8}.btn.btn-hint:active:before{opacity:.7}.btn.btn-outline{border:2px solid currentColor;background:#fff}.btn.btn-secondary,.btn.btn-outline{box-shadow:none;color:var(--txtPrimaryColor)}.btn.btn-secondary:before,.btn.btn-outline:before{opacity:0;background:var(--baseAlt4Color)}.btn.btn-secondary:focus-visible:before,.btn.btn-secondary:hover:before,.btn.btn-secondary:active:before,.btn.btn-secondary.active:before,.btn.btn-outline:focus-visible:before,.btn.btn-outline:hover:before,.btn.btn-outline:active:before,.btn.btn-outline.active:before{opacity:.11}.btn.btn-secondary.active:before,.btn.btn-secondary:active:before,.btn.btn-outline.active:before,.btn.btn-outline:active:before{opacity:.22}.btn.btn-secondary.btn-info,.btn.btn-outline.btn-info{color:var(--infoColor)}.btn.btn-secondary.btn-info:before,.btn.btn-outline.btn-info:before{background:var(--infoColor)}.btn.btn-secondary.btn-success,.btn.btn-outline.btn-success{color:var(--successColor)}.btn.btn-secondary.btn-success:before,.btn.btn-outline.btn-success:before{background:var(--successColor)}.btn.btn-secondary.btn-danger,.btn.btn-outline.btn-danger{color:var(--dangerColor)}.btn.btn-secondary.btn-danger:before,.btn.btn-outline.btn-danger:before{background:var(--dangerColor)}.btn.btn-secondary.btn-warning,.btn.btn-outline.btn-warning{color:var(--warningColor)}.btn.btn-secondary.btn-warning:before,.btn.btn-outline.btn-warning:before{background:var(--warningColor)}.btn.btn-secondary.btn-hint,.btn.btn-outline.btn-hint{color:var(--baseAlt4Color)}.btn.btn-secondary.btn-hint:before,.btn.btn-outline.btn-hint:before{background:var(--baseAlt4Color)}.btn.btn-secondary.btn-hint,.btn.btn-outline.btn-hint{color:var(--txtHintColor)}.btn.btn-disabled,.btn[disabled]{box-shadow:none;cursor:default;background:var(--baseAlt2Color);color:var(--txtDisabledColor)!important}.btn.btn-disabled:before,.btn[disabled]:before{display:none}.btn.btn-disabled.btn-secondary,.btn[disabled].btn-secondary{background:none}.btn.btn-disabled.btn-outline,.btn[disabled].btn-outline{border-color:var(--baseAlt2Color)}.btn.btn-expanded{min-width:140px}.btn.btn-expanded-sm{min-width:90px}.btn.btn-expanded-lg{min-width:170px}.btn.btn-lg{column-gap:10px;font-size:var(--lgFontSize);min-height:var(--lgBtnHeight);min-width:var(--lgBtnHeight);padding-left:30px;padding-right:30px}.btn.btn-lg i{font-size:1.2666em}.btn.btn-lg.btn-expanded{min-width:240px}.btn.btn-lg.btn-expanded-sm{min-width:160px}.btn.btn-lg.btn-expanded-lg{min-width:300px}.btn.btn-sm,.btn.btn-xs{column-gap:5px;font-size:var(--smFontSize);min-height:var(--smBtnHeight);min-width:var(--smBtnHeight);padding-left:12px;padding-right:12px}.btn.btn-sm i,.btn.btn-xs i{font-size:1rem}.btn.btn-sm.btn-expanded,.btn.btn-xs.btn-expanded{min-width:100px}.btn.btn-sm.btn-expanded-sm,.btn.btn-xs.btn-expanded-sm{min-width:80px}.btn.btn-sm.btn-expanded-lg,.btn.btn-xs.btn-expanded-lg{min-width:130px}.btn.btn-xs{min-width:var(--xsBtnHeight);min-height:var(--xsBtnHeight)}.btn.btn-block{display:flex;width:100%}.btn.btn-circle{border-radius:50%;padding:0}.btn.btn-circle i{font-size:1.2857rem}.btn.btn-circle.btn-sm i,.btn.btn-circle.btn-xs i{font-size:1.1rem}.btn.btn-loading{--loaderSize: 24px;cursor:default;pointer-events:none}.btn.btn-loading:after{content:"\eec4";position:absolute;display:inline-block;vertical-align:top;left:50%;top:50%;width:var(--loaderSize);height:var(--loaderSize);line-height:var(--loaderSize);font-size:var(--loaderSize);color:inherit;text-align:center;font-weight:400;margin-left:calc(var(--loaderSize) * -.5);margin-top:calc(var(--loaderSize) * -.5);font-family:var(--iconFontFamily);animation:loaderShow var(--baseAnimationSpeed),rotate .9s var(--baseAnimationSpeed) infinite linear}.btn.btn-loading>*{opacity:0;transform:scale(.9)}.btn.btn-loading.btn-sm,.btn.btn-loading.btn-xs{--loaderSize: 20px}.btn.btn-loading.btn-lg{--loaderSize: 28px}.btn.btn-prev i,.btn.btn-next i{transition:transform var(--baseAnimationSpeed)}.btn.btn-prev:hover i,.btn.btn-prev:focus-within i,.btn.btn-next:hover i,.btn.btn-next:focus-within i{transform:translate(3px)}.btn.btn-prev:hover i,.btn.btn-prev:focus-within i{transform:translate(-3px)}.btns-group{display:inline-flex;align-items:center;gap:var(--xsSpacing)}.code-editor,.select .selected-container,input,select,textarea{display:block;width:100%;outline:0;border:0;margin:0;background:none;padding:5px 10px;line-height:20px;min-width:0;min-height:var(--inputHeight);background:var(--baseAlt1Color);color:var(--txtPrimaryColor);font-size:var(--baseFontSize);font-family:var(--baseFontFamily);font-weight:400;border-radius:var(--baseRadius)}.code-editor::placeholder,.select .selected-container::placeholder,input::placeholder,select::placeholder,textarea::placeholder{color:var(--txtDisabledColor)}.code-editor:focus,.select .selected-container:focus,input:focus,select:focus,textarea:focus,.active.code-editor,.select .active.selected-container,input.active,select.active,textarea.active{border-color:var(--primaryColor)}[readonly].code-editor,.select [readonly].selected-container,input[readonly],select[readonly],textarea[readonly],.readonly.code-editor,.select .readonly.selected-container,input.readonly,select.readonly,textarea.readonly{cursor:default;color:var(--txtHintColor)}[disabled].code-editor,.select [disabled].selected-container,input[disabled],select[disabled],textarea[disabled],.disabled.code-editor,.select .disabled.selected-container,input.disabled,select.disabled,textarea.disabled{cursor:default;color:var(--txtDisabledColor);border-color:var(--baseAlt2Color)}.txt-mono.code-editor,.select .txt-mono.selected-container,input.txt-mono,select.txt-mono,textarea.txt-mono{font-size:var(--smFontSize)}input:-webkit-autofill{-webkit-text-fill-color:var(--txtPrimaryColor);-webkit-box-shadow:inset 0 0 0 50px var(--baseAlt1Color)}.form-field:focus-within input:-webkit-autofill,input:-webkit-autofill:focus{-webkit-box-shadow:inset 0 0 0 50px var(--baseAlt2Color)}input[type=file]{padding:9px}input[type=checkbox],input[type=radio]{width:auto;height:auto;display:inline}input[type=number]{-moz-appearance:textfield;appearance:textfield}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none}textarea{min-height:80px;resize:vertical}select{padding-left:8px}.form-field{--hPadding: 15px;position:relative;display:block;width:100%;margin-bottom:var(--baseSpacing)}.form-field .code-editor,.form-field .select .selected-container,.select .form-field .selected-container,.form-field input,.form-field select,.form-field textarea{z-index:0;padding-left:var(--hPadding);padding-right:var(--hPadding)}.form-field select{padding-left:8px}.form-field label{display:flex;width:100%;column-gap:10px;align-items:center;user-select:none;font-weight:600;color:var(--txtHintColor);font-size:var(--xsFontSize);text-transform:uppercase;line-height:1;padding-top:12px;padding-bottom:2px;padding-left:var(--hPadding);padding-right:var(--hPadding);border:0;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.form-field label~.code-editor,.form-field .select label~.selected-container,.select .form-field label~.selected-container,.form-field label~input,.form-field label~select,.form-field label~textarea{border-top:0;padding-top:2px;padding-bottom:8px;border-top-left-radius:0;border-top-right-radius:0}.form-field label i{font-size:.96rem;line-height:1;margin-top:-2px;margin-bottom:-2px}.form-field label i+.txt{margin-left:-5px}.form-field .code-editor,.form-field .select .selected-container,.select .form-field .selected-container,.form-field input,.form-field select,.form-field textarea,.form-field label{background:var(--baseAlt1Color);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.form-field:focus-within .code-editor,.form-field:focus-within .select .selected-container,.select .form-field:focus-within .selected-container,.form-field:focus-within input,.form-field:focus-within select,.form-field:focus-within textarea,.form-field:focus-within label{background:var(--baseAlt2Color)}.form-field:focus-within label{color:var(--txtPrimaryColor)}.form-field .form-field-addon{position:absolute;display:inline-flex;align-items:center;z-index:1;top:0px;right:var(--hPadding);min-height:var(--inputHeight);color:var(--txtHintColor)}.form-field .form-field-addon .btn{margin-right:-5px}.form-field .form-field-addon~.code-editor,.form-field .select .form-field-addon~.selected-container,.select .form-field .form-field-addon~.selected-container,.form-field .form-field-addon~input,.form-field .form-field-addon~select,.form-field .form-field-addon~textarea{padding-right:35px}.form-field label~.form-field-addon{min-height:calc(var(--inputHeight) + var(--baseLineHeight))}.form-field .help-block{margin-top:8px;font-size:var(--smFontSize);line-height:var(--smLineHeight);color:var(--txtHintColor)}.form-field.error .help-block-error{color:var(--dangerColor)}.form-field.error>label{color:var(--dangerColor)}.form-field.required:not(.form-field-toggle)>label:after{content:"*";color:var(--dangerColor);margin-left:-7px}.form-field.disabled>label{color:var(--txtDisabledColor)}.form-field.disabled label,.form-field.disabled .code-editor,.form-field.disabled .select .selected-container,.select .form-field.disabled .selected-container,.form-field.disabled input,.form-field.disabled select,.form-field.disabled textarea{border-color:var(--baseAlt2Color)}.form-field.disabled.required>label:after{opacity:.5}.form-field input[type=radio],.form-field input[type=checkbox]{position:absolute;z-index:-1;left:0;width:0;height:0;min-height:0;min-width:0;border:0;background:none;user-select:none;pointer-events:none;box-shadow:none;opacity:0}.form-field input[type=radio]~label,.form-field input[type=checkbox]~label{border:0;margin:0;outline:0;background:none;display:inline-flex;vertical-align:top;align-items:center;width:auto;column-gap:5px;user-select:none;padding:0 0 0 27px;line-height:20px;min-height:20px;font-weight:400;font-size:var(--baseFontSize);text-transform:none;color:var(--txtPrimaryColor)}.form-field input[type=radio]~label:before,.form-field input[type=checkbox]~label:before{content:"";display:inline-block;vertical-align:top;position:absolute;z-index:0;left:0;top:0;width:20px;height:20px;line-height:16px;font-family:var(--iconFontFamily);font-size:1.2rem;text-align:center;color:var(--baseColor);cursor:pointer;background:var(--baseColor);border-radius:var(--baseRadius);border:2px solid var(--baseAlt3Color);transition:transform var(--baseAnimationSpeed),border-color var(--baseAnimationSpeed),color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.form-field input[type=radio]~label:active:before,.form-field input[type=checkbox]~label:active:before{transform:scale(.9)}.form-field input[type=radio]:focus~label:before,.form-field input[type=radio]~label:hover:before,.form-field input[type=checkbox]:focus~label:before,.form-field input[type=checkbox]~label:hover:before{border-color:var(--baseAlt4Color)}.form-field input[type=radio]:checked~label:before,.form-field input[type=checkbox]:checked~label:before{content:"\eb7a";box-shadow:none;mix-blend-mode:unset;background:var(--successColor);border-color:var(--successColor)}.form-field input[type=radio]:disabled~label,.form-field input[type=checkbox]:disabled~label{pointer-events:none;cursor:not-allowed;color:var(--txtDisabledColor)}.form-field input[type=radio]:disabled~label:before,.form-field input[type=checkbox]:disabled~label:before{opacity:.5}.form-field input[type=radio]~label:before{border-radius:50%;font-size:.5rem}.form-field input[type=radio]:checked~label:before{content:"\eb7c";color:#fff}.form-field.form-field-toggle input[type=radio]~label,.form-field.form-field-toggle input[type=checkbox]~label{min-height:24px;padding-left:47px}.form-field.form-field-toggle input[type=radio]~label:empty,.form-field.form-field-toggle input[type=checkbox]~label:empty{padding-left:40px}.form-field.form-field-toggle input[type=radio]~label:before,.form-field.form-field-toggle input[type=checkbox]~label:before{content:"";width:40px;height:24px;border-radius:24px;border:0;box-shadow:none;background:var(--baseAlt3Color);transition:background var(--activeAnimationSpeed)}.form-field.form-field-toggle input[type=radio]~label:after,.form-field.form-field-toggle input[type=checkbox]~label:after{content:"";position:absolute;z-index:1;top:4px;left:4px;width:16px;height:16px;cursor:pointer;background:var(--baseColor);border-radius:16px;transition:left var(--activeAnimationSpeed),transform var(--activeAnimationSpeed),background var(--activeAnimationSpeed);box-shadow:0 2px 5px 0 var(--shadowColor)}.form-field.form-field-toggle input[type=radio]~label:active:before,.form-field.form-field-toggle input[type=checkbox]~label:active:before{transform:none}.form-field.form-field-toggle input[type=radio]~label:active:after,.form-field.form-field-toggle input[type=checkbox]~label:active:after{transform:scale(.9)}.form-field.form-field-toggle input[type=radio]:focus-visible~label:before,.form-field.form-field-toggle input[type=checkbox]:focus-visible~label:before{box-shadow:0 0 0 2px var(--baseAlt2Color)}.form-field.form-field-toggle input[type=radio]~label:hover:before,.form-field.form-field-toggle input[type=checkbox]~label:hover:before{background:var(--baseAlt4Color)}.form-field.form-field-toggle input[type=radio]:checked~label:before,.form-field.form-field-toggle input[type=checkbox]:checked~label:before{background:var(--successColor)}.form-field.form-field-toggle input[type=radio]:checked~label:after,.form-field.form-field-toggle input[type=checkbox]:checked~label:after{left:20px;background:var(--baseColor)}.select{position:relative;display:block;outline:0}.select .option{user-select:none;column-gap:8px}.select .option .icon{min-width:20px;text-align:center;line-height:inherit}.select .option .icon i{vertical-align:middle;line-height:inherit}.select .txt-placeholder{color:var(--txtHintColor)}label~.select .selected-container{border-top:0}.select .selected-container{position:relative;display:flex;flex-wrap:wrap;width:100%;align-items:center;padding-top:0;padding-bottom:0;padding-right:35px!important;user-select:none}.select .selected-container:after{content:"\ea4d";position:absolute;right:5px;top:50%;width:20px;height:20px;line-height:20px;text-align:center;margin-top:-10px;display:inline-block;vertical-align:top;font-size:1rem;font-family:var(--iconFontFamily);align-self:flex-end;color:var(--txtHintColor);transition:color var(--baseAnimationSpeed),transform var(--baseAnimationSpeed)}.select .selected-container:active,.select .selected-container.active{border-bottom-left-radius:0;border-bottom-right-radius:0}.select .selected-container:active:after,.select .selected-container.active:after{color:var(--txtPrimaryColor);transform:rotate(180deg)}.select .selected-container .option{display:flex;width:100%;align-items:center;max-width:100%;user-select:text}.select .selected-container .clear{margin-left:auto;cursor:pointer;color:var(--txtHintColor);transition:color var(--baseAnimationSpeed)}.select .selected-container .clear i{display:inline-block;vertical-align:middle;line-height:1}.select .selected-container .clear:hover{color:var(--txtPrimaryColor)}.select.multiple .selected-container{display:flex;align-items:center;padding-left:2px;row-gap:3px;column-gap:4px}.select.multiple .selected-container .txt-placeholder{margin-left:5px}.select.multiple .selected-container .option{display:inline-flex;width:auto;padding:3px 5px;line-height:1;border-radius:var(--baseRadius);background:var(--baseColor)}.select:not(.multiple) .selected-container .label{margin-left:-2px}.select:not(.multiple) .selected-container .option .txt{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;max-width:100%;line-height:normal}.select:not(.multiple) .selected-container:hover{cursor:pointer}.select.disabled{color:var(--txtDisabledColor);pointer-events:none}.select.disabled .txt-placeholder,.select.disabled .selected-container{color:inherit}.select.disabled .selected-container .link-hint{pointer-events:auto}.select.disabled .selected-container *:not(.link-hint){color:inherit!important}.select.disabled .selected-container:after,.select.disabled .selected-container .clear{display:none}.select.disabled .selected-container:hover{cursor:inherit}.select .txt-missing{color:var(--txtHintColor);padding:5px 12px;margin:0}.select .options-dropdown{max-height:none;border:0;overflow:auto;border-top-left-radius:0;border-top-right-radius:0;margin-top:-2px;box-shadow:0 2px 5px 0 var(--shadowColor),inset 0 0 0 2px var(--baseAlt2Color)}.select .options-dropdown .input-group:focus-within{box-shadow:none}.select .options-dropdown .form-field.options-search{margin:0 0 5px;padding:0 0 2px;color:var(--txtHintColor);border-bottom:1px solid var(--baseAlt2Color)}.select .options-dropdown .form-field.options-search .input-group{border-radius:0;padding:0 0 0 10px;margin:0;background:none;column-gap:0;border:0}.select .options-dropdown .form-field.options-search input{border:0;padding-left:9px;padding-right:9px;background:none}.select .options-dropdown .options-list{overflow:auto;max-height:270px;width:auto;margin-left:0;margin-right:-5px;padding-right:5px}.select .options-list:not(:empty)~[slot=afterOptions]:not(:empty){margin:5px -5px -5px}.select .options-list:not(:empty)~[slot=afterOptions]:not(:empty) .btn-block{border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}label~.select .selected-container{padding-bottom:4px;border-top-left-radius:0;border-top-right-radius:0}label~.select.multiple .selected-container{padding-top:3px;padding-bottom:3px;padding-left:10px}.select.block-options.multiple .selected-container .option{width:100%;box-shadow:0 2px 5px 0 var(--shadowColor)}.field-type-select .options-dropdown .options-list{max-height:490px}.form-field-file label{border-bottom:0}.form-field-file .filename{align-items:center;max-width:100%;min-width:0;margin-right:auto;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.form-field-file .filename i{text-decoration:none}.form-field-file .files-list{padding-top:5px;background:var(--baseAlt1Color);border:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius);transition:background var(--baseAnimationSpeed)}.form-field-file .files-list .list-item{display:flex;width:100%;align-items:center;row-gap:10px;column-gap:var(--xsSpacing);padding:10px 15px;min-height:44px;border-top:1px solid var(--baseAlt2Color)}.form-field-file .files-list .list-item:last-child{border-radius:inherit;border-bottom:0}.form-field-file .files-list .btn-list-item{padding:5px}.form-field-file:focus-within .files-list,.form-field-file:focus-within label{background:var(--baseAlt1Color)}.form-field label~.code-editor{padding-bottom:6px;padding-top:4px}.code-editor .cm-editor{border:0!important;outline:none!important}.code-editor .cm-editor .cm-line{padding-left:0;padding-right:0}.code-editor .cm-editor .cm-tooltip-autocomplete{box-shadow:0 2px 5px 0 var(--shadowColor);border-radius:var(--baseRadius);background:var(--baseColor);border:0;padding:0 3px;font-size:.92rem}.code-editor .cm-editor .cm-tooltip-autocomplete ul{margin:0;border-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul>:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul>:last-child{border-bottom-left-radius:inherit;border-bottom-right-radius:inherit}.code-editor .cm-editor .cm-tooltip-autocomplete ul li[aria-selected]{background:var(--infoColor)}.code-editor .cm-editor .cm-scroller{outline:0!important;font-family:var(--monospaceFontFamily);font-size:var(--baseFontSize);line-height:var(--baseLineHeight)}.code-editor .cm-editor .cm-cursorLayer .cm-cursor{margin-left:0!important}.code-editor .cm-editor .cm-placeholder{color:var(--txtDisabledColor);font-family:var(--monospaceFontFamily);font-size:var(--baseFontSize);line-height:var(--baseLineHeight)}.app-footer{display:flex;width:100%;align-items:center;justify-content:right;column-gap:var(--smSpacing);font-size:var(--smFontSize);line-height:var(--smLineHeight);color:var(--txtHintColor)}.app-footer .footer-item{display:inline-flex;align-items:center;column-gap:5px}.app-footer .footer-item img{width:16px}.app-footer a{color:inherit;text-decoration:none;transition:color var(--baseAnimationSpeed)}.app-footer a:focus-visible,.app-footer a:hover,.app-footer a:active{color:var(--txtPrimaryColor)}.main-menu{--menuItemSize: 45px;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;row-gap:var(--smSpacing);font-size:16px;color:var(--txtPrimaryColor)}.main-menu i{font-size:24px;line-height:1}.main-menu .menu-item{position:relative;outline:0;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;text-align:center;justify-content:center;user-select:none;color:inherit;min-width:var(--menuItemSize);min-height:var(--menuItemSize);border:2px solid transparent;border-radius:var(--lgRadius);transition:background var(--baseAnimationSpeed),border var(--baseAnimationSpeed)}.main-menu .menu-item:focus-visible,.main-menu .menu-item:hover{background:var(--baseAlt1Color)}.main-menu .menu-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}.main-menu .menu-item.active,.main-menu .menu-item.current-route{background:var(--baseColor);border-color:var(--primaryColor)}.app-sidebar{position:relative;z-index:1;display:flex;flex-grow:0;flex-shrink:0;flex-direction:column;align-items:center;width:var(--appSidebarWidth);padding:var(--smSpacing) 0px var(--smSpacing);background:var(--baseColor);border-right:1px solid var(--baseAlt2Color)}.app-sidebar .main-menu{flex-grow:1;justify-content:flex-start;overflow-x:hidden;overflow-y:auto;overflow-y:overlay;margin-top:34px;margin-bottom:var(--baseSpacing)}.app-layout{display:flex;width:100%;height:100vh;overflow-x:overlay}.app-layout .app-body{flex-grow:1;min-width:0;height:100%;display:flex;align-items:stretch}.app-layout .app-sidebar~.app-body{min-width:650px}.page-sidebar{display:flex;flex-direction:column;width:var(--pageSidebarWidth);flex-shrink:0;flex-grow:0;overflow-x:hidden;overflow-y:auto;background:var(--baseColor);padding:calc(var(--baseSpacing) - 5px) 0 var(--smSpacing);border-right:1px solid var(--baseAlt2Color)}.page-sidebar>*{padding:0 var(--smSpacing)}.page-sidebar .sidebar-content{overflow-x:hidden;overflow-y:auto;overflow-y:overlay}.page-sidebar .sidebar-content>:first-child{margin-top:0}.page-sidebar .sidebar-content>:last-child{margin-bottom:0}.page-sidebar .sidebar-footer{margin-top:var(--smSpacing)}.page-sidebar .search{display:flex;align-items:center;width:auto;column-gap:5px;margin:0 0 var(--xsSpacing);color:var(--txtHintColor);opacity:.7;transition:opacity var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.page-sidebar .search input{border:0;background:var(--baseColor);transition:box-shadow var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.page-sidebar .search .btn-clear{margin-right:-8px}.page-sidebar .search:hover,.page-sidebar .search:focus-within,.page-sidebar .search.active{opacity:1;color:var(--txtPrimaryColor)}.page-sidebar .search:hover input,.page-sidebar .search:focus-within input,.page-sidebar .search.active input{background:var(--baseAlt2Color)}.page-sidebar .sidebar-title{margin:var(--baseSpacing) 0 var(--xsSpacing);font-weight:600;font-size:1rem;line-height:var(--smLineHeight);color:var(--txtHintColor)}.page-sidebar .sidebar-list-item{cursor:pointer;outline:0;text-decoration:none;position:relative;display:flex;align-items:center;column-gap:10px;margin:10px 0;padding:3px 10px;font-size:16px;min-height:var(--btnHeight);min-width:0;color:var(--txtHintColor);border-radius:var(--baseRadius);user-select:none;transition:background var(--baseAnimationSpeed),color var(--baseAnimationSpeed)}.page-sidebar .sidebar-list-item i{font-size:18px}.page-sidebar .sidebar-list-item .txt{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.page-sidebar .sidebar-list-item:focus-visible,.page-sidebar .sidebar-list-item:hover,.page-sidebar .sidebar-list-item:active,.page-sidebar .sidebar-list-item.active{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.page-sidebar .sidebar-list-item:active{background:var(--baseAlt2Color);transition-duration:var(--activeAnimationSpeed)}@media screen and (max-width: 1100px){.page-sidebar{--pageSidebarWidth: 190px}.page-sidebar>*{padding-left:10px;padding-right:10px}}.page-header{display:flex;align-items:center;width:100%;min-height:var(--btnHeight);gap:var(--xsSpacing);margin:0 0 var(--baseSpacing)}.page-header .btns-group{margin-left:auto;justify-content:end}@media screen and (max-width: 1050px){.page-header{flex-wrap:wrap}.page-header .btns-group{width:100%}.page-header .btns-group .btn{flex-grow:1;flex-basis:0}}.page-header-wrapper{background:var(--baseColor);width:auto;margin-top:calc(-1 * (var(--baseSpacing) - 5px));margin-left:calc(-1 * var(--baseSpacing));margin-right:calc(-1 * var(--baseSpacing));margin-bottom:var(--baseSpacing);padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing);border-bottom:1px solid var(--baseAlt2Color)}.breadcrumbs{display:flex;align-items:center;gap:30px;user-select:none;color:var(--txtDisabledColor)}.breadcrumbs .breadcrumb-item{position:relative;margin:0;line-height:1;font-weight:400}.breadcrumbs .breadcrumb-item:after{content:"/";position:absolute;right:-20px;top:0;width:10px;text-align:center;pointer-events:none;opacity:.4}.breadcrumbs .breadcrumb-item:last-child{word-break:break-word;color:var(--txtPrimaryColor)}.breadcrumbs .breadcrumb-item:last-child:after{content:none;display:none}.breadcrumbs a{text-decoration:none;color:inherit;transition:color var(--baseAnimationSpeed)}.breadcrumbs a:hover{color:var(--txtPrimaryColor)}.page-wrapper{position:relative;display:block;width:100%;flex-grow:1;padding:calc(var(--baseSpacing) - 5px) var(--baseSpacing);overflow-x:hidden;overflow-y:auto;overflow-y:overlay}@keyframes tabChange{0%{opacity:.5}to{opacity:1}}.tabs-header{display:flex;align-items:stretch;justify-content:flex-start;column-gap:10px;width:100%;min-height:50px;user-select:none;margin:0 0 var(--baseSpacing);border-bottom:1px solid var(--baseAlt2Color)}.tabs-header .tab-item{position:relative;outline:0;border:0;background:none;display:inline-flex;align-items:center;justify-content:center;min-width:50px;gap:5px;padding:10px;margin:0;font-size:var(--lgFontSize);line-height:var(--baseLineHeight);font-family:var(--baseFontFamily);color:var(--txtHintColor);text-align:center;text-decoration:none;cursor:pointer;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}.tabs-header .tab-item:after{content:"";position:absolute;display:block;left:0;bottom:-1px;width:100%;height:2px;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);background:var(--primaryColor);transform:rotateY(90deg);transition:transform .25s}.tabs-header .tab-item .txt,.tabs-header .tab-item i{display:inline-block;vertical-align:top}.tabs-header .tab-item:hover,.tabs-header .tab-item:focus-visible,.tabs-header .tab-item:active{color:var(--txtPrimaryColor)}.tabs-header .tab-item:focus-visible,.tabs-header .tab-item:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}.tabs-header .tab-item.active{color:var(--txtPrimaryColor)}.tabs-header .tab-item.active:after{transform:rotateY(0)}.tabs-header .tab-item.disabled{pointer-events:none;color:var(--txtDisabledColor)}.tabs-header .tab-item.disabled:after{display:none}.tabs-header.right{justify-content:flex-end}.tabs-header.center{justify-content:center}.tabs-header.stretched .tab-item{flex-grow:1;flex-basis:0}.tabs-header.compact{min-height:30px;margin-bottom:var(--smSpacing)}.tabs-content{position:relative}.tabs-content>.tab-item{width:100%;display:none}.tabs-content>.tab-item.active{display:block;opacity:0;animation:tabChange .3s forwards}.tabs-content>.tab-item>:first-child{margin-top:0}.tabs-content>.tab-item>:last-child{margin-bottom:0}.tabs{position:relative}.accordion{outline:0;position:relative;border-radius:var(--baseRadius);background:var(--baseColor);border:1px solid var(--baseAlt2Color);transition:box-shadow var(--baseAnimationSpeed),margin var(--baseAnimationSpeed)}.accordion .accordion-header{outline:0;position:relative;display:flex;min-height:52px;align-items:center;row-gap:10px;column-gap:var(--smSpacing);padding:12px 20px 10px;width:100%;user-select:none;color:var(--txtPrimaryColor);border-radius:inherit;transition:background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.accordion .accordion-header .icon{width:18px;text-align:center}.accordion .accordion-header .icon i{display:inline-block;vertical-align:top;font-size:1.1rem}.accordion .accordion-header.interactive{padding-right:50px;cursor:pointer}.accordion .accordion-header.interactive:after{content:"\ea4e";position:absolute;right:15px;top:50%;margin-top:-12.5px;width:25px;height:25px;line-height:25px;color:var(--txtHintColor);font-family:var(--iconFontFamily);font-size:1.3em;text-align:center;transition:color var(--baseAnimationSpeed)}.accordion .accordion-header:hover:after,.accordion .accordion-header:focus-visible:after{color:var(--txtPrimaryColor)}.accordion .accordion-header:active{transition-duration:var(--activeAnimationSpeed)}.accordion .accordion-content{padding:20px}.accordion:hover,.accordion:focus-visible,.accordion.active{z-index:9}.accordion:hover .accordion-header.interactive,.accordion:focus-visible .accordion-header.interactive,.accordion.active .accordion-header.interactive{background:var(--baseAlt1Color)}.accordion.active{box-shadow:0 2px 5px 0 var(--shadowColor)}.accordion.active .accordion-header{color:var(--baseColor);box-shadow:0 0 0 1px var(--primaryColor);border-bottom-left-radius:0;border-bottom-right-radius:0;background:var(--primaryColor)}.accordion.active .accordion-header.interactive{background:var(--primaryColor)}.accordion.active .accordion-header.interactive:after{color:inherit;content:"\ea78"}.accordion.disabled{z-index:0;border-color:var(--baseAlt1Color)}.accordion.disabled .accordion-header{color:var(--txtDisabledColor)}.accordions .accordion{border-radius:0;margin:-1px 0 0}.accordions .accordion.active{border-radius:var(--baseRadius);margin:var(--smSpacing) 0}.accordions .accordion.active+.accordion{border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.accordions .accordion:first-child{margin-top:0;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius)}.accordions .accordion:last-child{margin-bottom:0;border-bottom-left-radius:var(--baseRadius);border-bottom-right-radius:var(--baseRadius)}.field-accordion.active .accordion-header{padding-right:var(--smSpacing)}.field-accordion.active .accordion-header:after{content:none;display:none}table{border-collapse:separate;min-width:100%;transition:opacity var(--baseAnimationSpeed)}table .form-field{margin:0;line-height:1}table td,table th{outline:0;vertical-align:middle;position:relative;text-align:left;padding:10px;border-bottom:1px solid var(--baseAlt2Color)}table td:first-child,table th:first-child{padding-left:20px}table td:last-child,table th:last-child{padding-right:20px}table th{color:var(--txtHintColor);font-weight:600;font-size:1rem;user-select:none;height:50px;line-height:var(--smLineHeight)}table th i{font-size:inherit}table td{height:60px;word-break:break-word}table .min-width{width:1%!important;white-space:nowrap}table .nowrap{white-space:nowrap}table .col-sort{cursor:pointer;border-top-left-radius:var(--baseRadius);border-top-right-radius:var(--baseRadius);padding-right:30px;transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed)}table .col-sort:after{content:"\ea4c";position:absolute;right:10px;top:50%;margin-top:-12.5px;line-height:25px;height:25px;font-family:var(--iconFontFamily);font-weight:400;color:var(--txtHintColor);opacity:0;transition:color var(--baseAnimationSpeed),opacity var(--baseAnimationSpeed)}table .col-sort.sort-desc:after{content:"\ea4c"}table .col-sort.sort-asc:after{content:"\ea76"}table .col-sort.sort-active:after{opacity:1}table .col-sort:hover,table .col-sort:focus-visible{background:var(--baseAlt1Color)}table .col-sort:hover:after,table .col-sort:focus-visible:after{opacity:1}table .col-sort:active{transition-duration:var(--activeAnimationSpeed);background:var(--baseAlt2Color)}table .col-sort.col-sort-disabled{cursor:default;background:none}table .col-sort.col-sort-disabled:after{display:none}table .col-header-content{display:inline-flex;align-items:center;flex-wrap:nowrap;gap:5px}table .col-header-content .txt{max-width:140px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table .col-field-created,table .col-field-updated,table .col-type-action{width:1%!important;white-space:nowrap}table .col-type-action{white-space:nowrap;text-align:right;color:var(--txtHintColor)}table .col-type-action i{display:inline-block;vertical-align:top;transition:transform var(--baseAnimationSpeed)}table td.col-type-json{font-family:monospace;font-size:var(--smFontSize);line-height:var(--smLineHeight);max-width:300px}table .col-type-text{max-width:300px}table .col-type-select{min-width:150px}table .col-type-email{min-width:120px}table .col-type-file{min-width:100px}table td.col-field-id{width:0;white-space:nowrap}table tr{outline:0;background:var(--bodyColor);transition:background var(--baseAnimationSpeed)}table tr.row-handle{cursor:pointer;user-select:none}table tr.row-handle:focus-visible,table tr.row-handle:hover,table tr.row-handle:active{background:var(--baseAlt1Color)}table tr.row-handle:focus-visible .action-col,table tr.row-handle:hover .action-col,table tr.row-handle:active .action-col{color:var(--txtPrimaryColor)}table tr.row-handle:focus-visible .action-col i,table tr.row-handle:hover .action-col i,table tr.row-handle:active .action-col i{transform:translate(3px)}table tr.row-handle:active{transition-duration:var(--activeAnimationSpeed)}table.table-compact td,table.table-compact th{height:auto}table.table-border{border:1px solid var(--baseAlt2Color)}table.table-border tr{background:var(--baseColor)}table.table-border th{background:var(--baseAlt1Color)}table.table-border>:last-child>:last-child th,table.table-border>:last-child>:last-child td{border-bottom:0}table.table-loading{pointer-events:none}.table-wrapper{width:auto;padding:0;overflow-x:auto;max-width:calc(100% + 2 * var(--baseSpacing));margin-left:calc(var(--baseSpacing) * -1);margin-right:calc(var(--baseSpacing) * -1)}.table-wrapper .bulk-select-col input[type=checkbox]~label{opacity:.7}.table-wrapper .bulk-select-col label:hover,.table-wrapper .bulk-select-col label:focus-within,.table-wrapper .bulk-select-col input[type=checkbox]:checked~label{opacity:1!important}.table-wrapper td:first-child,.table-wrapper th:first-child{padding-left:calc(var(--baseSpacing) + 3px)}.table-wrapper td:last-child,.table-wrapper th:last-child{padding-right:calc(var(--baseSpacing) + 3px)}.table-wrapper .bulk-select-col,.table-wrapper .col-type-action{position:sticky;z-index:99}.table-wrapper .bulk-select-col{left:0px}.table-wrapper .col-type-action{right:0}.table-wrapper .bulk-select-col,.table-wrapper .col-type-action{background:inherit}.table-wrapper th.bulk-select-col,.table-wrapper th.col-type-action{background:var(--bodyColor)}.searchbar{--searchHeight: 44px;outline:0;display:flex;align-items:center;height:var(--searchHeight);width:100%;flex-grow:1;padding:5px 7px;margin:0 0 var(--smSpacing);white-space:nowrap;color:var(--txtHintColor);background:var(--baseAlt1Color);border-radius:var(--btnHeight);transition:color var(--baseAnimationSpeed),background var(--baseAnimationSpeed),box-shadow var(--baseAnimationSpeed)}.searchbar>:first-child{border-top-left-radius:var(--btnHeight);border-bottom-left-radius:var(--btnHeight)}.searchbar>:last-child{border-top-right-radius:var(--btnHeight);border-bottom-right-radius:var(--btnHeight)}.searchbar .btn{border-radius:var(--btnHeight)}.searchbar .code-editor,.searchbar input,.searchbar input:focus{font-size:var(--baseFontSize);font-family:var(--monospaceFontFamily);border:0;background:none}.searchbar label>i{line-height:inherit}.searchbar .search-options{flex-shrink:0;width:90px}.searchbar .search-options .selected-container{border-radius:inherit;background:none;padding-right:25px!important}.searchbar .search-options:not(:focus-within) .selected-container{color:var(--txtHintColor)}.searchbar:focus-within{color:var(--txtPrimaryColor);background:var(--baseAlt2Color)}.searchbar-wrapper{position:relative;display:flex;align-items:center;width:100%;min-width:var(--btnHeight);min-height:var(--btnHeight)}.searchbar-wrapper .search-toggle{position:absolute;right:0;top:0}.bulkbar{position:sticky;bottom:-10px;z-index:101;gap:10px;display:flex;justify-content:center;align-items:center;width:var(--smWrapperWidth);max-width:100%;margin:var(--smSpacing) auto;padding:10px var(--smSpacing);border-radius:var(--btnHeight);background:var(--baseColor);border:1px solid var(--baseAlt2Color);box-shadow:0 2px 5px 0 var(--shadowColor)}.flatpickr-calendar{opacity:0;display:none;text-align:center;visibility:hidden;padding:0;animation:none;direction:ltr;border:0;font-size:1rem;line-height:24px;position:absolute;width:298px;box-sizing:border-box;user-select:none;color:var(--txtPrimaryColor);background:var(--baseColor);border-radius:var(--baseRadius);box-shadow:0 2px 5px 0 var(--shadowColor),0 0 0 1px var(--baseAlt2Color)}.flatpickr-calendar input,.flatpickr-calendar select{box-shadow:none;min-height:0;height:var(--inputHeight);border-radius:var(--baseRadius);border:1px solid var(--baseAlt1Color)}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1);animation:fpFadeInDown .3s cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:0;width:100%}.flatpickr-calendar.static{position:absolute;top:100%;margin-top:2px;margin-bottom:10px;width:100%}.flatpickr-calendar.static .flatpickr-days{width:100%}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none!important;box-shadow:none!important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color);box-shadow:-2px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color)}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid var(--baseAlt2Color)}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:"";height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:var(--baseColor)}.flatpickr-calendar.arrowTop:after{border-bottom-color:var(--baseColor)}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:var(--baseColor)}.flatpickr-calendar.arrowBottom:after{border-top-color:var(--baseColor)}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative}.flatpickr-months{display:flex;margin:0 0 4px}.flatpickr-months .flatpickr-month{background:transparent;color:var(--txtPrimaryColor);fill:var(--txtPrimaryColor);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:var(--txtPrimaryColor);fill:var(--txtPrimaryColor)}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{left:0}.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,.flatpickr-months .flatpickr-next-month.flatpickr-next-month{right:0}.flatpickr-months .flatpickr-prev-month:hover,.flatpickr-months .flatpickr-next-month:hover,.flatpickr-months .flatpickr-prev-month:hover svg,.flatpickr-months .flatpickr-next-month:hover svg{fill:var(--txtHintColor)}.flatpickr-months .flatpickr-prev-month svg,.flatpickr-months .flatpickr-next-month svg{width:14px;height:14px}.flatpickr-months .flatpickr-prev-month svg path,.flatpickr-months .flatpickr-next-month svg path{-webkit-transition:fill .1s;transition:fill .1s;fill:inherit}.numInputWrapper{position:relative;height:auto}.numInputWrapper input,.numInputWrapper span{display:inline-block}.numInputWrapper input{width:100%}.numInputWrapper input::-ms-clear{display:none}.numInputWrapper input::-webkit-outer-spin-button,.numInputWrapper input::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.numInputWrapper span{position:absolute;right:0;width:14px;padding:0 4px 0 2px;height:50%;line-height:50%;opacity:0;cursor:pointer;border:1px solid rgba(57,57,57,.15);box-sizing:border-box}.numInputWrapper span:hover{background:rgba(0,0,0,.1)}.numInputWrapper span:active{background:rgba(0,0,0,.2)}.numInputWrapper span:after{display:block;content:"";position:absolute}.numInputWrapper span.arrowUp{top:0;border-bottom:0}.numInputWrapper span.arrowUp:after{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:4px solid rgba(57,57,57,.6);top:26%}.numInputWrapper span.arrowDown{top:50%}.numInputWrapper span.arrowDown:after{border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(57,57,57,.6);top:40%}.numInputWrapper span svg{width:inherit;height:auto}.numInputWrapper span svg path{fill:#00000080}.numInputWrapper:hover{background:var(--baseAlt2Color)}.numInputWrapper:hover span{opacity:1}.flatpickr-current-month{line-height:inherit;color:inherit;position:absolute;width:75%;left:12.5%;padding:1px 0;line-height:1;display:flex;align-items:center;justify-content:center;text-align:center}.flatpickr-current-month span.cur-month{font-family:inherit;font-weight:700;color:inherit;display:inline-block;margin-left:.5ch;padding:0}.flatpickr-current-month span.cur-month:hover{background:var(--baseAlt2Color)}.flatpickr-current-month .numInputWrapper{display:inline-flex;align-items:center;justify-content:center;width:63px;margin:0 5px}.flatpickr-current-month .numInputWrapper span.arrowUp:after{border-bottom-color:var(--txtPrimaryColor)}.flatpickr-current-month .numInputWrapper span.arrowDown:after{border-top-color:var(--txtPrimaryColor)}.flatpickr-current-month input.cur-year{background:transparent;box-sizing:border-box;color:inherit;cursor:text;margin:0;display:inline-block;font-size:inherit;font-family:inherit;line-height:inherit;vertical-align:initial;-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-current-month input.cur-year:focus{outline:0}.flatpickr-current-month input.cur-year[disabled],.flatpickr-current-month input.cur-year[disabled]:hover{color:var(--txtDisabledColor);background:transparent;pointer-events:none}.flatpickr-current-month .flatpickr-monthDropdown-months{appearance:menulist;box-sizing:border-box;color:inherit;cursor:pointer;font-size:inherit;line-height:inherit;outline:none;position:relative;vertical-align:initial;-webkit-box-sizing:border-box;-webkit-appearance:menulist;-moz-appearance:menulist;width:auto}.flatpickr-current-month .flatpickr-monthDropdown-months:focus,.flatpickr-current-month .flatpickr-monthDropdown-months:active{outline:none}.flatpickr-current-month .flatpickr-monthDropdown-months:hover{background:var(--baseAlt2Color)}.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month{background-color:transparent;outline:none;padding:0}.flatpickr-weekdays{background:transparent;text-align:center;overflow:hidden;width:100%;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:28px}.flatpickr-weekdays .flatpickr-weekdaycontainer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}span.flatpickr-weekday{display:block;flex:1;margin:0;cursor:default;line-height:1;background:transparent;color:var(--txtHintColor);text-align:center;font-weight:bolder;font-size:var(--smFontSize)}.dayContainer,.flatpickr-weeks{padding:1px 0 0}.flatpickr-days{position:relative;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.flatpickr-days:focus{outline:0}.dayContainer{padding:0;outline:0;text-align:left;width:100%;box-sizing:border-box;display:inline-block;display:flex;flex-wrap:wrap;transform:translateZ(0);opacity:1}.dayContainer+.dayContainer{-webkit-box-shadow:-1px 0 0 var(--baseAlt2Color);box-shadow:-1px 0 0 var(--baseAlt2Color)}.flatpickr-day{background:none;border:1px solid transparent;border-radius:var(--baseRadius);box-sizing:border-box;color:var(--txtPrimaryColor);cursor:pointer;font-weight:400;width:calc(14.2857143% - 2px);flex-basis:calc(14.2857143% - 2px);height:39px;margin:1px;display:inline-flex;align-items:center;justify-content:center;position:relative;text-align:center;flex-direction:column}.flatpickr-day.weekend,.flatpickr-day:nth-child(7n+6),.flatpickr-day:nth-child(7n+7){color:var(--dangerColor)}.flatpickr-day.inRange,.flatpickr-day.prevMonthDay.inRange,.flatpickr-day.nextMonthDay.inRange,.flatpickr-day.today.inRange,.flatpickr-day.prevMonthDay.today.inRange,.flatpickr-day.nextMonthDay.today.inRange,.flatpickr-day:hover,.flatpickr-day.prevMonthDay:hover,.flatpickr-day.nextMonthDay:hover,.flatpickr-day:focus,.flatpickr-day.prevMonthDay:focus,.flatpickr-day.nextMonthDay:focus{cursor:pointer;outline:0;background:var(--baseAlt2Color);border-color:var(--baseAlt2Color)}.flatpickr-day.today{border-color:var(--baseColor)}.flatpickr-day.today:hover,.flatpickr-day.today:focus{border-color:var(--primaryColor);background:var(--primaryColor);color:var(--txtPrimaryColor)}.flatpickr-day.selected,.flatpickr-day.startRange,.flatpickr-day.endRange,.flatpickr-day.selected.inRange,.flatpickr-day.startRange.inRange,.flatpickr-day.endRange.inRange,.flatpickr-day.selected:focus,.flatpickr-day.startRange:focus,.flatpickr-day.endRange:focus,.flatpickr-day.selected:hover,.flatpickr-day.startRange:hover,.flatpickr-day.endRange:hover,.flatpickr-day.selected.prevMonthDay,.flatpickr-day.startRange.prevMonthDay,.flatpickr-day.endRange.prevMonthDay,.flatpickr-day.selected.nextMonthDay,.flatpickr-day.startRange.nextMonthDay,.flatpickr-day.endRange.nextMonthDay{background:var(--primaryColor);-webkit-box-shadow:none;box-shadow:none;color:#fff;border-color:var(--primaryColor)}.flatpickr-day.selected.startRange,.flatpickr-day.startRange.startRange,.flatpickr-day.endRange.startRange{border-radius:50px 0 0 50px}.flatpickr-day.selected.endRange,.flatpickr-day.startRange.endRange,.flatpickr-day.endRange.endRange{border-radius:0 50px 50px 0}.flatpickr-day.selected.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.startRange.startRange+.endRange:not(:nth-child(7n+1)),.flatpickr-day.endRange.startRange+.endRange:not(:nth-child(7n+1)){-webkit-box-shadow:-10px 0 0 var(--primaryColor);box-shadow:-10px 0 0 var(--primaryColor)}.flatpickr-day.selected.startRange.endRange,.flatpickr-day.startRange.startRange.endRange,.flatpickr-day.endRange.startRange.endRange{border-radius:50px}.flatpickr-day.inRange{border-radius:0;box-shadow:-5px 0 0 var(--baseAlt2Color),5px 0 0 var(--baseAlt2Color)}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover,.flatpickr-day.prevMonthDay,.flatpickr-day.nextMonthDay,.flatpickr-day.notAllowed,.flatpickr-day.notAllowed.prevMonthDay,.flatpickr-day.notAllowed.nextMonthDay{color:var(--txtDisabledColor);background:transparent;border-color:transparent;cursor:default}.flatpickr-day.flatpickr-disabled,.flatpickr-day.flatpickr-disabled:hover{cursor:not-allowed;color:var(--txtDisabledColor);background:var(--baseAlt2Color)}.flatpickr-day.week.selected{border-radius:0;box-shadow:-5px 0 0 var(--primaryColor),5px 0 0 var(--primaryColor)}.flatpickr-day.hidden{visibility:hidden}.rangeMode .flatpickr-day{margin-top:1px}.flatpickr-weekwrapper{float:left}.flatpickr-weekwrapper .flatpickr-weeks{padding:0 12px;-webkit-box-shadow:1px 0 0 var(--baseAlt2Color);box-shadow:1px 0 0 var(--baseAlt2Color)}.flatpickr-weekwrapper .flatpickr-weekday{float:none;width:100%;line-height:28px}.flatpickr-weekwrapper span.flatpickr-day,.flatpickr-weekwrapper span.flatpickr-day:hover{display:block;width:100%;max-width:none;color:var(--txtHintColor);background:transparent;cursor:default;border:none}.flatpickr-innerContainer{display:flex;box-sizing:border-box;overflow:hidden;padding:5px}.flatpickr-rContainer{display:inline-block;padding:0;width:100%;box-sizing:border-box}.flatpickr-time{text-align:center;outline:0;display:block;height:0;line-height:40px;max-height:40px;box-sizing:border-box;overflow:hidden;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-time:after{content:"";display:table;clear:both}.flatpickr-time .numInputWrapper{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;width:40%;height:40px;float:left}.flatpickr-time .numInputWrapper span.arrowUp:after{border-bottom-color:var(--txtPrimaryColor)}.flatpickr-time .numInputWrapper span.arrowDown:after{border-top-color:var(--txtPrimaryColor)}.flatpickr-time.hasSeconds .numInputWrapper{width:26%}.flatpickr-time.time24hr .numInputWrapper{width:49%}.flatpickr-time input{background:transparent;box-shadow:none;border:0;text-align:center;margin:0;padding:0;height:inherit;line-height:inherit;color:var(--txtPrimaryColor);font-size:14px;position:relative;box-sizing:border-box;background:var(--baseColor);-webkit-appearance:textfield;-moz-appearance:textfield;appearance:textfield}.flatpickr-time input.flatpickr-hour{font-weight:700}.flatpickr-time input.flatpickr-minute,.flatpickr-time input.flatpickr-second{font-weight:400}.flatpickr-time input:focus{outline:0;border:0}.flatpickr-time .flatpickr-time-separator,.flatpickr-time .flatpickr-am-pm{height:inherit;float:left;line-height:inherit;color:var(--txtPrimaryColor);font-weight:700;width:2%;user-select:none;align-self:center}.flatpickr-time .flatpickr-am-pm{outline:0;width:18%;cursor:pointer;text-align:center;font-weight:400}.flatpickr-time input:hover,.flatpickr-time .flatpickr-am-pm:hover,.flatpickr-time input:focus,.flatpickr-time .flatpickr-am-pm:focus{background:var(--baseAlt1Color)}.flatpickr-input[readonly]{cursor:pointer}@keyframes fpFadeInDown{0%{opacity:0;transform:translate3d(0,10px,0)}to{opacity:1;transform:translateZ(0)}}.flatpickr-hide-prev-next-month-days .flatpickr-calendar .prevMonthDay{visibility:hidden}.flatpickr-hide-prev-next-month-days .flatpickr-calendar .nextMonthDay,.flatpickr-inline-container .flatpickr-input{display:none}.flatpickr-inline-container .flatpickr-calendar{margin:0;box-shadow:none;border:1px solid var(--baseAlt2Color)}.chart-wrapper.svelte-vh4sl8.svelte-vh4sl8{position:relative;display:block;width:100%}.chart-wrapper.loading.svelte-vh4sl8 .chart-canvas.svelte-vh4sl8{pointer-events:none;opacity:.5}.chart-loader.svelte-vh4sl8.svelte-vh4sl8{position:absolute;z-index:999;top:50%;left:50%;transform:translate(-50%,-50%)}.prism-light code[class*=language-],.prism-light pre[class*=language-]{color:#111b27;background:0 0;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.prism-light code[class*=language-] ::-moz-selection,.prism-light code[class*=language-]::-moz-selection,.prism-light pre[class*=language-] ::-moz-selection,.prism-light pre[class*=language-]::-moz-selection{background:#8da1b9}.prism-light code[class*=language-] ::selection,.prism-light code[class*=language-]::selection,.prism-light pre[class*=language-] ::selection,.prism-light pre[class*=language-]::selection{background:#8da1b9}.prism-light pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}.prism-light :not(pre)>code[class*=language-],.prism-light pre[class*=language-]{background:#e3eaf2}.prism-light :not(pre)>code[class*=language-]{padding:.1em .3em;border-radius:.3em;white-space:normal}.prism-light .token.cdata,.prism-light .token.comment,.prism-light .token.doctype,.prism-light .token.prolog{color:#3c526d}.prism-light .token.punctuation{color:#111b27}.prism-light .token.delimiter.important,.prism-light .token.selector .parent,.prism-light .token.tag,.prism-light .token.tag .token.punctuation{color:#006d6d}.prism-light .token.attr-name,.prism-light .token.boolean,.prism-light .token.boolean.important,.prism-light .token.constant,.prism-light .token.number,.prism-light .token.selector .token.attribute{color:#755f00}.prism-light .token.class-name,.prism-light .token.key,.prism-light .token.parameter,.prism-light .token.property,.prism-light .token.property-access,.prism-light .token.variable{color:#005a8e}.prism-light .token.attr-value,.prism-light .token.color,.prism-light .token.inserted,.prism-light .token.selector .token.value,.prism-light .token.string,.prism-light .token.string .token.url-link{color:#116b00}.prism-light .token.builtin,.prism-light .token.keyword-array,.prism-light .token.package,.prism-light .token.regex{color:#af00af}.prism-light .token.function,.prism-light .token.selector .token.class,.prism-light .token.selector .token.id{color:#7c00aa}.prism-light .token.atrule .token.rule,.prism-light .token.combinator,.prism-light .token.keyword,.prism-light .token.operator,.prism-light .token.pseudo-class,.prism-light .token.pseudo-element,.prism-light .token.selector,.prism-light .token.unit{color:#a04900}.prism-light .token.deleted,.prism-light .token.important{color:#c22f2e}.prism-light .token.keyword-this,.prism-light .token.this{color:#005a8e}.prism-light .token.bold,.prism-light .token.important,.prism-light .token.keyword-this,.prism-light .token.this{font-weight:700}.prism-light .token.delimiter.important{font-weight:inherit}.prism-light .token.italic{font-style:italic}.prism-light .token.entity{cursor:help}.prism-light .language-markdown .token.title,.prism-light .language-markdown .token.title .token.punctuation{color:#005a8e;font-weight:700}.prism-light .language-markdown .token.blockquote.punctuation{color:#af00af}.prism-light .language-markdown .token.code{color:#006d6d}.prism-light .language-markdown .token.hr.punctuation{color:#005a8e}.prism-light .language-markdown .token.url>.token.content{color:#116b00}.prism-light .language-markdown .token.url-link{color:#755f00}.prism-light .language-markdown .token.list.punctuation{color:#af00af}.prism-light .language-markdown .token.table-header,.prism-light .language-json .token.operator{color:#111b27}.prism-light .language-scss .token.variable{color:#006d6d}.prism-light .token.token.cr:before,.prism-light .token.token.lf:before,.prism-light .token.token.space:before,.prism-light .token.token.tab:not(:empty):before{color:#3c526d}.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>a,.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>button{color:#e3eaf2;background:#005a8e}.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>a:focus,.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>a:hover,.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>button:focus,.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>button:hover{color:#e3eaf2;background:rgba(0,90,142,.8549019608);text-decoration:none}.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>span,.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>span:focus,.prism-light div.code-toolbar>.toolbar.toolbar>.toolbar-item>span:hover{color:#e3eaf2;background:#3c526d}.prism-light .line-highlight.line-highlight{background:rgba(141,161,185,.1843137255);background:linear-gradient(to right,rgba(141,161,185,.1843137255) 70%,rgba(141,161,185,.1450980392))}.prism-light .line-highlight.line-highlight:before,.prism-light .line-highlight.line-highlight[data-end]:after{background-color:#3c526d;color:#e3eaf2;box-shadow:0 1px #8da1b9}.prism-light pre[id].linkable-line-numbers.linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:#3c526d1f}.prism-light .line-numbers.line-numbers .line-numbers-rows{border-right:1px solid rgba(141,161,185,.4784313725);background:rgba(208,218,231,.4784313725)}.prism-light .line-numbers .line-numbers-rows>span:before{color:#3c526dda}.prism-light .rainbow-braces .token.token.punctuation.brace-level-1,.prism-light .rainbow-braces .token.token.punctuation.brace-level-5,.prism-light .rainbow-braces .token.token.punctuation.brace-level-9{color:#755f00}.prism-light .rainbow-braces .token.token.punctuation.brace-level-10,.prism-light .rainbow-braces .token.token.punctuation.brace-level-2,.prism-light .rainbow-braces .token.token.punctuation.brace-level-6{color:#af00af}.prism-light .rainbow-braces .token.token.punctuation.brace-level-11,.prism-light .rainbow-braces .token.token.punctuation.brace-level-3,.prism-light .rainbow-braces .token.token.punctuation.brace-level-7{color:#005a8e}.prism-light .rainbow-braces .token.token.punctuation.brace-level-12,.prism-light .rainbow-braces .token.token.punctuation.brace-level-4,.prism-light .rainbow-braces .token.token.punctuation.brace-level-8{color:#7c00aa}.prism-light pre.diff-highlight>code .token.token.deleted:not(.prefix),.prism-light pre>code.diff-highlight .token.token.deleted:not(.prefix){background-color:#c22f2e1f}.prism-light pre.diff-highlight>code .token.token.inserted:not(.prefix),.prism-light pre>code.diff-highlight .token.token.inserted:not(.prefix){background-color:#116b001f}.prism-light .command-line .command-line-prompt{border-right:1px solid rgba(141,161,185,.4784313725)}.prism-light .command-line .command-line-prompt>span:before{color:#3c526dda}code.svelte-tv7jme.svelte-tv7jme{display:block;width:100%;padding:var(--xsSpacing);white-space:pre-wrap;word-break:break-word}.code-wrapper.svelte-tv7jme.svelte-tv7jme{display:block;width:100%}.prism-light.svelte-tv7jme code.svelte-tv7jme{color:var(--txtPrimaryColor);background:var(--baseAlt1Color)}.title.field-name.svelte-162uq6{max-width:130px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.rule-block.svelte-fjxz7k{display:flex;align-items:flex-start;gap:var(--xsSpacing)}.rule-toggle-btn.svelte-fjxz7k{margin-top:15px}.changes-list.svelte-1ghly2p{word-break:break-all}.tabs-content.svelte-b10vi{z-index:3}.filter-op.svelte-1w7s5nw{display:inline-block;vertical-align:top;margin-right:5px;width:30px;text-align:center;padding-left:0;padding-right:0}textarea.svelte-1x1pbts{resize:none;padding-top:4px!important;padding-bottom:5px!important;min-height:var(--inputHeight);height:var(--inputHeight)}.content.svelte-1gjwqyd{flex-shrink:1;flex-grow:0;width:auto;min-width:0}.full-page-panel.svelte-1wbawr2.svelte-1wbawr2{display:flex;flex-direction:column;align-items:center;background:var(--baseColor)}.full-page-panel.svelte-1wbawr2 .wrapper.svelte-1wbawr2{animation:slideIn .2s} diff --git a/ui/dist/assets/index.944ee0db.js b/ui/dist/assets/index.944ee0db.js new file mode 100644 index 00000000..3f488028 --- /dev/null +++ b/ui/dist/assets/index.944ee0db.js @@ -0,0 +1,363 @@ +const I1=function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))i(o);new MutationObserver(o=>{for(const r of o)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function t(o){const r={};return o.integrity&&(r.integrity=o.integrity),o.referrerpolicy&&(r.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?r.credentials="include":o.crossorigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(o){if(o.ep)return;o.ep=!0;const r=t(o);fetch(o.href,r)}};I1();function le(){}const Vr=n=>n;function ut(n,e){for(const t in e)n[t]=e[t];return n}function Ib(n){return n()}function bc(){return Object.create(null)}function rt(n){n.forEach(Ib)}function Yn(n){return typeof n=="function"}function Ee(n,e){return n!=n?e==e:n!==e||n&&typeof n=="object"||typeof n=="function"}let rl;function Qn(n,e){return rl||(rl=document.createElement("a")),rl.href=e,n===rl.href}function R1(n){return Object.keys(n).length===0}function Rb(n,...e){if(n==null)return le;const t=n.subscribe(...e);return t.unsubscribe?()=>t.unsubscribe():t}function pn(n,e,t){n.$$.on_destroy.push(Rb(e,t))}function $n(n,e,t,i){if(n){const o=Nb(n,e,t,i);return n[0](o)}}function Nb(n,e,t,i){return n[1]&&i?ut(t.ctx.slice(),n[1](i(e))):t.ctx}function An(n,e,t,i){if(n[2]&&i){const o=n[2](i(t));if(e.dirty===void 0)return o;if(typeof o=="object"){const r=[],l=Math.max(e.dirty.length,o.length);for(let s=0;s32){const e=[],t=n.ctx.length/32;for(let i=0;iwindow.performance.now():()=>Date.now(),gf=zb?n=>requestAnimationFrame(n):le;const Lo=new Set;function Hb(n){Lo.forEach(e=>{e.c(n)||(Lo.delete(e),e.f())}),Lo.size!==0&&gf(Hb)}function ys(n){let e;return Lo.size===0&&gf(Hb),{promise:new Promise(t=>{Lo.add(e={c:n,f:t})}),abort(){Lo.delete(e)}}}function m(n,e){n.appendChild(e)}function qb(n){if(!n)return document;const e=n.getRootNode?n.getRootNode():n.ownerDocument;return e&&e.host?e:n.ownerDocument}function N1(n){const e=g("style");return j1(qb(n),e),e.sheet}function j1(n,e){m(n.head||n,e)}function w(n,e,t){n.insertBefore(e,t||null)}function k(n){n.parentNode.removeChild(n)}function qn(n,e){for(let t=0;tn.removeEventListener(e,t,i)}function Gt(n){return function(e){return e.preventDefault(),n.call(this,e)}}function Vn(n){return function(e){return e.stopPropagation(),n.call(this,e)}}function Vb(n){return function(e){e.target===this&&n.call(this,e)}}function p(n,e,t){t==null?n.removeAttribute(e):n.getAttribute(e)!==t&&n.setAttribute(e,t)}function ci(n,e){const t=Object.getOwnPropertyDescriptors(n.__proto__);for(const i in e)e[i]==null?n.removeAttribute(i):i==="style"?n.style.cssText=e[i]:i==="__value"?n.value=n[i]=e[i]:t[i]&&t[i].set?n[i]=e[i]:p(n,i,e[i])}function At(n){return n===""?null:+n}function z1(n){return Array.from(n.childNodes)}function ge(n,e){e=""+e,n.wholeText!==e&&(n.data=e)}function Me(n,e){n.value=e==null?"":e}function _c(n,e,t,i){t===null?n.style.removeProperty(e):n.style.setProperty(e,t,i?"important":"")}function ne(n,e,t){n.classList[t?"add":"remove"](e)}function Bb(n,e,{bubbles:t=!1,cancelable:i=!1}={}){const o=document.createEvent("CustomEvent");return o.initCustomEvent(n,t,i,e),o}const Kl=new Map;let Jl=0;function H1(n){let e=5381,t=n.length;for(;t--;)e=(e<<5)-e^n.charCodeAt(t);return e>>>0}function q1(n,e){const t={stylesheet:N1(e),rules:{}};return Kl.set(n,t),t}function Er(n,e,t,i,o,r,l,s=0){const a=16.666/i;let f=`{ +`;for(let _=0;_<=1;_+=a){const y=e+(t-e)*r(_);f+=_*100+`%{${l(y,1-y)}} +`}const c=f+`100% {${l(t,1-t)}} +}`,u=`__svelte_${H1(c)}_${s}`,d=qb(n),{stylesheet:h,rules:b}=Kl.get(d)||q1(d,n);b[u]||(b[u]=!0,h.insertRule(`@keyframes ${u} ${c}`,h.cssRules.length));const v=n.style.animation||"";return n.style.animation=`${v?`${v}, `:""}${u} ${i}ms linear ${o}ms 1 both`,Jl+=1,u}function Pr(n,e){const t=(n.style.animation||"").split(", "),i=t.filter(e?r=>r.indexOf(e)<0:r=>r.indexOf("__svelte")===-1),o=t.length-i.length;o&&(n.style.animation=i.join(", "),Jl-=o,Jl||V1())}function V1(){gf(()=>{Jl||(Kl.forEach(n=>{const{stylesheet:e}=n;let t=e.cssRules.length;for(;t--;)e.deleteRule(t);n.rules={}}),Kl.clear())})}function B1(n,e,t,i){if(!e)return le;const o=n.getBoundingClientRect();if(e.left===o.left&&e.right===o.right&&e.top===o.top&&e.bottom===o.bottom)return le;const{delay:r=0,duration:l=300,easing:s=Vr,start:a=vs()+r,end:f=a+l,tick:c=le,css:u}=t(n,{from:e,to:o},i);let d=!0,h=!1,b;function v(){u&&(b=Er(n,0,1,l,r,s,u)),r||(h=!0)}function _(){u&&Pr(n,b),d=!1}return ys(y=>{if(!h&&y>=a&&(h=!0),h&&y>=f&&(c(1,0),_()),!d)return!1;if(h){const S=y-a,C=0+1*s(S/l);c(C,1-C)}return!0}),v(),c(0,1),_}function U1(n){const e=getComputedStyle(n);if(e.position!=="absolute"&&e.position!=="fixed"){const{width:t,height:i}=e,o=n.getBoundingClientRect();n.style.position="absolute",n.style.width=t,n.style.height=i,Ub(n,o)}}function Ub(n,e){const t=n.getBoundingClientRect();if(e.left!==t.left||e.top!==t.top){const i=getComputedStyle(n),o=i.transform==="none"?"":i.transform;n.style.transform=`${o} translate(${e.left-t.left}px, ${e.top-t.top}px)`}}let Fr;function wr(n){Fr=n}function ks(){if(!Fr)throw new Error("Function called outside component initialization");return Fr}function di(n){ks().$$.on_mount.push(n)}function W1(n){ks().$$.after_update.push(n)}function Y1(n){ks().$$.on_destroy.push(n)}function yn(){const n=ks();return(e,t,{cancelable:i=!1}={})=>{const o=n.$$.callbacks[e];if(o){const r=Bb(e,t,{cancelable:i});return o.slice().forEach(l=>{l.call(n,r)}),!r.defaultPrevented}return!0}}function ft(n,e){const t=n.$$.callbacks[e.type];t&&t.slice().forEach(i=>i.call(this,e))}const br=[],he=[],Rl=[],Da=[],Wb=Promise.resolve();let Oa=!1;function Yb(){Oa||(Oa=!0,Wb.then(Gb))}function Bi(){return Yb(),Wb}function Dt(n){Rl.push(n)}function Re(n){Da.push(n)}const Is=new Set;let ll=0;function Gb(){const n=Fr;do{for(;ll{tr=null})),tr}function fo(n,e,t){n.dispatchEvent(Bb(`${e?"intro":"outro"}${t}`))}const Nl=new Set;let si;function Ae(){si={r:0,c:[],p:si}}function De(){si.r||rt(si.c),si=si.p}function T(n,e){n&&n.i&&(Nl.delete(n),n.i(e))}function F(n,e,t,i){if(n&&n.o){if(Nl.has(n))return;Nl.add(n),si.c.push(()=>{Nl.delete(n),i&&(t&&n.d(1),i())}),n.o(e)}}const vf={duration:0};function yf(n,e,t){let i=e(n,t),o=!1,r,l,s=0;function a(){r&&Pr(n,r)}function f(){const{delay:u=0,duration:d=300,easing:h=Vr,tick:b=le,css:v}=i||vf;v&&(r=Er(n,0,1,d,u,h,v,s++)),b(0,1);const _=vs()+u,y=_+d;l&&l.abort(),o=!0,Dt(()=>fo(n,!0,"start")),l=ys(S=>{if(o){if(S>=y)return b(1,0),fo(n,!0,"end"),a(),o=!1;if(S>=_){const C=h((S-_)/d);b(C,1-C)}}return o})}let c=!1;return{start(){c||(c=!0,Pr(n),Yn(i)?(i=i(),_f().then(f)):f())},invalidate(){c=!1},end(){o&&(a(),o=!1)}}}function Kb(n,e,t){let i=e(n,t),o=!0,r;const l=si;l.r+=1;function s(){const{delay:a=0,duration:f=300,easing:c=Vr,tick:u=le,css:d}=i||vf;d&&(r=Er(n,1,0,f,a,c,d));const h=vs()+a,b=h+f;Dt(()=>fo(n,!1,"start")),ys(v=>{if(o){if(v>=b)return u(0,1),fo(n,!1,"end"),--l.r||rt(l.c),!1;if(v>=h){const _=c((v-h)/f);u(1-_,_)}}return o})}return Yn(i)?_f().then(()=>{i=i(),s()}):s(),{end(a){a&&i.tick&&i.tick(1,0),o&&(r&&Pr(n,r),o=!1)}}}function ct(n,e,t,i){let o=e(n,t),r=i?0:1,l=null,s=null,a=null;function f(){a&&Pr(n,a)}function c(d,h){const b=d.b-r;return h*=Math.abs(b),{a:r,b:d.b,d:b,duration:h,start:d.start,end:d.start+h,group:d.group}}function u(d){const{delay:h=0,duration:b=300,easing:v=Vr,tick:_=le,css:y}=o||vf,S={start:vs()+h,b:d};d||(S.group=si,si.r+=1),l||s?s=S:(y&&(f(),a=Er(n,r,d,b,h,v,y)),d&&_(0,1),l=c(S,b),Dt(()=>fo(n,d,"start")),ys(C=>{if(s&&C>s.start&&(l=c(s,b),s=null,fo(n,l.b,"start"),y&&(f(),a=Er(n,r,l.b,l.duration,0,v,o.css))),l){if(C>=l.end)_(r=l.b,1-r),fo(n,l.b,"end"),s||(l.b?f():--l.group.r||rt(l.group.c)),l=null;else if(C>=l.start){const x=C-l.start;r=l.a+l.d*v(x/l.duration),_(r,1-r)}}return!!(l||s)}))}return{run(d){Yn(o)?_f().then(()=>{o=o(),u(d)}):u(d)},end(){f(),l=s=null}}}function an(n,e){n.d(1),e.delete(n.key)}function Pt(n,e){F(n,1,1,()=>{e.delete(n.key)})}function K1(n,e){n.f(),Pt(n,e)}function st(n,e,t,i,o,r,l,s,a,f,c,u){let d=n.length,h=r.length,b=d;const v={};for(;b--;)v[n[b].key]=b;const _=[],y=new Map,S=new Map;for(b=h;b--;){const A=u(o,r,b),O=t(A);let D=l.get(O);D?i&&D.p(A,e):(D=f(O,A),D.c()),y.set(O,_[b]=D),O in v&&S.set(O,Math.abs(b-v[O]))}const C=new Set,x=new Set;function M(A){T(A,1),A.m(s,c),l.set(A.key,A),c=A.first,h--}for(;d&&h;){const A=_[h-1],O=n[d-1],D=A.key,E=O.key;A===O?(c=A.first,d--,h--):y.has(E)?!l.has(D)||C.has(D)?M(A):x.has(E)?d--:S.get(D)>S.get(E)?(x.add(D),M(A)):(C.add(E),d--):(a(O,l),d--)}for(;d--;){const A=n[d];y.has(A.key)||a(A,l)}for(;h;)M(_[h-1]);return _}function bn(n,e){const t={},i={},o={$$scope:1};let r=n.length;for(;r--;){const l=n[r],s=e[r];if(s){for(const a in l)a in s||(i[a]=1);for(const a in s)o[a]||(t[a]=s[a],o[a]=1);n[r]=s}else for(const a in l)o[a]=1}for(const l in i)l in t||(t[l]=void 0);return t}function pi(n){return typeof n=="object"&&n!==null?n:{}}function Fe(n,e,t){const i=n.$$.props[e];i!==void 0&&(n.$$.bound[i]=t,t(n.$$.ctx[i]))}function V(n){n&&n.c()}function H(n,e,t,i){const{fragment:o,on_mount:r,on_destroy:l,after_update:s}=n.$$;o&&o.m(e,t),i||Dt(()=>{const a=r.map(Ib).filter(Yn);l?l.push(...a):rt(a),n.$$.on_mount=[]}),s.forEach(Dt)}function q(n,e){const t=n.$$;t.fragment!==null&&(rt(t.on_destroy),t.fragment&&t.fragment.d(e),t.on_destroy=t.fragment=null,t.ctx=[])}function J1(n,e){n.$$.dirty[0]===-1&&(br.push(n),Yb(),n.$$.dirty.fill(0)),n.$$.dirty[e/31|0]|=1<{const b=h.length?h[0]:d;return f.ctx&&o(f.ctx[u],f.ctx[u]=b)&&(!f.skip_bound&&f.bound[u]&&f.bound[u](b),c&&J1(n,u)),d}):[],f.update(),c=!0,rt(f.before_update),f.fragment=i?i(f.ctx):!1,e.target){if(e.hydrate){const u=z1(e.target);f.fragment&&f.fragment.l(u),u.forEach(k)}else f.fragment&&f.fragment.c();e.intro&&T(n.$$.fragment),H(n,e.target,e.anchor,e.customElement),Gb()}wr(a)}class Ie{$destroy(){q(this,1),this.$destroy=le}$on(e,t){const i=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return i.push(t),()=>{const o=i.indexOf(t);o!==-1&&i.splice(o,1)}}$set(e){this.$$set&&!R1(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}}function Zt(n){if(!n)throw Error("Parameter args is required");if(!n.component==!n.asyncComponent)throw Error("One and only one of component and asyncComponent is required");if(n.component&&(n.asyncComponent=()=>Promise.resolve(n.component)),typeof n.asyncComponent!="function")throw Error("Parameter asyncComponent must be a function");if(n.conditions){Array.isArray(n.conditions)||(n.conditions=[n.conditions]);for(let t=0;t{i.delete(f),i.size===0&&(t(),t=null)}}return{set:o,update:r,subscribe:l}}function Zb(n,e,t){const i=!Array.isArray(n),o=i?[n]:n,r=e.length<2;return Jb(t,l=>{let s=!1;const a=[];let f=0,c=le;const u=()=>{if(f)return;c();const h=e(i?a[0]:a,l);r?l(h):c=Yn(h)?h:le},d=o.map((h,b)=>Rb(h,v=>{a[b]=v,f&=~(1<{f|=1<{q(c,1)}),De()}r?(e=new r(l()),e.$on("routeEvent",s[7]),V(e.$$.fragment),T(e.$$.fragment,1),H(e,t.parentNode,t)):e=null}else r&&e.$set(f)},i(s){i||(e&&T(e.$$.fragment,s),i=!0)},o(s){e&&F(e.$$.fragment,s),i=!1},d(s){s&&k(t),e&&q(e,s)}}}function X1(n){let e,t,i;const o=[{params:n[1]},n[2]];var r=n[0];function l(s){let a={};for(let f=0;f{q(c,1)}),De()}r?(e=new r(l()),e.$on("routeEvent",s[6]),V(e.$$.fragment),T(e.$$.fragment,1),H(e,t.parentNode,t)):e=null}else r&&e.$set(f)},i(s){i||(e&&T(e.$$.fragment,s),i=!0)},o(s){e&&F(e.$$.fragment,s),i=!1},d(s){s&&k(t),e&&q(e,s)}}}function Q1(n){let e,t,i,o;const r=[X1,Z1],l=[];function s(a,f){return a[1]?0:1}return e=s(n),t=l[e]=r[e](n),{c(){t.c(),i=lt()},m(a,f){l[e].m(a,f),w(a,i,f),o=!0},p(a,[f]){let c=e;e=s(a),e===c?l[e].p(a,f):(Ae(),F(l[c],1,1,()=>{l[c]=null}),De(),t=l[e],t?t.p(a,f):(t=l[e]=r[e](a),t.c()),T(t,1),t.m(i.parentNode,i))},i(a){o||(T(t),o=!0)},o(a){F(t),o=!1},d(a){l[e].d(a),a&&k(i)}}}function vc(){const n=window.location.href.indexOf("#/");let e=n>-1?window.location.href.substr(n+1):"/";const t=e.indexOf("?");let i="";return t>-1&&(i=e.substr(t+1),e=e.substr(0,t)),{location:e,querystring:i}}const ws=Jb(null,function(e){e(vc());const t=()=>{e(vc())};return window.addEventListener("hashchange",t,!1),function(){window.removeEventListener("hashchange",t,!1)}});Zb(ws,n=>n.location);Zb(ws,n=>n.querystring);const yc=Mi(void 0);async function Ss(n){if(!n||n.length<1||n.charAt(0)!="/"&&n.indexOf("#/")!==0)throw Error("Invalid parameter location");await Bi();const e=(n.charAt(0)=="#"?"":"#")+n;try{const t={...history.state};delete t.__svelte_spa_router_scrollX,delete t.__svelte_spa_router_scrollY,window.history.replaceState(t,void 0,e)}catch{console.warn("Caught exception while replacing the current page. If you're running this in the Svelte REPL, please note that the `replace` method might not work in this environment.")}window.dispatchEvent(new Event("hashchange"))}function xn(n,e){if(e=wc(e),!n||!n.tagName||n.tagName.toLowerCase()!="a")throw Error('Action "link" can only be used with
    tags');return kc(n,e),{update(t){t=wc(t),kc(n,t)}}}function kc(n,e){let t=e.href||n.getAttribute("href");if(t&&t.charAt(0)=="/")t="#"+t;else if(!t||t.length<2||t.slice(0,2)!="#/")throw Error('Invalid value for "href" attribute: '+t);n.setAttribute("href",t),n.addEventListener("click",i=>{i.preventDefault(),e.disabled||e_(i.currentTarget.getAttribute("href"))})}function wc(n){return n&&typeof n=="string"?{href:n}:n||{}}function e_(n){history.replaceState({...history.state,__svelte_spa_router_scrollX:window.scrollX,__svelte_spa_router_scrollY:window.scrollY},void 0,void 0),window.location.hash=n}function t_(n,e,t){let{routes:i={}}=e,{prefix:o=""}=e,{restoreScrollState:r=!1}=e;class l{constructor(M,A){if(!A||typeof A!="function"&&(typeof A!="object"||A._sveltesparouter!==!0))throw Error("Invalid component object");if(!M||typeof M=="string"&&(M.length<1||M.charAt(0)!="/"&&M.charAt(0)!="*")||typeof M=="object"&&!(M instanceof RegExp))throw Error('Invalid value for "path" argument - strings must start with / or *');const{pattern:O,keys:D}=Xb(M);this.path=M,typeof A=="object"&&A._sveltesparouter===!0?(this.component=A.component,this.conditions=A.conditions||[],this.userData=A.userData,this.props=A.props||{}):(this.component=()=>Promise.resolve(A),this.conditions=[],this.props={}),this._pattern=O,this._keys=D}match(M){if(o){if(typeof o=="string")if(M.startsWith(o))M=M.substr(o.length)||"/";else return null;else if(o instanceof RegExp){const E=M.match(o);if(E&&E[0])M=M.substr(E[0].length)||"/";else return null}}const A=this._pattern.exec(M);if(A===null)return null;if(this._keys===!1)return A;const O={};let D=0;for(;D{s.push(new l(M,x))}):Object.keys(i).forEach(x=>{s.push(new l(x,i[x]))});let a=null,f=null,c={};const u=yn();async function d(x,M){await Bi(),u(x,M)}let h=null,b=null;r&&(b=x=>{x.state&&x.state.__svelte_spa_router_scrollY?h=x.state:h=null},window.addEventListener("popstate",b),W1(()=>{h?window.scrollTo(h.__svelte_spa_router_scrollX,h.__svelte_spa_router_scrollY):window.scrollTo(0,0)}));let v=null,_=null;const y=ws.subscribe(async x=>{v=x;let M=0;for(;M{yc.set(f)});return}t(0,a=null),_=null,yc.set(void 0)});Y1(()=>{y(),b&&window.removeEventListener("popstate",b)});function S(x){ft.call(this,n,x)}function C(x){ft.call(this,n,x)}return n.$$set=x=>{"routes"in x&&t(3,i=x.routes),"prefix"in x&&t(4,o=x.prefix),"restoreScrollState"in x&&t(5,r=x.restoreScrollState)},n.$$.update=()=>{n.$$.dirty&32&&(history.scrollRestoration=r?"manual":"auto")},[a,f,c,i,o,r,S,C]}class n_ extends Ie{constructor(e){super(),Le(this,e,t_,Q1,Ee,{routes:3,prefix:4,restoreScrollState:5})}}const jl=[];let Qb;function eg(n){const e=n.pattern.test(Qb);Sc(n,n.className,e),Sc(n,n.inactiveClassName,!e)}function Sc(n,e,t){(e||"").split(" ").forEach(i=>{!i||(n.node.classList.remove(i),t&&n.node.classList.add(i))})}ws.subscribe(n=>{Qb=n.location+(n.querystring?"?"+n.querystring:""),jl.map(eg)});function li(n,e){if(e&&(typeof e=="string"||typeof e=="object"&&e instanceof RegExp)?e={path:e}:e=e||{},!e.path&&n.hasAttribute("href")&&(e.path=n.getAttribute("href"),e.path&&e.path.length>1&&e.path.charAt(0)=="#"&&(e.path=e.path.substring(1))),e.className||(e.className="active"),!e.path||typeof e.path=="string"&&(e.path.length<1||e.path.charAt(0)!="/"&&e.path.charAt(0)!="*"))throw Error('Invalid value for "path" argument');const{pattern:t}=typeof e.path=="string"?Xb(e.path):{pattern:e.path},i={node:n,className:e.className,inactiveClassName:e.inactiveClassName,pattern:t};return jl.push(i),eg(i),{destroy(){jl.splice(jl.indexOf(i),1)}}}const i_="modulepreload",Cc={},o_="/_/",_i=function(e,t){return!t||t.length===0?e():Promise.all(t.map(i=>{if(i=`${o_}${i}`,i in Cc)return;Cc[i]=!0;const o=i.endsWith(".css"),r=o?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${i}"]${r}`))return;const l=document.createElement("link");if(l.rel=o?"stylesheet":i_,o||(l.as="script",l.crossOrigin=""),l.href=i,document.head.appendChild(l),o)return new Promise((s,a)=>{l.addEventListener("load",s),l.addEventListener("error",()=>a(new Error(`Unable to preload CSS for ${i}`)))})})).then(()=>e())};function r_(n){if(n.__esModule)return n;var e=Object.defineProperty({},"__esModule",{value:!0});return Object.keys(n).forEach(function(t){var i=Object.getOwnPropertyDescriptor(n,t);Object.defineProperty(e,t,i.get?i:{enumerable:!0,get:function(){return n[t]}})}),e}var Ta={exports:{}},tg=function(n,e){return function(){for(var t=new Array(arguments.length),i=0;i=0)return;o[e]=e==="set-cookie"?(o[e]?o[e]:[]).concat([t]):o[e]?o[e]+", "+t:t}}),o},w_=b_,zs=og,S_=function(n){return new Promise(function(e,t){var i=n.data,o=n.headers,r=n.responseType;fl.isFormData(i)&&delete o["Content-Type"];var l=new XMLHttpRequest;if(n.auth){var s=n.auth.username||"",a=n.auth.password?unescape(encodeURIComponent(n.auth.password)):"";o.Authorization="Basic "+btoa(s+":"+a)}var f=y_(n.baseURL,n.url);function c(){if(l){var d="getAllResponseHeaders"in l?k_(l.getAllResponseHeaders()):null,h={data:r&&r!=="text"&&r!=="json"?l.response:l.responseText,status:l.status,statusText:l.statusText,headers:d,config:n,request:l};g_(e,t,h),l=null}}if(l.open(n.method.toUpperCase(),v_(f,n.params,n.paramsSerializer),!0),l.timeout=n.timeout,"onloadend"in l?l.onloadend=c:l.onreadystatechange=function(){l&&l.readyState===4&&(l.status!==0||l.responseURL&&l.responseURL.indexOf("file:")===0)&&setTimeout(c)},l.onabort=function(){l&&(t(zs("Request aborted",n,"ECONNABORTED",l)),l=null)},l.onerror=function(){t(zs("Network Error",n,null,l)),l=null},l.ontimeout=function(){var d="timeout of "+n.timeout+"ms exceeded";n.timeoutErrorMessage&&(d=n.timeoutErrorMessage),t(zs(d,n,n.transitional&&n.transitional.clarifyTimeoutError?"ETIMEDOUT":"ECONNABORTED",l)),l=null},fl.isStandardBrowserEnv()){var u=(n.withCredentials||w_(f))&&n.xsrfCookieName?__.read(n.xsrfCookieName):void 0;u&&(o[n.xsrfHeaderName]=u)}"setRequestHeader"in l&&fl.forEach(o,function(d,h){i===void 0&&h.toLowerCase()==="content-type"?delete o[h]:l.setRequestHeader(h,d)}),fl.isUndefined(n.withCredentials)||(l.withCredentials=!!n.withCredentials),r&&r!=="json"&&(l.responseType=n.responseType),typeof n.onDownloadProgress=="function"&&l.addEventListener("progress",n.onDownloadProgress),typeof n.onUploadProgress=="function"&&l.upload&&l.upload.addEventListener("progress",n.onUploadProgress),n.cancelToken&&n.cancelToken.promise.then(function(d){l&&(l.abort(),t(d),l=null)}),i||(i=null),l.send(i)})},Qt=Pn,Dc=function(n,e){f_.forEach(n,function(t,i){i!==e&&i.toUpperCase()===e.toUpperCase()&&(n[e]=t,delete n[i])})},C_=ig,x_={"Content-Type":"application/x-www-form-urlencoded"};function Oc(n,e){!Qt.isUndefined(n)&&Qt.isUndefined(n["Content-Type"])&&(n["Content-Type"]=e)}var Tc,Hl={transitional:{silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},adapter:((typeof XMLHttpRequest!="undefined"||typeof process!="undefined"&&Object.prototype.toString.call(process)==="[object process]")&&(Tc=S_),Tc),transformRequest:[function(n,e){return Dc(e,"Accept"),Dc(e,"Content-Type"),Qt.isFormData(n)||Qt.isArrayBuffer(n)||Qt.isBuffer(n)||Qt.isStream(n)||Qt.isFile(n)||Qt.isBlob(n)?n:Qt.isArrayBufferView(n)?n.buffer:Qt.isURLSearchParams(n)?(Oc(e,"application/x-www-form-urlencoded;charset=utf-8"),n.toString()):Qt.isObject(n)||e&&e["Content-Type"]==="application/json"?(Oc(e,"application/json"),function(t,i,o){if(Qt.isString(t))try{return(i||JSON.parse)(t),Qt.trim(t)}catch(r){if(r.name!=="SyntaxError")throw r}return(o||JSON.stringify)(t)}(n)):n}],transformResponse:[function(n){var e=this.transitional,t=e&&e.silentJSONParsing,i=e&&e.forcedJSONParsing,o=!t&&this.responseType==="json";if(o||i&&Qt.isString(n)&&n.length)try{return JSON.parse(n)}catch(r){if(o)throw r.name==="SyntaxError"?C_(r,this,"E_JSON_PARSE"):r}return n}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,validateStatus:function(n){return n>=200&&n<300}};Hl.headers={common:{Accept:"application/json, text/plain, */*"}},Qt.forEach(["delete","get","head"],function(n){Hl.headers[n]={}}),Qt.forEach(["post","put","patch"],function(n){Hl.headers[n]=Qt.merge(x_)});var kf=Hl,M_=Pn,$_=kf,rg=function(n){return!(!n||!n.__CANCEL__)},Ec=Pn,Hs=function(n,e,t){var i=this||$_;return M_.forEach(t,function(o){n=o.call(i,n,e)}),n},A_=rg,D_=kf;function qs(n){n.cancelToken&&n.cancelToken.throwIfRequested()}var on=Pn,lg=function(n,e){e=e||{};var t={},i=["url","method","data"],o=["headers","auth","proxy","params"],r=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],l=["validateStatus"];function s(u,d){return on.isPlainObject(u)&&on.isPlainObject(d)?on.merge(u,d):on.isPlainObject(d)?on.merge({},d):on.isArray(d)?d.slice():d}function a(u){on.isUndefined(e[u])?on.isUndefined(n[u])||(t[u]=s(void 0,n[u])):t[u]=s(n[u],e[u])}on.forEach(i,function(u){on.isUndefined(e[u])||(t[u]=s(void 0,e[u]))}),on.forEach(o,a),on.forEach(r,function(u){on.isUndefined(e[u])?on.isUndefined(n[u])||(t[u]=s(void 0,n[u])):t[u]=s(void 0,e[u])}),on.forEach(l,function(u){u in e?t[u]=s(n[u],e[u]):u in n&&(t[u]=s(void 0,n[u]))});var f=i.concat(o).concat(r).concat(l),c=Object.keys(n).concat(Object.keys(e)).filter(function(u){return f.indexOf(u)===-1});return on.forEach(c,a),t},sg={name:"axios",version:"0.21.4",description:"Promise based HTTP client for the browser and node.js",main:"index.js",scripts:{test:"grunt test",start:"node ./sandbox/server.js",build:"NODE_ENV=production grunt build",preversion:"npm test",version:"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json",postversion:"git push && git push --tags",examples:"node ./examples/server.js",coveralls:"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",fix:"eslint --fix lib/**/*.js"},repository:{type:"git",url:"https://github.com/axios/axios.git"},keywords:["xhr","http","ajax","promise","node"],author:"Matt Zabriskie",license:"MIT",bugs:{url:"https://github.com/axios/axios/issues"},homepage:"https://axios-http.com",devDependencies:{coveralls:"^3.0.0","es6-promise":"^4.2.4",grunt:"^1.3.0","grunt-banner":"^0.6.0","grunt-cli":"^1.2.0","grunt-contrib-clean":"^1.1.0","grunt-contrib-watch":"^1.0.0","grunt-eslint":"^23.0.0","grunt-karma":"^4.0.0","grunt-mocha-test":"^0.13.3","grunt-ts":"^6.0.0-beta.19","grunt-webpack":"^4.0.2","istanbul-instrumenter-loader":"^1.0.0","jasmine-core":"^2.4.1",karma:"^6.3.2","karma-chrome-launcher":"^3.1.0","karma-firefox-launcher":"^2.1.0","karma-jasmine":"^1.1.1","karma-jasmine-ajax":"^0.1.13","karma-safari-launcher":"^1.0.0","karma-sauce-launcher":"^4.3.6","karma-sinon":"^1.0.5","karma-sourcemap-loader":"^0.3.8","karma-webpack":"^4.0.2","load-grunt-tasks":"^3.5.2",minimist:"^1.2.0",mocha:"^8.2.1",sinon:"^4.5.0","terser-webpack-plugin":"^4.2.3",typescript:"^4.0.5","url-search-params":"^0.10.0",webpack:"^4.44.2","webpack-dev-server":"^3.11.0"},browser:{"./lib/adapters/http.js":"./lib/adapters/xhr.js"},jsdelivr:"dist/axios.min.js",unpkg:"dist/axios.min.js",typings:"./index.d.ts",dependencies:{"follow-redirects":"^1.14.0"},bundlesize:[{path:"./dist/axios.min.js",threshold:"5kB"}]},wf={};["object","boolean","number","function","string","symbol"].forEach(function(n,e){wf[n]=function(t){return typeof t===n||"a"+(e<1?"n ":" ")+n}});var Pc={},O_=sg.version.split(".");function ag(n,e){for(var t=e?e.split("."):O_,i=n.split("."),o=0;o<3;o++){if(t[o]>i[o])return!0;if(t[o]0;){var r=i[o],l=e[r];if(l){var s=n[r],a=s===void 0||l(s,r,n);if(a!==!0)throw new TypeError("option "+r+" must be "+a)}else if(t!==!0)throw Error("Unknown option "+r)}},validators:wf},Fc=Pn,E_=ng,Lc=a_,Ic=function(n){return qs(n),n.headers=n.headers||{},n.data=Hs.call(n,n.data,n.headers,n.transformRequest),n.headers=Ec.merge(n.headers.common||{},n.headers[n.method]||{},n.headers),Ec.forEach(["delete","get","head","post","put","patch","common"],function(e){delete n.headers[e]}),(n.adapter||D_.adapter)(n).then(function(e){return qs(n),e.data=Hs.call(n,e.data,e.headers,n.transformResponse),e},function(e){return A_(e)||(qs(n),e&&e.response&&(e.response.data=Hs.call(n,e.response.data,e.response.headers,n.transformResponse))),Promise.reject(e)})},cl=lg,fg=T_,$o=fg.validators;function gr(n){this.defaults=n,this.interceptors={request:new Lc,response:new Lc}}gr.prototype.request=function(n){typeof n=="string"?(n=arguments[1]||{}).url=arguments[0]:n=n||{},(n=cl(this.defaults,n)).method?n.method=n.method.toLowerCase():this.defaults.method?n.method=this.defaults.method.toLowerCase():n.method="get";var e=n.transitional;e!==void 0&&fg.assertOptions(e,{silentJSONParsing:$o.transitional($o.boolean,"1.0.0"),forcedJSONParsing:$o.transitional($o.boolean,"1.0.0"),clarifyTimeoutError:$o.transitional($o.boolean,"1.0.0")},!1);var t=[],i=!0;this.interceptors.request.forEach(function(c){typeof c.runWhen=="function"&&c.runWhen(n)===!1||(i=i&&c.synchronous,t.unshift(c.fulfilled,c.rejected))});var o,r=[];if(this.interceptors.response.forEach(function(c){r.push(c.fulfilled,c.rejected)}),!i){var l=[Ic,void 0];for(Array.prototype.unshift.apply(l,t),l=l.concat(r),o=Promise.resolve(n);l.length;)o=o.then(l.shift(),l.shift());return o}for(var s=n;t.length;){var a=t.shift(),f=t.shift();try{s=a(s)}catch(c){f(c);break}}try{o=Ic(s)}catch(c){return Promise.reject(c)}for(;r.length;)o=o.then(r.shift(),r.shift());return o},gr.prototype.getUri=function(n){return n=cl(this.defaults,n),E_(n.url,n.params,n.paramsSerializer).replace(/^\?/,"")},Fc.forEach(["delete","get","head","options"],function(n){gr.prototype[n]=function(e,t){return this.request(cl(t||{},{method:n,url:e,data:(t||{}).data}))}}),Fc.forEach(["post","put","patch"],function(n){gr.prototype[n]=function(e,t,i){return this.request(cl(i||{},{method:n,url:e,data:t}))}});var P_=gr;function Pa(n){this.message=n}Pa.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},Pa.prototype.__CANCEL__=!0;var cg=Pa,F_=cg;function ql(n){if(typeof n!="function")throw new TypeError("executor must be a function.");var e;this.promise=new Promise(function(i){e=i});var t=this;n(function(i){t.reason||(t.reason=new F_(i),e(t.reason))})}ql.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},ql.source=function(){var n;return{token:new ql(function(e){n=e}),cancel:n}};var L_=ql,Rc=Pn,I_=tg,Vl=P_,R_=lg;function ug(n){var e=new Vl(n),t=I_(Vl.prototype.request,e);return Rc.extend(t,Vl.prototype,e),Rc.extend(t,e),t}var Gn=ug(kf);Gn.Axios=Vl,Gn.create=function(n){return ug(R_(Gn.defaults,n))},Gn.Cancel=cg,Gn.CancelToken=L_,Gn.isCancel=rg,Gn.all=function(n){return Promise.all(n)},Gn.spread=function(n){return function(e){return n.apply(null,e)}},Gn.isAxiosError=function(n){return typeof n=="object"&&n.isAxiosError===!0},Ta.exports=Gn,Ta.exports.default=Gn;var Vs=Ta.exports,Nc=typeof Symbol!="undefined"&&Symbol,N_=function(){if(typeof Symbol!="function"||typeof Object.getOwnPropertySymbols!="function")return!1;if(typeof Symbol.iterator=="symbol")return!0;var n={},e=Symbol("test"),t=Object(e);if(typeof e=="string"||Object.prototype.toString.call(e)!=="[object Symbol]"||Object.prototype.toString.call(t)!=="[object Symbol]")return!1;for(e in n[e]=42,n)return!1;if(typeof Object.keys=="function"&&Object.keys(n).length!==0||typeof Object.getOwnPropertyNames=="function"&&Object.getOwnPropertyNames(n).length!==0)return!1;var i=Object.getOwnPropertySymbols(n);if(i.length!==1||i[0]!==e||!Object.prototype.propertyIsEnumerable.call(n,e))return!1;if(typeof Object.getOwnPropertyDescriptor=="function"){var o=Object.getOwnPropertyDescriptor(n,e);if(o.value!==42||o.enumerable!==!0)return!1}return!0},j_="Function.prototype.bind called on incompatible ",Bs=Array.prototype.slice,z_=Object.prototype.toString,H_=function(n){var e=this;if(typeof e!="function"||z_.call(e)!=="[object Function]")throw new TypeError(j_+e);for(var t,i=Bs.call(arguments,1),o=function(){if(this instanceof t){var f=e.apply(this,i.concat(Bs.call(arguments)));return Object(f)===f?f:this}return e.apply(n,i.concat(Bs.call(arguments)))},r=Math.max(0,e.length-i.length),l=[],s=0;s1&&typeof e!="boolean")throw new Io('"allowMissing" argument must be a boolean');if(G_(/^%?[^%]*%?$/g,n)===null)throw new qo("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var t=Z_(n),i=t.length>0?t[0]:"",o=X_("%"+i+"%",e),r=o.name,l=o.value,s=!1,a=o.alias;a&&(i=a[0],Y_(t,W_([0,1],a)));for(var f=1,c=!0;f=t.length){var b=co(l,u);l=(c=!!b)&&"get"in b&&!("originalValue"in b.get)?b.get:l[u]}else c=Zl(l,u),l=l[u];c&&!s&&(Ro[r]=l)}}return l},pg={exports:{}};(function(n){var e=Sf,t=Cf,i=t("%Function.prototype.apply%"),o=t("%Function.prototype.call%"),r=t("%Reflect.apply%",!0)||e.call(o,i),l=t("%Object.getOwnPropertyDescriptor%",!0),s=t("%Object.defineProperty%",!0),a=t("%Math.max%");if(s)try{s({},"a",{value:1})}catch{s=null}n.exports=function(c){var u=r(e,o,arguments);if(l&&s){var d=l(u,"length");d.configurable&&s(u,"length",{value:1+a(0,c.length-(arguments.length-1))})}return u};var f=function(){return r(e,i,arguments)};s?s(n.exports,"apply",{value:f}):n.exports.apply=f})(pg);var hg=Cf,mg=pg.exports,Q_=mg(hg("String.prototype.indexOf")),ev=r_(Object.freeze({__proto__:null,default:{}})),xf=typeof Map=="function"&&Map.prototype,Ys=Object.getOwnPropertyDescriptor&&xf?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,ul=xf&&Ys&&typeof Ys.get=="function"?Ys.get:null,tv=xf&&Map.prototype.forEach,Mf=typeof Set=="function"&&Set.prototype,Gs=Object.getOwnPropertyDescriptor&&Mf?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,dl=Mf&&Gs&&typeof Gs.get=="function"?Gs.get:null,nv=Mf&&Set.prototype.forEach,nr=typeof WeakMap=="function"&&WeakMap.prototype?WeakMap.prototype.has:null,ir=typeof WeakSet=="function"&&WeakSet.prototype?WeakSet.prototype.has:null,Hc=typeof WeakRef=="function"&&WeakRef.prototype?WeakRef.prototype.deref:null,iv=Boolean.prototype.valueOf,ov=Object.prototype.toString,rv=Function.prototype.toString,lv=String.prototype.match,$f=String.prototype.slice,Ri=String.prototype.replace,sv=String.prototype.toUpperCase,qc=String.prototype.toLowerCase,bg=RegExp.prototype.test,Vc=Array.prototype.concat,ri=Array.prototype.join,av=Array.prototype.slice,Bc=Math.floor,Ks=typeof BigInt=="function"?BigInt.prototype.valueOf:null,Js=Object.getOwnPropertySymbols,Fa=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Symbol.prototype.toString:null,Vo=typeof Symbol=="function"&&typeof Symbol.iterator=="object",rn=typeof Symbol=="function"&&Symbol.toStringTag&&(typeof Symbol.toStringTag===Vo||"symbol")?Symbol.toStringTag:null,gg=Object.prototype.propertyIsEnumerable,Uc=(typeof Reflect=="function"?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(n){return n.__proto__}:null);function Wc(n,e){if(n===1/0||n===-1/0||n!=n||n&&n>-1e3&&n<1e3||bg.call(/e/,e))return e;var t=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if(typeof n=="number"){var i=n<0?-Bc(-n):Bc(n);if(i!==n){var o=String(i),r=$f.call(e,o.length+1);return Ri.call(o,t,"$&_")+"."+Ri.call(Ri.call(r,/([0-9]{3})/g,"$&_"),/_$/,"")}}return Ri.call(e,t,"$&_")}var La=ev,Yc=La.custom,Gc=vg(Yc)?Yc:null;function _g(n,e,t){var i=(t.quoteStyle||e)==="double"?'"':"'";return i+n+i}function fv(n){return Ri.call(String(n),/"/g,""")}function Ia(n){return!(Pi(n)!=="[object Array]"||rn&&typeof n=="object"&&rn in n)}function Kc(n){return!(Pi(n)!=="[object RegExp]"||rn&&typeof n=="object"&&rn in n)}function vg(n){if(Vo)return n&&typeof n=="object"&&n instanceof Symbol;if(typeof n=="symbol")return!0;if(!n||typeof n!="object"||!Fa)return!1;try{return Fa.call(n),!0}catch{}return!1}var cv=Object.prototype.hasOwnProperty||function(n){return n in this};function Ei(n,e){return cv.call(n,e)}function Pi(n){return ov.call(n)}function Jc(n,e){if(n.indexOf)return n.indexOf(e);for(var t=0,i=n.length;te.maxStringLength){var t=n.length-e.maxStringLength,i="... "+t+" more character"+(t>1?"s":"");return yg($f.call(n,0,e.maxStringLength),e)+i}return _g(Ri.call(Ri.call(n,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,uv),"single",e)}function uv(n){var e=n.charCodeAt(0),t={8:"b",9:"t",10:"n",12:"f",13:"r"}[e];return t?"\\"+t:"\\x"+(e<16?"0":"")+sv.call(e.toString(16))}function or(n){return"Object("+n+")"}function Zs(n){return n+" { ? }"}function Zc(n,e,t,i){return n+" ("+e+") {"+(i?Ra(t,i):ri.call(t,", "))+"}"}function Ra(n,e){if(n.length===0)return"";var t=` +`+e.prev+e.base;return t+ri.call(n,","+t)+` +`+e.prev}function pl(n,e){var t=Ia(n),i=[];if(t){i.length=n.length;for(var o=0;o-1?mg(t):t},dv=function n(e,t,i,o){var r=t||{};if(Ei(r,"quoteStyle")&&r.quoteStyle!=="single"&&r.quoteStyle!=="double")throw new TypeError('option "quoteStyle" must be "single" or "double"');if(Ei(r,"maxStringLength")&&(typeof r.maxStringLength=="number"?r.maxStringLength<0&&r.maxStringLength!==1/0:r.maxStringLength!==null))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var l=!Ei(r,"customInspect")||r.customInspect;if(typeof l!="boolean"&&l!=="symbol")throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(Ei(r,"indent")&&r.indent!==null&&r.indent!==" "&&!(parseInt(r.indent,10)===r.indent&&r.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(Ei(r,"numericSeparator")&&typeof r.numericSeparator!="boolean")throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var s=r.numericSeparator;if(e===void 0)return"undefined";if(e===null)return"null";if(typeof e=="boolean")return e?"true":"false";if(typeof e=="string")return yg(e,r);if(typeof e=="number"){if(e===0)return 1/0/e>0?"0":"-0";var a=String(e);return s?Wc(e,a):a}if(typeof e=="bigint"){var f=String(e)+"n";return s?Wc(e,f):f}var c=r.depth===void 0?5:r.depth;if(i===void 0&&(i=0),i>=c&&c>0&&typeof e=="object")return Ia(e)?"[Array]":"[Object]";var u=function(R,G){var U;if(R.indent===" ")U=" ";else{if(!(typeof R.indent=="number"&&R.indent>0))return null;U=ri.call(Array(R.indent+1)," ")}return{base:U,prev:ri.call(Array(G+1),U)}}(r,i);if(o===void 0)o=[];else if(Jc(o,e)>=0)return"[Circular]";function d(R,G,U){if(G&&(o=av.call(o)).push(G),U){var z={depth:r.depth};return Ei(r,"quoteStyle")&&(z.quoteStyle=r.quoteStyle),n(R,z,i+1,o)}return n(R,r,i+1,o)}if(typeof e=="function"&&!Kc(e)){var h=function(R){if(R.name)return R.name;var G=lv.call(rv.call(R),/^function\s*([\w$]+)/);return G?G[1]:null}(e),b=pl(e,d);return"[Function"+(h?": "+h:" (anonymous)")+"]"+(b.length>0?" { "+ri.call(b,", ")+" }":"")}if(vg(e)){var v=Vo?Ri.call(String(e),/^(Symbol\(.*\))_[^)]*$/,"$1"):Fa.call(e);return typeof e!="object"||Vo?v:or(v)}if(function(R){return!R||typeof R!="object"?!1:typeof HTMLElement!="undefined"&&R instanceof HTMLElement?!0:typeof R.nodeName=="string"&&typeof R.getAttribute=="function"}(e)){for(var _="<"+qc.call(String(e.nodeName)),y=e.attributes||[],S=0;S"}if(Ia(e)){if(e.length===0)return"[]";var C=pl(e,d);return u&&!function(R){for(var G=0;G=0)return!1;return!0}(C)?"["+Ra(C,u)+"]":"[ "+ri.call(C,", ")+" ]"}if(function(R){return!(Pi(R)!=="[object Error]"||rn&&typeof R=="object"&&rn in R)}(e)){var x=pl(e,d);return"cause"in Error.prototype||!("cause"in e)||gg.call(e,"cause")?x.length===0?"["+String(e)+"]":"{ ["+String(e)+"] "+ri.call(x,", ")+" }":"{ ["+String(e)+"] "+ri.call(Vc.call("[cause]: "+d(e.cause),x),", ")+" }"}if(typeof e=="object"&&l){if(Gc&&typeof e[Gc]=="function"&&La)return La(e,{depth:c-i});if(l!=="symbol"&&typeof e.inspect=="function")return e.inspect()}if(function(R){if(!ul||!R||typeof R!="object")return!1;try{ul.call(R);try{dl.call(R)}catch{return!0}return R instanceof Map}catch{}return!1}(e)){var M=[];return tv.call(e,function(R,G){M.push(d(G,e,!0)+" => "+d(R,e))}),Zc("Map",ul.call(e),M,u)}if(function(R){if(!dl||!R||typeof R!="object")return!1;try{dl.call(R);try{ul.call(R)}catch{return!0}return R instanceof Set}catch{}return!1}(e)){var A=[];return nv.call(e,function(R){A.push(d(R,e))}),Zc("Set",dl.call(e),A,u)}if(function(R){if(!nr||!R||typeof R!="object")return!1;try{nr.call(R,nr);try{ir.call(R,ir)}catch{return!0}return R instanceof WeakMap}catch{}return!1}(e))return Zs("WeakMap");if(function(R){if(!ir||!R||typeof R!="object")return!1;try{ir.call(R,ir);try{nr.call(R,nr)}catch{return!0}return R instanceof WeakSet}catch{}return!1}(e))return Zs("WeakSet");if(function(R){if(!Hc||!R||typeof R!="object")return!1;try{return Hc.call(R),!0}catch{}return!1}(e))return Zs("WeakRef");if(function(R){return!(Pi(R)!=="[object Number]"||rn&&typeof R=="object"&&rn in R)}(e))return or(d(Number(e)));if(function(R){if(!R||typeof R!="object"||!Ks)return!1;try{return Ks.call(R),!0}catch{}return!1}(e))return or(d(Ks.call(e)));if(function(R){return!(Pi(R)!=="[object Boolean]"||rn&&typeof R=="object"&&rn in R)}(e))return or(iv.call(e));if(function(R){return!(Pi(R)!=="[object String]"||rn&&typeof R=="object"&&rn in R)}(e))return or(d(String(e)));if(!function(R){return!(Pi(R)!=="[object Date]"||rn&&typeof R=="object"&&rn in R)}(e)&&!Kc(e)){var O=pl(e,d),D=Uc?Uc(e)===Object.prototype:e instanceof Object||e.constructor===Object,E=e instanceof Object?"":"null prototype",P=!D&&rn&&Object(e)===e&&rn in e?$f.call(Pi(e),8,-1):E?"Object":"",I=(D||typeof e.constructor!="function"?"":e.constructor.name?e.constructor.name+" ":"")+(P||E?"["+ri.call(Vc.call([],P||[],E||[]),": ")+"] ":"");return O.length===0?I+"{}":u?I+"{"+Ra(O,u)+"}":I+"{ "+ri.call(O,", ")+" }"}return String(e)},pv=Af("%TypeError%"),hl=Af("%WeakMap%",!0),ml=Af("%Map%",!0),hv=Ko("WeakMap.prototype.get",!0),mv=Ko("WeakMap.prototype.set",!0),bv=Ko("WeakMap.prototype.has",!0),gv=Ko("Map.prototype.get",!0),_v=Ko("Map.prototype.set",!0),vv=Ko("Map.prototype.has",!0),Xs=function(n,e){for(var t,i=n;(t=i.next)!==null;i=t)if(t.key===e)return i.next=t.next,t.next=n.next,n.next=t,t},yv=String.prototype.replace,kv=/%20/g,Xc="RFC3986",Df={default:Xc,formatters:{RFC1738:function(n){return yv.call(n,kv,"+")},RFC3986:function(n){return String(n)}},RFC1738:"RFC1738",RFC3986:Xc},wv=Df,Qs=Object.prototype.hasOwnProperty,Ji=Array.isArray,ii=function(){for(var n=[],e=0;e<256;++e)n.push("%"+((e<16?"0":"")+e.toString(16)).toUpperCase());return n}(),Qc=function(n,e){for(var t=e&&e.plainObjects?Object.create(null):{},i=0;i1;){var u=c.pop(),d=u.obj[u.prop];if(Ji(d)){for(var h=[],b=0;b=48&&a<=57||a>=65&&a<=90||a>=97&&a<=122||o===wv.RFC1738&&(a===40||a===41)?l+=r.charAt(s):a<128?l+=ii[a]:a<2048?l+=ii[192|a>>6]+ii[128|63&a]:a<55296||a>=57344?l+=ii[224|a>>12]+ii[128|a>>6&63]+ii[128|63&a]:(s+=1,a=65536+((1023&a)<<10|1023&r.charCodeAt(s)),l+=ii[240|a>>18]+ii[128|a>>12&63]+ii[128|a>>6&63]+ii[128|63&a])}return l},isBuffer:function(n){return!(!n||typeof n!="object")&&!!(n.constructor&&n.constructor.isBuffer&&n.constructor.isBuffer(n))},isRegExp:function(n){return Object.prototype.toString.call(n)==="[object RegExp]"},maybeMap:function(n,e){if(Ji(n)){for(var t=[],i=0;i0?S.join(",")||null:void 0}];else if(vi(a))I=a;else{var G=Object.keys(S);I=f?G.sort(f):G}for(var U=o&&vi(S)&&S.length===1?t+"[]":t,z=0;z-1?n.split(","):n},Ov=function(n,e,t,i){if(n){var o=t.allowDots?n.replace(/\.([^.[]+)/g,"[$1]"):n,r=/(\[[^[\]]*])/g,l=t.depth>0&&/(\[[^[\]]*])/.exec(o),s=l?o.slice(0,l.index):o,a=[];if(s){if(!t.plainObjects&&ja.call(Object.prototype,s)&&!t.allowPrototypes)return;a.push(s)}for(var f=0;t.depth>0&&(l=r.exec(o))!==null&&f=0;--v){var _,y=c[v];if(y==="[]"&&d.parseArrays)_=[].concat(b);else{_=d.plainObjects?Object.create(null):{};var S=y.charAt(0)==="["&&y.charAt(y.length-1)==="]"?y.slice(1,-1):y,C=parseInt(S,10);d.parseArrays||S!==""?!isNaN(C)&&y!==S&&String(C)===S&&C>=0&&d.parseArrays&&C<=d.arrayLimit?(_=[])[C]=b:S!=="__proto__"&&(_[S]=b):_={0:b}}b=_}return b}(a,e,t,i)}},Tv=function(n,e){var t,i=n,o=function(b){if(!b)return Xt;if(b.encoder!==null&&b.encoder!==void 0&&typeof b.encoder!="function")throw new TypeError("Encoder has to be a function.");var v=b.charset||Xt.charset;if(b.charset!==void 0&&b.charset!=="utf-8"&&b.charset!=="iso-8859-1")throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var _=Sr.default;if(b.format!==void 0){if(!Sv.call(Sr.formatters,b.format))throw new TypeError("Unknown format option provided.");_=b.format}var y=Sr.formatters[_],S=Xt.filter;return(typeof b.filter=="function"||vi(b.filter))&&(S=b.filter),{addQueryPrefix:typeof b.addQueryPrefix=="boolean"?b.addQueryPrefix:Xt.addQueryPrefix,allowDots:b.allowDots===void 0?Xt.allowDots:!!b.allowDots,charset:v,charsetSentinel:typeof b.charsetSentinel=="boolean"?b.charsetSentinel:Xt.charsetSentinel,delimiter:b.delimiter===void 0?Xt.delimiter:b.delimiter,encode:typeof b.encode=="boolean"?b.encode:Xt.encode,encoder:typeof b.encoder=="function"?b.encoder:Xt.encoder,encodeValuesOnly:typeof b.encodeValuesOnly=="boolean"?b.encodeValuesOnly:Xt.encodeValuesOnly,filter:S,format:_,formatter:y,serializeDate:typeof b.serializeDate=="function"?b.serializeDate:Xt.serializeDate,skipNulls:typeof b.skipNulls=="boolean"?b.skipNulls:Xt.skipNulls,sort:typeof b.sort=="function"?b.sort:null,strictNullHandling:typeof b.strictNullHandling=="boolean"?b.strictNullHandling:Xt.strictNullHandling}}(e);typeof o.filter=="function"?i=(0,o.filter)("",i):vi(o.filter)&&(t=o.filter);var r,l=[];if(typeof i!="object"||i===null)return"";r=e&&e.arrayFormat in eu?e.arrayFormat:e&&"indices"in e?e.indices?"indices":"repeat":"indices";var s=eu[r];if(e&&"commaRoundTrip"in e&&typeof e.commaRoundTrip!="boolean")throw new TypeError("`commaRoundTrip` must be a boolean, or absent");var a=s==="comma"&&e&&e.commaRoundTrip;t||(t=Object.keys(i)),o.sort&&t.sort(o.sort);for(var f=Sg(),c=0;c0?h+d:""},Ev={formats:Df,parse:function(n,e){var t=function(f){if(!f)return Jt;if(f.decoder!==null&&f.decoder!==void 0&&typeof f.decoder!="function")throw new TypeError("Decoder has to be a function.");if(f.charset!==void 0&&f.charset!=="utf-8"&&f.charset!=="iso-8859-1")throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var c=f.charset===void 0?Jt.charset:f.charset;return{allowDots:f.allowDots===void 0?Jt.allowDots:!!f.allowDots,allowPrototypes:typeof f.allowPrototypes=="boolean"?f.allowPrototypes:Jt.allowPrototypes,allowSparse:typeof f.allowSparse=="boolean"?f.allowSparse:Jt.allowSparse,arrayLimit:typeof f.arrayLimit=="number"?f.arrayLimit:Jt.arrayLimit,charset:c,charsetSentinel:typeof f.charsetSentinel=="boolean"?f.charsetSentinel:Jt.charsetSentinel,comma:typeof f.comma=="boolean"?f.comma:Jt.comma,decoder:typeof f.decoder=="function"?f.decoder:Jt.decoder,delimiter:typeof f.delimiter=="string"||Fo.isRegExp(f.delimiter)?f.delimiter:Jt.delimiter,depth:typeof f.depth=="number"||f.depth===!1?+f.depth:Jt.depth,ignoreQueryPrefix:f.ignoreQueryPrefix===!0,interpretNumericEntities:typeof f.interpretNumericEntities=="boolean"?f.interpretNumericEntities:Jt.interpretNumericEntities,parameterLimit:typeof f.parameterLimit=="number"?f.parameterLimit:Jt.parameterLimit,parseArrays:f.parseArrays!==!1,plainObjects:typeof f.plainObjects=="boolean"?f.plainObjects:Jt.plainObjects,strictNullHandling:typeof f.strictNullHandling=="boolean"?f.strictNullHandling:Jt.strictNullHandling}}(e);if(n===""||n==null)return t.plainObjects?Object.create(null):{};for(var i=typeof n=="string"?function(f,c){var u,d={},h=c.ignoreQueryPrefix?f.replace(/^\?/,""):f,b=c.parameterLimit===1/0?void 0:c.parameterLimit,v=h.split(c.delimiter,b),_=-1,y=c.charset;if(c.charsetSentinel)for(u=0;u-1&&(C=Av(C)?[C]:C),ja.call(d,S)?d[S]=Fo.combine(d[S],C):d[S]=C}return d}(n,t):n,o=t.plainObjects?Object.create(null):{},r=Object.keys(i),l=0;l0&&(!i.exp||i.exp-t>Date.now()/1e3))},n}(),za=function(n,e){return za=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,i){t.__proto__=i}||function(t,i){for(var o in i)Object.prototype.hasOwnProperty.call(i,o)&&(t[o]=i[o])},za(n,e)};function mn(n,e){if(typeof e!="function"&&e!==null)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function t(){this.constructor=n}za(n,e),n.prototype=e===null?Object.create(e):(t.prototype=e.prototype,new t)}function Bl(n,e,t,i){return new(t||(t=Promise))(function(o,r){function l(f){try{a(i.next(f))}catch(c){r(c)}}function s(f){try{a(i.throw(f))}catch(c){r(c)}}function a(f){var c;f.done?o(f.value):(c=f.value,c instanceof t?c:new t(function(u){u(c)})).then(l,s)}a((i=i.apply(n,e||[])).next())})}function Ul(n,e){var t,i,o,r,l={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return r={next:s(0),throw:s(1),return:s(2)},typeof Symbol=="function"&&(r[Symbol.iterator]=function(){return this}),r;function s(a){return function(f){return function(c){if(t)throw new TypeError("Generator is already executing.");for(;l;)try{if(t=1,i&&(o=2&c[0]?i.return:c[0]?i.throw||((o=i.return)&&o.call(i),0):i.next)&&!(o=o.call(i,c[1])).done)return o;switch(i=0,o&&(c=[2&c[0],o.value]),c[0]){case 0:case 1:o=c;break;case 4:return l.label++,{value:c[1],done:!1};case 5:l.label++,i=c[1],c=[0];continue;case 7:c=l.ops.pop(),l.trys.pop();continue;default:if(o=l.trys,!((o=o.length>0&&o[o.length-1])||c[0]!==6&&c[0]!==2)){l=0;continue}if(c[0]===3&&(!o||c[1]>o[0]&&c[1]0?n:1,this.perPage=e>=0?e:0,this.totalItems=t>=0?t:0,this.items=i||[]},Ag=function(n){function e(){return n!==null&&n.apply(this,arguments)||this}return mn(e,n),e.prototype._getFullList=function(t,i,o){var r=this;i===void 0&&(i=100),o===void 0&&(o={});var l=[],s=function(a){return Bl(r,void 0,void 0,function(){return Ul(this,function(f){return[2,this._getList(t,a,i,o).then(function(c){var u=c,d=u.items,h=u.totalItems;return l=l.concat(d),d.length&&h>l.length?s(a+1):l})]})})};return s(1)},e.prototype._getList=function(t,i,o,r){var l=this;return i===void 0&&(i=1),o===void 0&&(o=30),r===void 0&&(r={}),r=Object.assign({page:i,perPage:o},r),this.client.send({method:"get",url:t,params:r}).then(function(s){var a,f,c,u,d,h=[];if(!((a=s==null?void 0:s.data)===null||a===void 0)&&a.items){s.data.items=((f=s==null?void 0:s.data)===null||f===void 0?void 0:f.items)||[];for(var b=0,v=s.data.items;b{const r=[e(o),o];return i&&t(i[0],r[0])===i[0]?i:r},null)[1]}function Kv(n,e){return e.reduce((t,i)=>(t[i]=n[i],t),{})}function Uo(n,e){return Object.prototype.hasOwnProperty.call(n,e)}function ki(n,e,t){return xs(n)&&n>=e&&n<=t}function Jv(n,e){return n-e*Math.floor(n/e)}function Bt(n,e=2){const t=n<0;let i;return t?i="-"+(""+-n).padStart(e,"0"):i=(""+n).padStart(e,"0"),i}function Fi(n){if(!(pt(n)||n===null||n===""))return parseInt(n,10)}function Zi(n){if(!(pt(n)||n===null||n===""))return parseFloat(n)}function Ef(n){if(!(pt(n)||n===null||n==="")){const e=parseFloat("0."+n)*1e3;return Math.floor(e)}}function Pf(n,e,t=!1){const i=10**e;return(t?Math.trunc:Math.round)(n*i)/i}function Wr(n){return n%4===0&&(n%100!==0||n%400===0)}function Cr(n){return Wr(n)?366:365}function ts(n,e){const t=Jv(e-1,12)+1,i=n+(e-t)/12;return t===2?Wr(i)?29:28:[31,null,31,30,31,30,31,31,30,31,30,31][t-1]}function Ff(n){let e=Date.UTC(n.year,n.month-1,n.day,n.hour,n.minute,n.second,n.millisecond);return n.year<100&&n.year>=0&&(e=new Date(e),e.setUTCFullYear(e.getUTCFullYear()-1900)),+e}function ns(n){const e=(n+Math.floor(n/4)-Math.floor(n/100)+Math.floor(n/400))%7,t=n-1,i=(t+Math.floor(t/4)-Math.floor(t/100)+Math.floor(t/400))%7;return e===4||i===3?53:52}function Va(n){return n>99?n:n>60?1900+n:2e3+n}function Jg(n,e,t,i=null){const o=new Date(n),r={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"};i&&(r.timeZone=i);const l={timeZoneName:e,...r},s=new Intl.DateTimeFormat(t,l).formatToParts(o).find(a=>a.type.toLowerCase()==="timezonename");return s?s.value:null}function Ms(n,e){let t=parseInt(n,10);Number.isNaN(t)&&(t=0);const i=parseInt(e,10)||0,o=t<0||Object.is(t,-0)?-i:i;return t*60+o}function Zg(n){const e=Number(n);if(typeof n=="boolean"||n===""||Number.isNaN(e))throw new jn(`Invalid unit value ${n}`);return e}function is(n,e){const t={};for(const i in n)if(Uo(n,i)){const o=n[i];if(o==null)continue;t[e(i)]=Zg(o)}return t}function xr(n,e){const t=Math.trunc(Math.abs(n/60)),i=Math.trunc(Math.abs(n%60)),o=n>=0?"+":"-";switch(e){case"short":return`${o}${Bt(t,2)}:${Bt(i,2)}`;case"narrow":return`${o}${t}${i>0?`:${i}`:""}`;case"techie":return`${o}${Bt(t,2)}${Bt(i,2)}`;default:throw new RangeError(`Value format ${e} is out of range for property format`)}}function $s(n){return Kv(n,["hour","minute","second","millisecond"])}const Xg=/[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/,Zv=["January","February","March","April","May","June","July","August","September","October","November","December"],Qg=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],Xv=["J","F","M","A","M","J","J","A","S","O","N","D"];function e0(n){switch(n){case"narrow":return[...Xv];case"short":return[...Qg];case"long":return[...Zv];case"numeric":return["1","2","3","4","5","6","7","8","9","10","11","12"];case"2-digit":return["01","02","03","04","05","06","07","08","09","10","11","12"];default:return null}}const t0=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],n0=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],Qv=["M","T","W","T","F","S","S"];function i0(n){switch(n){case"narrow":return[...Qv];case"short":return[...n0];case"long":return[...t0];case"numeric":return["1","2","3","4","5","6","7"];default:return null}}const o0=["AM","PM"],ey=["Before Christ","Anno Domini"],ty=["BC","AD"],ny=["B","A"];function r0(n){switch(n){case"narrow":return[...ny];case"short":return[...ty];case"long":return[...ey];default:return null}}function iy(n){return o0[n.hour<12?0:1]}function oy(n,e){return i0(e)[n.weekday-1]}function ry(n,e){return e0(e)[n.month-1]}function ly(n,e){return r0(e)[n.year<0?0:1]}function sy(n,e,t="always",i=!1){const o={years:["year","yr."],quarters:["quarter","qtr."],months:["month","mo."],weeks:["week","wk."],days:["day","day","days"],hours:["hour","hr."],minutes:["minute","min."],seconds:["second","sec."]},r=["hours","minutes","seconds"].indexOf(n)===-1;if(t==="auto"&&r){const u=n==="days";switch(e){case 1:return u?"tomorrow":`next ${o[n][0]}`;case-1:return u?"yesterday":`last ${o[n][0]}`;case 0:return u?"today":`this ${o[n][0]}`}}const l=Object.is(e,-0)||e<0,s=Math.abs(e),a=s===1,f=o[n],c=i?a?f[1]:f[2]||f[1]:a?o[n][0]:n;return l?`${s} ${c} ago`:`in ${s} ${c}`}function iu(n,e){let t="";for(const i of n)i.literal?t+=i.val:t+=e(i.val);return t}const ay={D:qa,DD:Og,DDD:Tg,DDDD:Eg,t:Pg,tt:Fg,ttt:Lg,tttt:Ig,T:Rg,TT:Ng,TTT:jg,TTTT:zg,f:Hg,ff:Vg,fff:Ug,ffff:Yg,F:qg,FF:Bg,FFF:Wg,FFFF:Gg};class vn{static create(e,t={}){return new vn(e,t)}static parseFormat(e){let t=null,i="",o=!1;const r=[];for(let l=0;l0&&r.push({literal:o,val:i}),t=null,i="",o=!o):o||s===t?i+=s:(i.length>0&&r.push({literal:!1,val:i}),i=s,t=s)}return i.length>0&&r.push({literal:o,val:i}),r}static macroTokenToFormatOpts(e){return ay[e]}constructor(e,t){this.opts=t,this.loc=e,this.systemLoc=null}formatWithSystemDefault(e,t){return this.systemLoc===null&&(this.systemLoc=this.loc.redefaultToSystem()),this.systemLoc.dtFormatter(e,{...this.opts,...t}).format()}formatDateTime(e,t={}){return this.loc.dtFormatter(e,{...this.opts,...t}).format()}formatDateTimeParts(e,t={}){return this.loc.dtFormatter(e,{...this.opts,...t}).formatToParts()}resolvedOptions(e,t={}){return this.loc.dtFormatter(e,{...this.opts,...t}).resolvedOptions()}num(e,t=0){if(this.opts.forceSimple)return Bt(e,t);const i={...this.opts};return t>0&&(i.padTo=t),this.loc.numberFormatter(i).format(e)}formatDateTimeFromString(e,t){const i=this.loc.listingMode()==="en",o=this.loc.outputCalendar&&this.loc.outputCalendar!=="gregory",r=(h,b)=>this.loc.extract(e,h,b),l=h=>e.isOffsetFixed&&e.offset===0&&h.allowZ?"Z":e.isValid?e.zone.formatOffset(e.ts,h.format):"",s=()=>i?iy(e):r({hour:"numeric",hourCycle:"h12"},"dayperiod"),a=(h,b)=>i?ry(e,h):r(b?{month:h}:{month:h,day:"numeric"},"month"),f=(h,b)=>i?oy(e,h):r(b?{weekday:h}:{weekday:h,month:"long",day:"numeric"},"weekday"),c=h=>{const b=vn.macroTokenToFormatOpts(h);return b?this.formatWithSystemDefault(e,b):h},u=h=>i?ly(e,h):r({era:h},"era"),d=h=>{switch(h){case"S":return this.num(e.millisecond);case"u":case"SSS":return this.num(e.millisecond,3);case"s":return this.num(e.second);case"ss":return this.num(e.second,2);case"uu":return this.num(Math.floor(e.millisecond/10),2);case"uuu":return this.num(Math.floor(e.millisecond/100));case"m":return this.num(e.minute);case"mm":return this.num(e.minute,2);case"h":return this.num(e.hour%12===0?12:e.hour%12);case"hh":return this.num(e.hour%12===0?12:e.hour%12,2);case"H":return this.num(e.hour);case"HH":return this.num(e.hour,2);case"Z":return l({format:"narrow",allowZ:this.opts.allowZ});case"ZZ":return l({format:"short",allowZ:this.opts.allowZ});case"ZZZ":return l({format:"techie",allowZ:this.opts.allowZ});case"ZZZZ":return e.zone.offsetName(e.ts,{format:"short",locale:this.loc.locale});case"ZZZZZ":return e.zone.offsetName(e.ts,{format:"long",locale:this.loc.locale});case"z":return e.zoneName;case"a":return s();case"d":return o?r({day:"numeric"},"day"):this.num(e.day);case"dd":return o?r({day:"2-digit"},"day"):this.num(e.day,2);case"c":return this.num(e.weekday);case"ccc":return f("short",!0);case"cccc":return f("long",!0);case"ccccc":return f("narrow",!0);case"E":return this.num(e.weekday);case"EEE":return f("short",!1);case"EEEE":return f("long",!1);case"EEEEE":return f("narrow",!1);case"L":return o?r({month:"numeric",day:"numeric"},"month"):this.num(e.month);case"LL":return o?r({month:"2-digit",day:"numeric"},"month"):this.num(e.month,2);case"LLL":return a("short",!0);case"LLLL":return a("long",!0);case"LLLLL":return a("narrow",!0);case"M":return o?r({month:"numeric"},"month"):this.num(e.month);case"MM":return o?r({month:"2-digit"},"month"):this.num(e.month,2);case"MMM":return a("short",!1);case"MMMM":return a("long",!1);case"MMMMM":return a("narrow",!1);case"y":return o?r({year:"numeric"},"year"):this.num(e.year);case"yy":return o?r({year:"2-digit"},"year"):this.num(e.year.toString().slice(-2),2);case"yyyy":return o?r({year:"numeric"},"year"):this.num(e.year,4);case"yyyyyy":return o?r({year:"numeric"},"year"):this.num(e.year,6);case"G":return u("short");case"GG":return u("long");case"GGGGG":return u("narrow");case"kk":return this.num(e.weekYear.toString().slice(-2),2);case"kkkk":return this.num(e.weekYear,4);case"W":return this.num(e.weekNumber);case"WW":return this.num(e.weekNumber,2);case"o":return this.num(e.ordinal);case"ooo":return this.num(e.ordinal,3);case"q":return this.num(e.quarter);case"qq":return this.num(e.quarter,2);case"X":return this.num(Math.floor(e.ts/1e3));case"x":return this.num(e.ts);default:return c(h)}};return iu(vn.parseFormat(t),d)}formatDurationFromString(e,t){const i=a=>{switch(a[0]){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":return"hour";case"d":return"day";case"w":return"week";case"M":return"month";case"y":return"year";default:return null}},o=a=>f=>{const c=i(f);return c?this.num(a.get(c),f.length):f},r=vn.parseFormat(t),l=r.reduce((a,{literal:f,val:c})=>f?a:a.concat(c),[]),s=e.shiftTo(...l.map(i).filter(a=>a));return iu(r,o(s))}}class Zn{constructor(e,t){this.reason=e,this.explanation=t}toMessage(){return this.explanation?`${this.reason}: ${this.explanation}`:this.reason}}class Yr{get type(){throw new Di}get name(){throw new Di}get ianaName(){return this.name}get isUniversal(){throw new Di}offsetName(e,t){throw new Di}formatOffset(e,t){throw new Di}offset(e){throw new Di}equals(e){throw new Di}get isValid(){throw new Di}}let ta=null;class Lf extends Yr{static get instance(){return ta===null&&(ta=new Lf),ta}get type(){return"system"}get name(){return new Intl.DateTimeFormat().resolvedOptions().timeZone}get isUniversal(){return!1}offsetName(e,{format:t,locale:i}){return Jg(e,t,i)}formatOffset(e,t){return xr(this.offset(e),t)}offset(e){return-new Date(e).getTimezoneOffset()}equals(e){return e.type==="system"}get isValid(){return!0}}let Wl={};function fy(n){return Wl[n]||(Wl[n]=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:n,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"})),Wl[n]}const cy={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};function uy(n,e){const t=n.format(e).replace(/\u200E/g,""),i=/(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(t),[,o,r,l,s,a,f,c]=i;return[l,o,r,s,a,f,c]}function dy(n,e){const t=n.formatToParts(e),i=[];for(let o=0;o=0?b:1e3+b,(d-h)/(60*1e3)}equals(e){return e.type==="iana"&&e.name===this.name}get isValid(){return this.valid}}let na=null;class dn extends Yr{static get utcInstance(){return na===null&&(na=new dn(0)),na}static instance(e){return e===0?dn.utcInstance:new dn(e)}static parseSpecifier(e){if(e){const t=e.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);if(t)return new dn(Ms(t[1],t[2]))}return null}constructor(e){super(),this.fixed=e}get type(){return"fixed"}get name(){return this.fixed===0?"UTC":`UTC${xr(this.fixed,"narrow")}`}get ianaName(){return this.fixed===0?"Etc/UTC":`Etc/GMT${xr(-this.fixed,"narrow")}`}offsetName(){return this.name}formatOffset(e,t){return xr(this.fixed,t)}get isUniversal(){return!0}offset(){return this.fixed}equals(e){return e.type==="fixed"&&e.fixed===this.fixed}get isValid(){return!0}}class py extends Yr{constructor(e){super(),this.zoneName=e}get type(){return"invalid"}get name(){return this.zoneName}get isUniversal(){return!1}offsetName(){return null}formatOffset(){return""}offset(){return NaN}equals(){return!1}get isValid(){return!1}}function Li(n,e){if(pt(n)||n===null)return e;if(n instanceof Yr)return n;if(Wv(n)){const t=n.toLowerCase();return t==="local"||t==="system"?e:t==="utc"||t==="gmt"?dn.utcInstance:dn.parseSpecifier(t)||Si.create(n)}else return uo(n)?dn.instance(n):typeof n=="object"&&n.offset&&typeof n.offset=="number"?n:new py(n)}let ou=()=>Date.now(),ru="system",lu=null,su=null,au=null,fu;class Yt{static get now(){return ou}static set now(e){ou=e}static set defaultZone(e){ru=e}static get defaultZone(){return Li(ru,Lf.instance)}static get defaultLocale(){return lu}static set defaultLocale(e){lu=e}static get defaultNumberingSystem(){return su}static set defaultNumberingSystem(e){su=e}static get defaultOutputCalendar(){return au}static set defaultOutputCalendar(e){au=e}static get throwOnInvalid(){return fu}static set throwOnInvalid(e){fu=e}static resetCaches(){Lt.resetCache(),Si.resetCache()}}let cu={};function hy(n,e={}){const t=JSON.stringify([n,e]);let i=cu[t];return i||(i=new Intl.ListFormat(n,e),cu[t]=i),i}let Ba={};function Ua(n,e={}){const t=JSON.stringify([n,e]);let i=Ba[t];return i||(i=new Intl.DateTimeFormat(n,e),Ba[t]=i),i}let Wa={};function my(n,e={}){const t=JSON.stringify([n,e]);let i=Wa[t];return i||(i=new Intl.NumberFormat(n,e),Wa[t]=i),i}let Ya={};function by(n,e={}){const{base:t,...i}=e,o=JSON.stringify([n,i]);let r=Ya[o];return r||(r=new Intl.RelativeTimeFormat(n,e),Ya[o]=r),r}let vr=null;function gy(){return vr||(vr=new Intl.DateTimeFormat().resolvedOptions().locale,vr)}function _y(n){const e=n.indexOf("-u-");if(e===-1)return[n];{let t;const i=n.substring(0,e);try{t=Ua(n).resolvedOptions()}catch{t=Ua(i).resolvedOptions()}const{numberingSystem:o,calendar:r}=t;return[i,o,r]}}function vy(n,e,t){return(t||e)&&(n+="-u",t&&(n+=`-ca-${t}`),e&&(n+=`-nu-${e}`)),n}function yy(n){const e=[];for(let t=1;t<=12;t++){const i=Qe.utc(2016,t,1);e.push(n(i))}return e}function ky(n){const e=[];for(let t=1;t<=7;t++){const i=Qe.utc(2016,11,13+t);e.push(n(i))}return e}function gl(n,e,t,i,o){const r=n.listingMode(t);return r==="error"?null:r==="en"?i(e):o(e)}function wy(n){return n.numberingSystem&&n.numberingSystem!=="latn"?!1:n.numberingSystem==="latn"||!n.locale||n.locale.startsWith("en")||new Intl.DateTimeFormat(n.intl).resolvedOptions().numberingSystem==="latn"}class Sy{constructor(e,t,i){this.padTo=i.padTo||0,this.floor=i.floor||!1;const{padTo:o,floor:r,...l}=i;if(!t||Object.keys(l).length>0){const s={useGrouping:!1,...i};i.padTo>0&&(s.minimumIntegerDigits=i.padTo),this.inf=my(e,s)}}format(e){if(this.inf){const t=this.floor?Math.floor(e):e;return this.inf.format(t)}else{const t=this.floor?Math.floor(e):Pf(e,3);return Bt(t,this.padTo)}}}class Cy{constructor(e,t,i){this.opts=i;let o;if(e.zone.isUniversal){const l=-1*(e.offset/60),s=l>=0?`Etc/GMT+${l}`:`Etc/GMT${l}`;e.offset!==0&&Si.create(s).valid?(o=s,this.dt=e):(o="UTC",i.timeZoneName?this.dt=e:this.dt=e.offset===0?e:Qe.fromMillis(e.ts+e.offset*60*1e3))}else e.zone.type==="system"?this.dt=e:(this.dt=e,o=e.zone.name);const r={...this.opts};o&&(r.timeZone=o),this.dtf=Ua(t,r)}format(){return this.dtf.format(this.dt.toJSDate())}formatToParts(){return this.dtf.formatToParts(this.dt.toJSDate())}resolvedOptions(){return this.dtf.resolvedOptions()}}class xy{constructor(e,t,i){this.opts={style:"long",...i},!t&&Kg()&&(this.rtf=by(e,i))}format(e,t){return this.rtf?this.rtf.format(e,t):sy(t,e,this.opts.numeric,this.opts.style!=="long")}formatToParts(e,t){return this.rtf?this.rtf.formatToParts(e,t):[]}}class Lt{static fromOpts(e){return Lt.create(e.locale,e.numberingSystem,e.outputCalendar,e.defaultToEN)}static create(e,t,i,o=!1){const r=e||Yt.defaultLocale,l=r||(o?"en-US":gy()),s=t||Yt.defaultNumberingSystem,a=i||Yt.defaultOutputCalendar;return new Lt(l,s,a,r)}static resetCache(){vr=null,Ba={},Wa={},Ya={}}static fromObject({locale:e,numberingSystem:t,outputCalendar:i}={}){return Lt.create(e,t,i)}constructor(e,t,i,o){const[r,l,s]=_y(e);this.locale=r,this.numberingSystem=t||l||null,this.outputCalendar=i||s||null,this.intl=vy(this.locale,this.numberingSystem,this.outputCalendar),this.weekdaysCache={format:{},standalone:{}},this.monthsCache={format:{},standalone:{}},this.meridiemCache=null,this.eraCache={},this.specifiedLocale=o,this.fastNumbersCached=null}get fastNumbers(){return this.fastNumbersCached==null&&(this.fastNumbersCached=wy(this)),this.fastNumbersCached}listingMode(){const e=this.isEnglish(),t=(this.numberingSystem===null||this.numberingSystem==="latn")&&(this.outputCalendar===null||this.outputCalendar==="gregory");return e&&t?"en":"intl"}clone(e){return!e||Object.getOwnPropertyNames(e).length===0?this:Lt.create(e.locale||this.specifiedLocale,e.numberingSystem||this.numberingSystem,e.outputCalendar||this.outputCalendar,e.defaultToEN||!1)}redefaultToEN(e={}){return this.clone({...e,defaultToEN:!0})}redefaultToSystem(e={}){return this.clone({...e,defaultToEN:!1})}months(e,t=!1,i=!0){return gl(this,e,i,e0,()=>{const o=t?{month:e,day:"numeric"}:{month:e},r=t?"format":"standalone";return this.monthsCache[r][e]||(this.monthsCache[r][e]=yy(l=>this.extract(l,o,"month"))),this.monthsCache[r][e]})}weekdays(e,t=!1,i=!0){return gl(this,e,i,i0,()=>{const o=t?{weekday:e,year:"numeric",month:"long",day:"numeric"}:{weekday:e},r=t?"format":"standalone";return this.weekdaysCache[r][e]||(this.weekdaysCache[r][e]=ky(l=>this.extract(l,o,"weekday"))),this.weekdaysCache[r][e]})}meridiems(e=!0){return gl(this,void 0,e,()=>o0,()=>{if(!this.meridiemCache){const t={hour:"numeric",hourCycle:"h12"};this.meridiemCache=[Qe.utc(2016,11,13,9),Qe.utc(2016,11,13,19)].map(i=>this.extract(i,t,"dayperiod"))}return this.meridiemCache})}eras(e,t=!0){return gl(this,e,t,r0,()=>{const i={era:e};return this.eraCache[e]||(this.eraCache[e]=[Qe.utc(-40,1,1),Qe.utc(2017,1,1)].map(o=>this.extract(o,i,"era"))),this.eraCache[e]})}extract(e,t,i){const o=this.dtFormatter(e,t),r=o.formatToParts(),l=r.find(s=>s.type.toLowerCase()===i);return l?l.value:null}numberFormatter(e={}){return new Sy(this.intl,e.forceSimple||this.fastNumbers,e)}dtFormatter(e,t={}){return new Cy(e,this.intl,t)}relFormatter(e={}){return new xy(this.intl,this.isEnglish(),e)}listFormatter(e={}){return hy(this.intl,e)}isEnglish(){return this.locale==="en"||this.locale.toLowerCase()==="en-us"||new Intl.DateTimeFormat(this.intl).resolvedOptions().locale.startsWith("en-us")}equals(e){return this.locale===e.locale&&this.numberingSystem===e.numberingSystem&&this.outputCalendar===e.outputCalendar}}function Jo(...n){const e=n.reduce((t,i)=>t+i.source,"");return RegExp(`^${e}$`)}function Zo(...n){return e=>n.reduce(([t,i,o],r)=>{const[l,s,a]=r(e,o);return[{...t,...l},s||i,a]},[{},null,1]).slice(0,2)}function Xo(n,...e){if(n==null)return[null,null];for(const[t,i]of e){const o=t.exec(n);if(o)return i(o)}return[null,null]}function l0(...n){return(e,t)=>{const i={};let o;for(o=0;oh!==void 0&&(b||h&&c)?-h:h;return[{years:d(Zi(t)),months:d(Zi(i)),weeks:d(Zi(o)),days:d(Zi(r)),hours:d(Zi(l)),minutes:d(Zi(s)),seconds:d(Zi(a),a==="-0"),milliseconds:d(Ef(f),u)}]}const Ny={GMT:0,EDT:-4*60,EST:-5*60,CDT:-5*60,CST:-6*60,MDT:-6*60,MST:-7*60,PDT:-7*60,PST:-8*60};function Nf(n,e,t,i,o,r,l){const s={year:e.length===2?Va(Fi(e)):Fi(e),month:Qg.indexOf(t)+1,day:Fi(i),hour:Fi(o),minute:Fi(r)};return l&&(s.second=Fi(l)),n&&(s.weekday=n.length>3?t0.indexOf(n)+1:n0.indexOf(n)+1),s}const jy=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/;function zy(n){const[,e,t,i,o,r,l,s,a,f,c,u]=n,d=Nf(e,o,i,t,r,l,s);let h;return a?h=Ny[a]:f?h=0:h=Ms(c,u),[d,new dn(h)]}function Hy(n){return n.replace(/\([^)]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").trim()}const qy=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/,Vy=/^(Monday|Tuesday|Wedsday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/,By=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/;function uu(n){const[,e,t,i,o,r,l,s]=n;return[Nf(e,o,i,t,r,l,s),dn.utcInstance]}function Uy(n){const[,e,t,i,o,r,l,s]=n;return[Nf(e,s,t,i,o,r,l),dn.utcInstance]}const Wy=Jo($y,Rf),Yy=Jo(Ay,Rf),Gy=Jo(Dy,Rf),Ky=Jo(a0),c0=Zo(Fy,Qo,Gr,Kr),Jy=Zo(Oy,Qo,Gr,Kr),Zy=Zo(Ty,Qo,Gr,Kr),Xy=Zo(Qo,Gr,Kr);function Qy(n){return Xo(n,[Wy,c0],[Yy,Jy],[Gy,Zy],[Ky,Xy])}function e2(n){return Xo(Hy(n),[jy,zy])}function t2(n){return Xo(n,[qy,uu],[Vy,uu],[By,Uy])}function n2(n){return Xo(n,[Iy,Ry])}const i2=Zo(Qo);function o2(n){return Xo(n,[Ly,i2])}const r2=Jo(Ey,Py),l2=Jo(f0),s2=Zo(Qo,Gr,Kr);function a2(n){return Xo(n,[r2,c0],[l2,s2])}const f2="Invalid Duration",u0={weeks:{days:7,hours:7*24,minutes:7*24*60,seconds:7*24*60*60,milliseconds:7*24*60*60*1e3},days:{hours:24,minutes:24*60,seconds:24*60*60,milliseconds:24*60*60*1e3},hours:{minutes:60,seconds:60*60,milliseconds:60*60*1e3},minutes:{seconds:60,milliseconds:60*1e3},seconds:{milliseconds:1e3}},c2={years:{quarters:4,months:12,weeks:52,days:365,hours:365*24,minutes:365*24*60,seconds:365*24*60*60,milliseconds:365*24*60*60*1e3},quarters:{months:3,weeks:13,days:91,hours:91*24,minutes:91*24*60,seconds:91*24*60*60,milliseconds:91*24*60*60*1e3},months:{weeks:4,days:30,hours:30*24,minutes:30*24*60,seconds:30*24*60*60,milliseconds:30*24*60*60*1e3},...u0},Ln=146097/400,Do=146097/4800,u2={years:{quarters:4,months:12,weeks:Ln/7,days:Ln,hours:Ln*24,minutes:Ln*24*60,seconds:Ln*24*60*60,milliseconds:Ln*24*60*60*1e3},quarters:{months:3,weeks:Ln/28,days:Ln/4,hours:Ln*24/4,minutes:Ln*24*60/4,seconds:Ln*24*60*60/4,milliseconds:Ln*24*60*60*1e3/4},months:{weeks:Do/7,days:Do,hours:Do*24,minutes:Do*24*60,seconds:Do*24*60*60,milliseconds:Do*24*60*60*1e3},...u0},io=["years","quarters","months","weeks","days","hours","minutes","seconds","milliseconds"],d2=io.slice(0).reverse();function Xi(n,e,t=!1){const i={values:t?e.values:{...n.values,...e.values||{}},loc:n.loc.clone(e.loc),conversionAccuracy:e.conversionAccuracy||n.conversionAccuracy};return new _t(i)}function p2(n){return n<0?Math.floor(n):Math.ceil(n)}function d0(n,e,t,i,o){const r=n[o][t],l=e[t]/r,s=Math.sign(l)===Math.sign(i[o]),a=!s&&i[o]!==0&&Math.abs(l)<=1?p2(l):Math.trunc(l);i[o]+=a,e[t]-=a*r}function h2(n,e){d2.reduce((t,i)=>pt(e[i])?t:(t&&d0(n,e,t,e,i),i),null)}class _t{constructor(e){const t=e.conversionAccuracy==="longterm"||!1;this.values=e.values,this.loc=e.loc||Lt.create(),this.conversionAccuracy=t?"longterm":"casual",this.invalid=e.invalid||null,this.matrix=t?u2:c2,this.isLuxonDuration=!0}static fromMillis(e,t){return _t.fromObject({milliseconds:e},t)}static fromObject(e,t={}){if(e==null||typeof e!="object")throw new jn(`Duration.fromObject: argument expected to be an object, got ${e===null?"null":typeof e}`);return new _t({values:is(e,_t.normalizeUnit),loc:Lt.fromObject(t),conversionAccuracy:t.conversionAccuracy})}static fromDurationLike(e){if(uo(e))return _t.fromMillis(e);if(_t.isDuration(e))return e;if(typeof e=="object")return _t.fromObject(e);throw new jn(`Unknown duration argument ${e} of type ${typeof e}`)}static fromISO(e,t){const[i]=n2(e);return i?_t.fromObject(i,t):_t.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static fromISOTime(e,t){const[i]=o2(e);return i?_t.fromObject(i,t):_t.invalid("unparsable",`the input "${e}" can't be parsed as ISO 8601`)}static invalid(e,t=null){if(!e)throw new jn("need to specify a reason the Duration is invalid");const i=e instanceof Zn?e:new Zn(e,t);if(Yt.throwOnInvalid)throw new Vv(i);return new _t({invalid:i})}static normalizeUnit(e){const t={year:"years",years:"years",quarter:"quarters",quarters:"quarters",month:"months",months:"months",week:"weeks",weeks:"weeks",day:"days",days:"days",hour:"hours",hours:"hours",minute:"minutes",minutes:"minutes",second:"seconds",seconds:"seconds",millisecond:"milliseconds",milliseconds:"milliseconds"}[e&&e.toLowerCase()];if(!t)throw new Dg(e);return t}static isDuration(e){return e&&e.isLuxonDuration||!1}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}toFormat(e,t={}){const i={...t,floor:t.round!==!1&&t.floor!==!1};return this.isValid?vn.create(this.loc,i).formatDurationFromString(this,e):f2}toHuman(e={}){const t=io.map(i=>{const o=this.values[i];return pt(o)?null:this.loc.numberFormatter({style:"unit",unitDisplay:"long",...e,unit:i.slice(0,-1)}).format(o)}).filter(i=>i);return this.loc.listFormatter({type:"conjunction",style:e.listStyle||"narrow",...e}).format(t)}toObject(){return this.isValid?{...this.values}:{}}toISO(){if(!this.isValid)return null;let e="P";return this.years!==0&&(e+=this.years+"Y"),(this.months!==0||this.quarters!==0)&&(e+=this.months+this.quarters*3+"M"),this.weeks!==0&&(e+=this.weeks+"W"),this.days!==0&&(e+=this.days+"D"),(this.hours!==0||this.minutes!==0||this.seconds!==0||this.milliseconds!==0)&&(e+="T"),this.hours!==0&&(e+=this.hours+"H"),this.minutes!==0&&(e+=this.minutes+"M"),(this.seconds!==0||this.milliseconds!==0)&&(e+=Pf(this.seconds+this.milliseconds/1e3,3)+"S"),e==="P"&&(e+="T0S"),e}toISOTime(e={}){if(!this.isValid)return null;const t=this.toMillis();if(t<0||t>=864e5)return null;e={suppressMilliseconds:!1,suppressSeconds:!1,includePrefix:!1,format:"extended",...e};const i=this.shiftTo("hours","minutes","seconds","milliseconds");let o=e.format==="basic"?"hhmm":"hh:mm";(!e.suppressSeconds||i.seconds!==0||i.milliseconds!==0)&&(o+=e.format==="basic"?"ss":":ss",(!e.suppressMilliseconds||i.milliseconds!==0)&&(o+=".SSS"));let r=i.toFormat(o);return e.includePrefix&&(r="T"+r),r}toJSON(){return this.toISO()}toString(){return this.toISO()}toMillis(){return this.as("milliseconds")}valueOf(){return this.toMillis()}plus(e){if(!this.isValid)return this;const t=_t.fromDurationLike(e),i={};for(const o of io)(Uo(t.values,o)||Uo(this.values,o))&&(i[o]=t.get(o)+this.get(o));return Xi(this,{values:i},!0)}minus(e){if(!this.isValid)return this;const t=_t.fromDurationLike(e);return this.plus(t.negate())}mapUnits(e){if(!this.isValid)return this;const t={};for(const i of Object.keys(this.values))t[i]=Zg(e(this.values[i],i));return Xi(this,{values:t},!0)}get(e){return this[_t.normalizeUnit(e)]}set(e){if(!this.isValid)return this;const t={...this.values,...is(e,_t.normalizeUnit)};return Xi(this,{values:t})}reconfigure({locale:e,numberingSystem:t,conversionAccuracy:i}={}){const o=this.loc.clone({locale:e,numberingSystem:t}),r={loc:o};return i&&(r.conversionAccuracy=i),Xi(this,r)}as(e){return this.isValid?this.shiftTo(e).get(e):NaN}normalize(){if(!this.isValid)return this;const e=this.toObject();return h2(this.matrix,e),Xi(this,{values:e},!0)}shiftTo(...e){if(!this.isValid)return this;if(e.length===0)return this;e=e.map(l=>_t.normalizeUnit(l));const t={},i={},o=this.toObject();let r;for(const l of io)if(e.indexOf(l)>=0){r=l;let s=0;for(const f in i)s+=this.matrix[f][l]*i[f],i[f]=0;uo(o[l])&&(s+=o[l]);const a=Math.trunc(s);t[l]=a,i[l]=(s*1e3-a*1e3)/1e3;for(const f in o)io.indexOf(f)>io.indexOf(l)&&d0(this.matrix,o,f,t,l)}else uo(o[l])&&(i[l]=o[l]);for(const l in i)i[l]!==0&&(t[r]+=l===r?i[l]:i[l]/this.matrix[r][l]);return Xi(this,{values:t},!0).normalize()}negate(){if(!this.isValid)return this;const e={};for(const t of Object.keys(this.values))e[t]=this.values[t]===0?0:-this.values[t];return Xi(this,{values:e},!0)}get years(){return this.isValid?this.values.years||0:NaN}get quarters(){return this.isValid?this.values.quarters||0:NaN}get months(){return this.isValid?this.values.months||0:NaN}get weeks(){return this.isValid?this.values.weeks||0:NaN}get days(){return this.isValid?this.values.days||0:NaN}get hours(){return this.isValid?this.values.hours||0:NaN}get minutes(){return this.isValid?this.values.minutes||0:NaN}get seconds(){return this.isValid?this.values.seconds||0:NaN}get milliseconds(){return this.isValid?this.values.milliseconds||0:NaN}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}equals(e){if(!this.isValid||!e.isValid||!this.loc.equals(e.loc))return!1;function t(i,o){return i===void 0||i===0?o===void 0||o===0:i===o}for(const i of io)if(!t(this.values[i],e.values[i]))return!1;return!0}}const rr="Invalid Interval";function m2(n,e){return!n||!n.isValid?It.invalid("missing or invalid start"):!e||!e.isValid?It.invalid("missing or invalid end"):ee:!1}isBefore(e){return this.isValid?this.e<=e:!1}contains(e){return this.isValid?this.s<=e&&this.e>e:!1}set({start:e,end:t}={}){return this.isValid?It.fromDateTimes(e||this.s,t||this.e):this}splitAt(...e){if(!this.isValid)return[];const t=e.map(ar).filter(l=>this.contains(l)).sort(),i=[];let{s:o}=this,r=0;for(;o+this.e?this.e:l;i.push(It.fromDateTimes(o,s)),o=s,r+=1}return i}splitBy(e){const t=_t.fromDurationLike(e);if(!this.isValid||!t.isValid||t.as("milliseconds")===0)return[];let{s:i}=this,o=1,r;const l=[];for(;ia*o));r=+s>+this.e?this.e:s,l.push(It.fromDateTimes(i,r)),i=r,o+=1}return l}divideEqually(e){return this.isValid?this.splitBy(this.length()/e).slice(0,e):[]}overlaps(e){return this.e>e.s&&this.s=e.e:!1}equals(e){return!this.isValid||!e.isValid?!1:this.s.equals(e.s)&&this.e.equals(e.e)}intersection(e){if(!this.isValid)return this;const t=this.s>e.s?this.s:e.s,i=this.e=i?null:It.fromDateTimes(t,i)}union(e){if(!this.isValid)return this;const t=this.se.e?this.e:e.e;return It.fromDateTimes(t,i)}static merge(e){const[t,i]=e.sort((o,r)=>o.s-r.s).reduce(([o,r],l)=>r?r.overlaps(l)||r.abutsStart(l)?[o,r.union(l)]:[o.concat([r]),l]:[o,l],[[],null]);return i&&t.push(i),t}static xor(e){let t=null,i=0;const o=[],r=e.map(a=>[{time:a.s,type:"s"},{time:a.e,type:"e"}]),l=Array.prototype.concat(...r),s=l.sort((a,f)=>a.time-f.time);for(const a of s)i+=a.type==="s"?1:-1,i===1?t=a.time:(t&&+t!=+a.time&&o.push(It.fromDateTimes(t,a.time)),t=null);return It.merge(o)}difference(...e){return It.xor([this].concat(e)).map(t=>this.intersection(t)).filter(t=>t&&!t.isEmpty())}toString(){return this.isValid?`[${this.s.toISO()} \u2013 ${this.e.toISO()})`:rr}toISO(e){return this.isValid?`${this.s.toISO(e)}/${this.e.toISO(e)}`:rr}toISODate(){return this.isValid?`${this.s.toISODate()}/${this.e.toISODate()}`:rr}toISOTime(e){return this.isValid?`${this.s.toISOTime(e)}/${this.e.toISOTime(e)}`:rr}toFormat(e,{separator:t=" \u2013 "}={}){return this.isValid?`${this.s.toFormat(e)}${t}${this.e.toFormat(e)}`:rr}toDuration(e,t){return this.isValid?this.e.diff(this.s,e,t):_t.invalid(this.invalidReason)}mapEndpoints(e){return It.fromDateTimes(e(this.s),e(this.e))}}class _l{static hasDST(e=Yt.defaultZone){const t=Qe.now().setZone(e).set({month:12});return!e.isUniversal&&t.offset!==t.set({month:6}).offset}static isValidIANAZone(e){return Si.isValidZone(e)}static normalizeZone(e){return Li(e,Yt.defaultZone)}static months(e="long",{locale:t=null,numberingSystem:i=null,locObj:o=null,outputCalendar:r="gregory"}={}){return(o||Lt.create(t,i,r)).months(e)}static monthsFormat(e="long",{locale:t=null,numberingSystem:i=null,locObj:o=null,outputCalendar:r="gregory"}={}){return(o||Lt.create(t,i,r)).months(e,!0)}static weekdays(e="long",{locale:t=null,numberingSystem:i=null,locObj:o=null}={}){return(o||Lt.create(t,i,null)).weekdays(e)}static weekdaysFormat(e="long",{locale:t=null,numberingSystem:i=null,locObj:o=null}={}){return(o||Lt.create(t,i,null)).weekdays(e,!0)}static meridiems({locale:e=null}={}){return Lt.create(e).meridiems()}static eras(e="short",{locale:t=null}={}){return Lt.create(t,null,"gregory").eras(e)}static features(){return{relative:Kg()}}}function du(n,e){const t=o=>o.toUTC(0,{keepLocalTime:!0}).startOf("day").valueOf(),i=t(e)-t(n);return Math.floor(_t.fromMillis(i).as("days"))}function b2(n,e,t){const i=[["years",(s,a)=>a.year-s.year],["quarters",(s,a)=>a.quarter-s.quarter],["months",(s,a)=>a.month-s.month+(a.year-s.year)*12],["weeks",(s,a)=>{const f=du(s,a);return(f-f%7)/7}],["days",du]],o={};let r,l;for(const[s,a]of i)if(t.indexOf(s)>=0){r=s;let f=a(n,e);l=n.plus({[s]:f}),l>e?(n=n.plus({[s]:f-1}),f-=1):n=l,o[s]=f}return[n,o,l,r]}function g2(n,e,t,i){let[o,r,l,s]=b2(n,e,t);const a=e-o,f=t.filter(u=>["hours","minutes","seconds","milliseconds"].indexOf(u)>=0);f.length===0&&(l0?_t.fromMillis(a,i).shiftTo(...f).plus(c):c}const jf={arab:"[\u0660-\u0669]",arabext:"[\u06F0-\u06F9]",bali:"[\u1B50-\u1B59]",beng:"[\u09E6-\u09EF]",deva:"[\u0966-\u096F]",fullwide:"[\uFF10-\uFF19]",gujr:"[\u0AE6-\u0AEF]",hanidec:"[\u3007|\u4E00|\u4E8C|\u4E09|\u56DB|\u4E94|\u516D|\u4E03|\u516B|\u4E5D]",khmr:"[\u17E0-\u17E9]",knda:"[\u0CE6-\u0CEF]",laoo:"[\u0ED0-\u0ED9]",limb:"[\u1946-\u194F]",mlym:"[\u0D66-\u0D6F]",mong:"[\u1810-\u1819]",mymr:"[\u1040-\u1049]",orya:"[\u0B66-\u0B6F]",tamldec:"[\u0BE6-\u0BEF]",telu:"[\u0C66-\u0C6F]",thai:"[\u0E50-\u0E59]",tibt:"[\u0F20-\u0F29]",latn:"\\d"},pu={arab:[1632,1641],arabext:[1776,1785],bali:[6992,7001],beng:[2534,2543],deva:[2406,2415],fullwide:[65296,65303],gujr:[2790,2799],khmr:[6112,6121],knda:[3302,3311],laoo:[3792,3801],limb:[6470,6479],mlym:[3430,3439],mong:[6160,6169],mymr:[4160,4169],orya:[2918,2927],tamldec:[3046,3055],telu:[3174,3183],thai:[3664,3673],tibt:[3872,3881]},_2=jf.hanidec.replace(/[\[|\]]/g,"").split("");function v2(n){let e=parseInt(n,10);if(isNaN(e)){e="";for(let t=0;t=r&&i<=l&&(e+=i-r)}}return parseInt(e,10)}else return e}function Kn({numberingSystem:n},e=""){return new RegExp(`${jf[n||"latn"]}${e}`)}const y2="missing Intl.DateTimeFormat.formatToParts support";function kt(n,e=t=>t){return{regex:n,deser:([t])=>e(v2(t))}}const k2=String.fromCharCode(160),p0=`[ ${k2}]`,h0=new RegExp(p0,"g");function w2(n){return n.replace(/\./g,"\\.?").replace(h0,p0)}function hu(n){return n.replace(/\./g,"").replace(h0," ").toLowerCase()}function Jn(n,e){return n===null?null:{regex:RegExp(n.map(w2).join("|")),deser:([t])=>n.findIndex(i=>hu(t)===hu(i))+e}}function mu(n,e){return{regex:n,deser:([,t,i])=>Ms(t,i),groups:e}}function ia(n){return{regex:n,deser:([e])=>e}}function S2(n){return n.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function C2(n,e){const t=Kn(e),i=Kn(e,"{2}"),o=Kn(e,"{3}"),r=Kn(e,"{4}"),l=Kn(e,"{6}"),s=Kn(e,"{1,2}"),a=Kn(e,"{1,3}"),f=Kn(e,"{1,6}"),c=Kn(e,"{1,9}"),u=Kn(e,"{2,4}"),d=Kn(e,"{4,6}"),h=_=>({regex:RegExp(S2(_.val)),deser:([y])=>y,literal:!0}),v=(_=>{if(n.literal)return h(_);switch(_.val){case"G":return Jn(e.eras("short",!1),0);case"GG":return Jn(e.eras("long",!1),0);case"y":return kt(f);case"yy":return kt(u,Va);case"yyyy":return kt(r);case"yyyyy":return kt(d);case"yyyyyy":return kt(l);case"M":return kt(s);case"MM":return kt(i);case"MMM":return Jn(e.months("short",!0,!1),1);case"MMMM":return Jn(e.months("long",!0,!1),1);case"L":return kt(s);case"LL":return kt(i);case"LLL":return Jn(e.months("short",!1,!1),1);case"LLLL":return Jn(e.months("long",!1,!1),1);case"d":return kt(s);case"dd":return kt(i);case"o":return kt(a);case"ooo":return kt(o);case"HH":return kt(i);case"H":return kt(s);case"hh":return kt(i);case"h":return kt(s);case"mm":return kt(i);case"m":return kt(s);case"q":return kt(s);case"qq":return kt(i);case"s":return kt(s);case"ss":return kt(i);case"S":return kt(a);case"SSS":return kt(o);case"u":return ia(c);case"uu":return ia(s);case"uuu":return kt(t);case"a":return Jn(e.meridiems(),0);case"kkkk":return kt(r);case"kk":return kt(u,Va);case"W":return kt(s);case"WW":return kt(i);case"E":case"c":return kt(t);case"EEE":return Jn(e.weekdays("short",!1,!1),1);case"EEEE":return Jn(e.weekdays("long",!1,!1),1);case"ccc":return Jn(e.weekdays("short",!0,!1),1);case"cccc":return Jn(e.weekdays("long",!0,!1),1);case"Z":case"ZZ":return mu(new RegExp(`([+-]${s.source})(?::(${i.source}))?`),2);case"ZZZ":return mu(new RegExp(`([+-]${s.source})(${i.source})?`),2);case"z":return ia(/[a-z_+-/]{1,256}?/i);default:return h(_)}})(n)||{invalidReason:y2};return v.token=n,v}const x2={year:{"2-digit":"yy",numeric:"yyyyy"},month:{numeric:"M","2-digit":"MM",short:"MMM",long:"MMMM"},day:{numeric:"d","2-digit":"dd"},weekday:{short:"EEE",long:"EEEE"},dayperiod:"a",dayPeriod:"a",hour:{numeric:"h","2-digit":"hh"},minute:{numeric:"m","2-digit":"mm"},second:{numeric:"s","2-digit":"ss"}};function M2(n,e,t){const{type:i,value:o}=n;if(i==="literal")return{literal:!0,val:o};const r=t[i];let l=x2[i];if(typeof l=="object"&&(l=l[r]),l)return{literal:!1,val:l}}function $2(n){return[`^${n.map(t=>t.regex).reduce((t,i)=>`${t}(${i.source})`,"")}$`,n]}function A2(n,e,t){const i=n.match(e);if(i){const o={};let r=1;for(const l in t)if(Uo(t,l)){const s=t[l],a=s.groups?s.groups+1:1;!s.literal&&s.token&&(o[s.token.val[0]]=s.deser(i.slice(r,r+a))),r+=a}return[i,o]}else return[i,{}]}function D2(n){const e=r=>{switch(r){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":case"H":return"hour";case"d":return"day";case"o":return"ordinal";case"L":case"M":return"month";case"y":return"year";case"E":case"c":return"weekday";case"W":return"weekNumber";case"k":return"weekYear";case"q":return"quarter";default:return null}};let t=null,i;return pt(n.z)||(t=Si.create(n.z)),pt(n.Z)||(t||(t=new dn(n.Z)),i=n.Z),pt(n.q)||(n.M=(n.q-1)*3+1),pt(n.h)||(n.h<12&&n.a===1?n.h+=12:n.h===12&&n.a===0&&(n.h=0)),n.G===0&&n.y&&(n.y=-n.y),pt(n.u)||(n.S=Ef(n.u)),[Object.keys(n).reduce((r,l)=>{const s=e(l);return s&&(r[s]=n[l]),r},{}),t,i]}let oa=null;function O2(){return oa||(oa=Qe.fromMillis(1555555555555)),oa}function T2(n,e){if(n.literal)return n;const t=vn.macroTokenToFormatOpts(n.val);if(!t)return n;const r=vn.create(e,t).formatDateTimeParts(O2()).map(l=>M2(l,e,t));return r.includes(void 0)?n:r}function E2(n,e){return Array.prototype.concat(...n.map(t=>T2(t,e)))}function m0(n,e,t){const i=E2(vn.parseFormat(t),n),o=i.map(l=>C2(l,n)),r=o.find(l=>l.invalidReason);if(r)return{input:e,tokens:i,invalidReason:r.invalidReason};{const[l,s]=$2(o),a=RegExp(l,"i"),[f,c]=A2(e,a,s),[u,d,h]=c?D2(c):[null,null,void 0];if(Uo(c,"a")&&Uo(c,"H"))throw new _r("Can't include meridiem when specifying 24-hour format");return{input:e,tokens:i,regex:a,rawMatches:f,matches:c,result:u,zone:d,specificOffset:h}}}function P2(n,e,t){const{result:i,zone:o,specificOffset:r,invalidReason:l}=m0(n,e,t);return[i,o,r,l]}const b0=[0,31,59,90,120,151,181,212,243,273,304,334],g0=[0,31,60,91,121,152,182,213,244,274,305,335];function Hn(n,e){return new Zn("unit out of range",`you specified ${e} (of type ${typeof e}) as a ${n}, which is invalid`)}function _0(n,e,t){const i=new Date(Date.UTC(n,e-1,t));n<100&&n>=0&&i.setUTCFullYear(i.getUTCFullYear()-1900);const o=i.getUTCDay();return o===0?7:o}function v0(n,e,t){return t+(Wr(n)?g0:b0)[e-1]}function y0(n,e){const t=Wr(n)?g0:b0,i=t.findIndex(r=>rns(e)?(s=e+1,l=1):s=e,{weekYear:s,weekNumber:l,weekday:r,...$s(n)}}function bu(n){const{weekYear:e,weekNumber:t,weekday:i}=n,o=_0(e,1,4),r=Cr(e);let l=t*7+i-o-3,s;l<1?(s=e-1,l+=Cr(s)):l>r?(s=e+1,l-=Cr(e)):s=e;const{month:a,day:f}=y0(s,l);return{year:s,month:a,day:f,...$s(n)}}function ra(n){const{year:e,month:t,day:i}=n,o=v0(e,t,i);return{year:e,ordinal:o,...$s(n)}}function gu(n){const{year:e,ordinal:t}=n,{month:i,day:o}=y0(e,t);return{year:e,month:i,day:o,...$s(n)}}function F2(n){const e=xs(n.weekYear),t=ki(n.weekNumber,1,ns(n.weekYear)),i=ki(n.weekday,1,7);return e?t?i?!1:Hn("weekday",n.weekday):Hn("week",n.week):Hn("weekYear",n.weekYear)}function L2(n){const e=xs(n.year),t=ki(n.ordinal,1,Cr(n.year));return e?t?!1:Hn("ordinal",n.ordinal):Hn("year",n.year)}function k0(n){const e=xs(n.year),t=ki(n.month,1,12),i=ki(n.day,1,ts(n.year,n.month));return e?t?i?!1:Hn("day",n.day):Hn("month",n.month):Hn("year",n.year)}function w0(n){const{hour:e,minute:t,second:i,millisecond:o}=n,r=ki(e,0,23)||e===24&&t===0&&i===0&&o===0,l=ki(t,0,59),s=ki(i,0,59),a=ki(o,0,999);return r?l?s?a?!1:Hn("millisecond",o):Hn("second",i):Hn("minute",t):Hn("hour",e)}const la="Invalid DateTime",_u=864e13;function vl(n){return new Zn("unsupported zone",`the zone "${n.name}" is not supported`)}function sa(n){return n.weekData===null&&(n.weekData=Ga(n.c)),n.weekData}function lr(n,e){const t={ts:n.ts,zone:n.zone,c:n.c,o:n.o,loc:n.loc,invalid:n.invalid};return new Qe({...t,...e,old:t})}function S0(n,e,t){let i=n-e*60*1e3;const o=t.offset(i);if(e===o)return[i,e];i-=(o-e)*60*1e3;const r=t.offset(i);return o===r?[i,o]:[n-Math.min(o,r)*60*1e3,Math.max(o,r)]}function vu(n,e){n+=e*60*1e3;const t=new Date(n);return{year:t.getUTCFullYear(),month:t.getUTCMonth()+1,day:t.getUTCDate(),hour:t.getUTCHours(),minute:t.getUTCMinutes(),second:t.getUTCSeconds(),millisecond:t.getUTCMilliseconds()}}function Yl(n,e,t){return S0(Ff(n),e,t)}function yu(n,e){const t=n.o,i=n.c.year+Math.trunc(e.years),o=n.c.month+Math.trunc(e.months)+Math.trunc(e.quarters)*3,r={...n.c,year:i,month:o,day:Math.min(n.c.day,ts(i,o))+Math.trunc(e.days)+Math.trunc(e.weeks)*7},l=_t.fromObject({years:e.years-Math.trunc(e.years),quarters:e.quarters-Math.trunc(e.quarters),months:e.months-Math.trunc(e.months),weeks:e.weeks-Math.trunc(e.weeks),days:e.days-Math.trunc(e.days),hours:e.hours,minutes:e.minutes,seconds:e.seconds,milliseconds:e.milliseconds}).as("milliseconds"),s=Ff(r);let[a,f]=S0(s,t,n.zone);return l!==0&&(a+=l,f=n.zone.offset(a)),{ts:a,o:f}}function sr(n,e,t,i,o,r){const{setZone:l,zone:s}=t;if(n&&Object.keys(n).length!==0){const a=e||s,f=Qe.fromObject(n,{...t,zone:a,specificOffset:r});return l?f:f.setZone(s)}else return Qe.invalid(new Zn("unparsable",`the input "${o}" can't be parsed as ${i}`))}function yl(n,e,t=!0){return n.isValid?vn.create(Lt.create("en-US"),{allowZ:t,forceSimple:!0}).formatDateTimeFromString(n,e):null}function aa(n,e){const t=n.c.year>9999||n.c.year<0;let i="";return t&&n.c.year>=0&&(i+="+"),i+=Bt(n.c.year,t?6:4),e?(i+="-",i+=Bt(n.c.month),i+="-",i+=Bt(n.c.day)):(i+=Bt(n.c.month),i+=Bt(n.c.day)),i}function ku(n,e,t,i,o,r){let l=Bt(n.c.hour);return e?(l+=":",l+=Bt(n.c.minute),(n.c.second!==0||!t)&&(l+=":")):l+=Bt(n.c.minute),(n.c.second!==0||!t)&&(l+=Bt(n.c.second),(n.c.millisecond!==0||!i)&&(l+=".",l+=Bt(n.c.millisecond,3))),o&&(n.isOffsetFixed&&n.offset===0&&!r?l+="Z":n.o<0?(l+="-",l+=Bt(Math.trunc(-n.o/60)),l+=":",l+=Bt(Math.trunc(-n.o%60))):(l+="+",l+=Bt(Math.trunc(n.o/60)),l+=":",l+=Bt(Math.trunc(n.o%60)))),r&&(l+="["+n.zone.ianaName+"]"),l}const C0={month:1,day:1,hour:0,minute:0,second:0,millisecond:0},I2={weekNumber:1,weekday:1,hour:0,minute:0,second:0,millisecond:0},R2={ordinal:1,hour:0,minute:0,second:0,millisecond:0},x0=["year","month","day","hour","minute","second","millisecond"],N2=["weekYear","weekNumber","weekday","hour","minute","second","millisecond"],j2=["year","ordinal","hour","minute","second","millisecond"];function wu(n){const e={year:"year",years:"year",month:"month",months:"month",day:"day",days:"day",hour:"hour",hours:"hour",minute:"minute",minutes:"minute",quarter:"quarter",quarters:"quarter",second:"second",seconds:"second",millisecond:"millisecond",milliseconds:"millisecond",weekday:"weekday",weekdays:"weekday",weeknumber:"weekNumber",weeksnumber:"weekNumber",weeknumbers:"weekNumber",weekyear:"weekYear",weekyears:"weekYear",ordinal:"ordinal"}[n.toLowerCase()];if(!e)throw new Dg(n);return e}function Su(n,e){const t=Li(e.zone,Yt.defaultZone),i=Lt.fromObject(e),o=Yt.now();let r,l;if(pt(n.year))r=o;else{for(const f of x0)pt(n[f])&&(n[f]=C0[f]);const s=k0(n)||w0(n);if(s)return Qe.invalid(s);const a=t.offset(o);[r,l]=Yl(n,a,t)}return new Qe({ts:r,zone:t,loc:i,o:l})}function Cu(n,e,t){const i=pt(t.round)?!0:t.round,o=(l,s)=>(l=Pf(l,i||t.calendary?0:2,!0),e.loc.clone(t).relFormatter(t).format(l,s)),r=l=>t.calendary?e.hasSame(n,l)?0:e.startOf(l).diff(n.startOf(l),l).get(l):e.diff(n,l).get(l);if(t.unit)return o(r(t.unit),t.unit);for(const l of t.units){const s=r(l);if(Math.abs(s)>=1)return o(s,l)}return o(n>e?-0:0,t.units[t.units.length-1])}function xu(n){let e={},t;return n.length>0&&typeof n[n.length-1]=="object"?(e=n[n.length-1],t=Array.from(n).slice(0,n.length-1)):t=Array.from(n),[e,t]}class Qe{constructor(e){const t=e.zone||Yt.defaultZone;let i=e.invalid||(Number.isNaN(e.ts)?new Zn("invalid input"):null)||(t.isValid?null:vl(t));this.ts=pt(e.ts)?Yt.now():e.ts;let o=null,r=null;if(!i)if(e.old&&e.old.ts===this.ts&&e.old.zone.equals(t))[o,r]=[e.old.c,e.old.o];else{const s=t.offset(this.ts);o=vu(this.ts,s),i=Number.isNaN(o.year)?new Zn("invalid input"):null,o=i?null:o,r=i?null:s}this._zone=t,this.loc=e.loc||Lt.create(),this.invalid=i,this.weekData=null,this.c=o,this.o=r,this.isLuxonDateTime=!0}static now(){return new Qe({})}static local(){const[e,t]=xu(arguments),[i,o,r,l,s,a,f]=t;return Su({year:i,month:o,day:r,hour:l,minute:s,second:a,millisecond:f},e)}static utc(){const[e,t]=xu(arguments),[i,o,r,l,s,a,f]=t;return e.zone=dn.utcInstance,Su({year:i,month:o,day:r,hour:l,minute:s,second:a,millisecond:f},e)}static fromJSDate(e,t={}){const i=Yv(e)?e.valueOf():NaN;if(Number.isNaN(i))return Qe.invalid("invalid input");const o=Li(t.zone,Yt.defaultZone);return o.isValid?new Qe({ts:i,zone:o,loc:Lt.fromObject(t)}):Qe.invalid(vl(o))}static fromMillis(e,t={}){if(uo(e))return e<-_u||e>_u?Qe.invalid("Timestamp out of range"):new Qe({ts:e,zone:Li(t.zone,Yt.defaultZone),loc:Lt.fromObject(t)});throw new jn(`fromMillis requires a numerical input, but received a ${typeof e} with value ${e}`)}static fromSeconds(e,t={}){if(uo(e))return new Qe({ts:e*1e3,zone:Li(t.zone,Yt.defaultZone),loc:Lt.fromObject(t)});throw new jn("fromSeconds requires a numerical input")}static fromObject(e,t={}){e=e||{};const i=Li(t.zone,Yt.defaultZone);if(!i.isValid)return Qe.invalid(vl(i));const o=Yt.now(),r=pt(t.specificOffset)?i.offset(o):t.specificOffset,l=is(e,wu),s=!pt(l.ordinal),a=!pt(l.year),f=!pt(l.month)||!pt(l.day),c=a||f,u=l.weekYear||l.weekNumber,d=Lt.fromObject(t);if((c||s)&&u)throw new _r("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(f&&s)throw new _r("Can't mix ordinal dates with month/day");const h=u||l.weekday&&!c;let b,v,_=vu(o,r);h?(b=N2,v=I2,_=Ga(_)):s?(b=j2,v=R2,_=ra(_)):(b=x0,v=C0);let y=!1;for(const D of b){const E=l[D];pt(E)?y?l[D]=v[D]:l[D]=_[D]:y=!0}const S=h?F2(l):s?L2(l):k0(l),C=S||w0(l);if(C)return Qe.invalid(C);const x=h?bu(l):s?gu(l):l,[M,A]=Yl(x,r,i),O=new Qe({ts:M,zone:i,o:A,loc:d});return l.weekday&&c&&e.weekday!==O.weekday?Qe.invalid("mismatched weekday",`you can't specify both a weekday of ${l.weekday} and a date of ${O.toISO()}`):O}static fromISO(e,t={}){const[i,o]=Qy(e);return sr(i,o,t,"ISO 8601",e)}static fromRFC2822(e,t={}){const[i,o]=e2(e);return sr(i,o,t,"RFC 2822",e)}static fromHTTP(e,t={}){const[i,o]=t2(e);return sr(i,o,t,"HTTP",t)}static fromFormat(e,t,i={}){if(pt(e)||pt(t))throw new jn("fromFormat requires an input string and a format");const{locale:o=null,numberingSystem:r=null}=i,l=Lt.fromOpts({locale:o,numberingSystem:r,defaultToEN:!0}),[s,a,f,c]=P2(l,e,t);return c?Qe.invalid(c):sr(s,a,i,`format ${t}`,e,f)}static fromString(e,t,i={}){return Qe.fromFormat(e,t,i)}static fromSQL(e,t={}){const[i,o]=a2(e);return sr(i,o,t,"SQL",e)}static invalid(e,t=null){if(!e)throw new jn("need to specify a reason the DateTime is invalid");const i=e instanceof Zn?e:new Zn(e,t);if(Yt.throwOnInvalid)throw new Hv(i);return new Qe({invalid:i})}static isDateTime(e){return e&&e.isLuxonDateTime||!1}get(e){return this[e]}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}get outputCalendar(){return this.isValid?this.loc.outputCalendar:null}get zone(){return this._zone}get zoneName(){return this.isValid?this.zone.name:null}get year(){return this.isValid?this.c.year:NaN}get quarter(){return this.isValid?Math.ceil(this.c.month/3):NaN}get month(){return this.isValid?this.c.month:NaN}get day(){return this.isValid?this.c.day:NaN}get hour(){return this.isValid?this.c.hour:NaN}get minute(){return this.isValid?this.c.minute:NaN}get second(){return this.isValid?this.c.second:NaN}get millisecond(){return this.isValid?this.c.millisecond:NaN}get weekYear(){return this.isValid?sa(this).weekYear:NaN}get weekNumber(){return this.isValid?sa(this).weekNumber:NaN}get weekday(){return this.isValid?sa(this).weekday:NaN}get ordinal(){return this.isValid?ra(this.c).ordinal:NaN}get monthShort(){return this.isValid?_l.months("short",{locObj:this.loc})[this.month-1]:null}get monthLong(){return this.isValid?_l.months("long",{locObj:this.loc})[this.month-1]:null}get weekdayShort(){return this.isValid?_l.weekdays("short",{locObj:this.loc})[this.weekday-1]:null}get weekdayLong(){return this.isValid?_l.weekdays("long",{locObj:this.loc})[this.weekday-1]:null}get offset(){return this.isValid?+this.o:NaN}get offsetNameShort(){return this.isValid?this.zone.offsetName(this.ts,{format:"short",locale:this.locale}):null}get offsetNameLong(){return this.isValid?this.zone.offsetName(this.ts,{format:"long",locale:this.locale}):null}get isOffsetFixed(){return this.isValid?this.zone.isUniversal:null}get isInDST(){return this.isOffsetFixed?!1:this.offset>this.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset}get isInLeapYear(){return Wr(this.year)}get daysInMonth(){return ts(this.year,this.month)}get daysInYear(){return this.isValid?Cr(this.year):NaN}get weeksInWeekYear(){return this.isValid?ns(this.weekYear):NaN}resolvedLocaleOptions(e={}){const{locale:t,numberingSystem:i,calendar:o}=vn.create(this.loc.clone(e),e).resolvedOptions(this);return{locale:t,numberingSystem:i,outputCalendar:o}}toUTC(e=0,t={}){return this.setZone(dn.instance(e),t)}toLocal(){return this.setZone(Yt.defaultZone)}setZone(e,{keepLocalTime:t=!1,keepCalendarTime:i=!1}={}){if(e=Li(e,Yt.defaultZone),e.equals(this.zone))return this;if(e.isValid){let o=this.ts;if(t||i){const r=e.offset(this.ts),l=this.toObject();[o]=Yl(l,r,e)}return lr(this,{ts:o,zone:e})}else return Qe.invalid(vl(e))}reconfigure({locale:e,numberingSystem:t,outputCalendar:i}={}){const o=this.loc.clone({locale:e,numberingSystem:t,outputCalendar:i});return lr(this,{loc:o})}setLocale(e){return this.reconfigure({locale:e})}set(e){if(!this.isValid)return this;const t=is(e,wu),i=!pt(t.weekYear)||!pt(t.weekNumber)||!pt(t.weekday),o=!pt(t.ordinal),r=!pt(t.year),l=!pt(t.month)||!pt(t.day),s=r||l,a=t.weekYear||t.weekNumber;if((s||o)&&a)throw new _r("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(l&&o)throw new _r("Can't mix ordinal dates with month/day");let f;i?f=bu({...Ga(this.c),...t}):pt(t.ordinal)?(f={...this.toObject(),...t},pt(t.day)&&(f.day=Math.min(ts(f.year,f.month),f.day))):f=gu({...ra(this.c),...t});const[c,u]=Yl(f,this.o,this.zone);return lr(this,{ts:c,o:u})}plus(e){if(!this.isValid)return this;const t=_t.fromDurationLike(e);return lr(this,yu(this,t))}minus(e){if(!this.isValid)return this;const t=_t.fromDurationLike(e).negate();return lr(this,yu(this,t))}startOf(e){if(!this.isValid)return this;const t={},i=_t.normalizeUnit(e);switch(i){case"years":t.month=1;case"quarters":case"months":t.day=1;case"weeks":case"days":t.hour=0;case"hours":t.minute=0;case"minutes":t.second=0;case"seconds":t.millisecond=0;break}if(i==="weeks"&&(t.weekday=1),i==="quarters"){const o=Math.ceil(this.month/3);t.month=(o-1)*3+1}return this.set(t)}endOf(e){return this.isValid?this.plus({[e]:1}).startOf(e).minus(1):this}toFormat(e,t={}){return this.isValid?vn.create(this.loc.redefaultToEN(t)).formatDateTimeFromString(this,e):la}toLocaleString(e=qa,t={}){return this.isValid?vn.create(this.loc.clone(t),e).formatDateTime(this):la}toLocaleParts(e={}){return this.isValid?vn.create(this.loc.clone(e),e).formatDateTimeParts(this):[]}toISO({format:e="extended",suppressSeconds:t=!1,suppressMilliseconds:i=!1,includeOffset:o=!0,extendedZone:r=!1}={}){if(!this.isValid)return null;const l=e==="extended";let s=aa(this,l);return s+="T",s+=ku(this,l,t,i,o,r),s}toISODate({format:e="extended"}={}){return this.isValid?aa(this,e==="extended"):null}toISOWeekDate(){return yl(this,"kkkk-'W'WW-c")}toISOTime({suppressMilliseconds:e=!1,suppressSeconds:t=!1,includeOffset:i=!0,includePrefix:o=!1,extendedZone:r=!1,format:l="extended"}={}){return this.isValid?(o?"T":"")+ku(this,l==="extended",t,e,i,r):null}toRFC2822(){return yl(this,"EEE, dd LLL yyyy HH:mm:ss ZZZ",!1)}toHTTP(){return yl(this.toUTC(),"EEE, dd LLL yyyy HH:mm:ss 'GMT'")}toSQLDate(){return this.isValid?aa(this,!0):null}toSQLTime({includeOffset:e=!0,includeZone:t=!1,includeOffsetSpace:i=!0}={}){let o="HH:mm:ss.SSS";return(t||e)&&(i&&(o+=" "),t?o+="z":e&&(o+="ZZ")),yl(this,o,!0)}toSQL(e={}){return this.isValid?`${this.toSQLDate()} ${this.toSQLTime(e)}`:null}toString(){return this.isValid?this.toISO():la}valueOf(){return this.toMillis()}toMillis(){return this.isValid?this.ts:NaN}toSeconds(){return this.isValid?this.ts/1e3:NaN}toUnixInteger(){return this.isValid?Math.floor(this.ts/1e3):NaN}toJSON(){return this.toISO()}toBSON(){return this.toJSDate()}toObject(e={}){if(!this.isValid)return{};const t={...this.c};return e.includeConfig&&(t.outputCalendar=this.outputCalendar,t.numberingSystem=this.loc.numberingSystem,t.locale=this.loc.locale),t}toJSDate(){return new Date(this.isValid?this.ts:NaN)}diff(e,t="milliseconds",i={}){if(!this.isValid||!e.isValid)return _t.invalid("created by diffing an invalid DateTime");const o={locale:this.locale,numberingSystem:this.numberingSystem,...i},r=Gv(t).map(_t.normalizeUnit),l=e.valueOf()>this.valueOf(),s=l?this:e,a=l?e:this,f=g2(s,a,r,o);return l?f.negate():f}diffNow(e="milliseconds",t={}){return this.diff(Qe.now(),e,t)}until(e){return this.isValid?It.fromDateTimes(this,e):this}hasSame(e,t){if(!this.isValid)return!1;const i=e.valueOf(),o=this.setZone(e.zone,{keepLocalTime:!0});return o.startOf(t)<=i&&i<=o.endOf(t)}equals(e){return this.isValid&&e.isValid&&this.valueOf()===e.valueOf()&&this.zone.equals(e.zone)&&this.loc.equals(e.loc)}toRelative(e={}){if(!this.isValid)return null;const t=e.base||Qe.fromObject({},{zone:this.zone}),i=e.padding?thist.valueOf(),Math.min)}static max(...e){if(!e.every(Qe.isDateTime))throw new jn("max requires all arguments be DateTimes");return nu(e,t=>t.valueOf(),Math.max)}static fromFormatExplain(e,t,i={}){const{locale:o=null,numberingSystem:r=null}=i,l=Lt.fromOpts({locale:o,numberingSystem:r,defaultToEN:!0});return m0(l,e,t)}static fromStringExplain(e,t,i={}){return Qe.fromFormatExplain(e,t,i)}static get DATE_SHORT(){return qa}static get DATE_MED(){return Og}static get DATE_MED_WITH_WEEKDAY(){return Bv}static get DATE_FULL(){return Tg}static get DATE_HUGE(){return Eg}static get TIME_SIMPLE(){return Pg}static get TIME_WITH_SECONDS(){return Fg}static get TIME_WITH_SHORT_OFFSET(){return Lg}static get TIME_WITH_LONG_OFFSET(){return Ig}static get TIME_24_SIMPLE(){return Rg}static get TIME_24_WITH_SECONDS(){return Ng}static get TIME_24_WITH_SHORT_OFFSET(){return jg}static get TIME_24_WITH_LONG_OFFSET(){return zg}static get DATETIME_SHORT(){return Hg}static get DATETIME_SHORT_WITH_SECONDS(){return qg}static get DATETIME_MED(){return Vg}static get DATETIME_MED_WITH_SECONDS(){return Bg}static get DATETIME_MED_WITH_WEEKDAY(){return Uv}static get DATETIME_FULL(){return Ug}static get DATETIME_FULL_WITH_SECONDS(){return Wg}static get DATETIME_HUGE(){return Yg}static get DATETIME_HUGE_WITH_SECONDS(){return Gg}}function ar(n){if(Qe.isDateTime(n))return n;if(n&&n.valueOf&&uo(n.valueOf()))return Qe.fromJSDate(n);if(n&&typeof n=="object")return Qe.fromObject(n);throw new jn(`Unknown datetime argument: ${n}, of type ${typeof n}`)}class B{static isObject(e){return e!==null&&typeof e=="object"&&e.constructor===Object}static isEmpty(e){return e===""||e===null||e==="00000000-0000-0000-0000-000000000000"||e==="0001-01-01T00:00:00Z"||e==="0001-01-01"||typeof e=="undefined"||Array.isArray(e)&&e.length===0||B.isObject(e)&&Object.keys(e).length===0}static isInput(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return t==="input"||t==="select"||t==="textarea"||e.isContentEditable}static isFocusable(e){let t=e&&e.tagName?e.tagName.toLowerCase():"";return B.isInput(e)||t==="button"||t==="a"||t==="details"||e.tabIndex>=0}static hasNonEmptyProps(e){for(let t in e)if(!B.isEmpty(e[t]))return!0;return!1}static isObjectArrayWithKeys(e,t){if(!Array.isArray(e)||typeof e[0]!="object")return!1;if(e.length==0)return!0;let i=B.toArray(t);for(let o of i)if(!(o in e[0]))return!1;return!0}static toArray(e,t=!1){return Array.isArray(e)?e:(t||e!==null)&&typeof e!="undefined"?[e]:[]}static inArray(e,t){for(let i=e.length-1;i>=0;i--)if(e[i]==t)return!0;return!1}static removeByValue(e,t){for(let i=e.length-1;i>=0;i--)if(e[i]==t){e.splice(i,1);break}}static pushUnique(e,t){B.inArray(e,t)||e.push(t)}static findByKey(e,t,i){for(let o in e)if(e[o][t]==i)return e[o];return null}static groupByKey(e,t){let i={};for(let o in e)i[e[o][t]]=i[e[o][t]]||[],i[e[o][t]].push(e[o]);return i}static removeByKey(e,t,i){for(let o in e)if(e[o][t]==i){e.splice(o,1);break}}static pushOrReplaceByKey(e,t,i="id"){for(let o=e.length-1;o>=0;o--)if(e[o][i]==t[i]){e[o]=t;return}e.push(t)}static filterDuplicatesByKey(e,t="id"){const i={};for(const o of e)i[o[t]]=o;return Object.values(i)}static filterRedactedProps(e,t="******"){const i=JSON.parse(JSON.stringify(e||{}));for(let o in i)typeof i[o]=="object"&&i[o]!==null?i[o]=B.filterRedactedProps(i[o],t):i[o]===t&&delete i[o];return i}static getNestedVal(e,t,i=null,o="."){let r=e||{},l=t.split(o);for(const s of l){if(!B.isObject(r)&&!Array.isArray(r)||typeof r[s]=="undefined")return i;r=r[s]}return r}static setByPath(e,t,i,o="."){if(!B.isObject(e)&&!Array.isArray(e)){console.warn("setByPath: data not an object or array.");return}let r=e,l=t.split(o),s=l.pop();for(const a of l)(!B.isObject(r)&&!Array.isArray(r)||!B.isObject(r[a])&&!Array.isArray(r[a]))&&(r[a]={}),r=r[a];r[s]=i}static deleteByPath(e,t,i="."){let o=e||{},r=t.split(i),l=r.pop();for(const s of r)(!B.isObject(o)&&!Array.isArray(o)||!B.isObject(o[s])&&!Array.isArray(o[s]))&&(o[s]={}),o=o[s];Array.isArray(o)?o.splice(l,1):B.isObject(o)&&delete o[l],r.length>0&&(Array.isArray(o)&&!o.length||B.isObject(o)&&!Object.keys(o).length)&&(Array.isArray(e)&&e.length>0||B.isObject(e)&&Object.keys(e).length>0)&&B.deleteByPath(e,r.join(i),i)}static randomString(e){e=e||10;let t="",i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let o=0;o{console.warn("Failed to copy.",i)})}static openInWindow(e,t,i,o){t=t||1024,i=i||768,o=o||"popup";let r=window.innerWidth,l=window.innerHeight;t=t>r?r:t,i=i>l?l:i;let s=r/2-t/2,a=l/2-i/2;return window.open(e,o,"width="+t+",height="+i+",top="+a+",left="+s+",resizable,menubar=no")}static getQueryString(e){let t=e.indexOf("?");if(t<0)return"";let i=e.indexOf("#");return e.substring(t+1,i>t?i:e.length)}static getQueryParams(e){let t={},i=B.getQueryString(e).split("&");for(let o in i){let r=i[o].split("=");if(r.length===2){let l=decodeURIComponent(r[1]);if(l.startsWith("{")||l.startsWith("["))try{l=JSON.parse(l)}catch{}t[decodeURIComponent(r[0])]=l}}return t}static setQueryParams(e,t,i=!0){let o=B.getQueryString(e),r=i&&o?B.getQueryParams(e):{},l=Object.assign(r,t),s="";for(let a in l)B.isEmpty(l[a])||(s&&(s+="&"),s+=encodeURIComponent(a)+"=",B.isObject(l[a])?s+=encodeURIComponent(JSON.stringify(l[a])):s+=encodeURIComponent(l[a]));return s=s?"?"+s:"",B.isEmpty(o)?e+s:e.replace("?"+o,s)}static replaceClientQueryParams(e){let t=B.setQueryParams(window.location.href,e);window.location.replace(t)}static getJWTPayload(e){const t=(e||"").split(".")[1]||"";if(t==="")return{};try{const i=decodeURIComponent(atob(t));return JSON.parse(i)||{}}catch(i){console.warn("Failed to parse JWT payload data.",i)}return{}}static hasImageExtension(e){return/\.jpg|\.jpeg|\.png|\.svg|\.webp|\.avif$/.test(e)}static checkImageUrl(e){return new Promise((t,i)=>{const o=new Image;o.onload=function(){return t(!0)},o.onerror=function(r){return i(r)},o.src=e})}static generateThumb(e,t=100,i=100){return new Promise(o=>{let r=new FileReader;r.onload=function(l){let s=new Image;s.onload=function(){let a=document.createElement("canvas"),f=a.getContext("2d"),c=s.width,u=s.height;return a.width=t,a.height=i,f.drawImage(s,c>u?(c-u)/2:0,0,c>u?u:c,c>u?u:c,0,0,t,i),o(a.toDataURL(e.type))},s.src=l.target.result},r.readAsDataURL(e)})}static setDocumentTitle(e,t="PocketBase"){let i=[];B.isEmpty(e)||i.push(e.trim()),B.isEmpty(t)||i.push(t.trim()),document.title=i.join(" - ")}static addValueToFormData(e,t,i){if(typeof i!="undefined")if(B.isEmpty(i))e.append(t,"");else if(Array.isArray(i))for(const o of i)B.addValueToFormData(e,t,o);else i instanceof File?e.append(t,i):i instanceof Date?e.append(t,i.toISOString()):B.isObject(i)?e.append(t,JSON.stringify(i)):e.append(t,""+i)}static defaultFlatpickrOptions(){return{dateFormat:"Y-m-d H:i:S",disableMobile:!0,allowInput:!0,enableTime:!0,time_24hr:!0,locale:{firstDayOfWeek:1}}}static dummyCollectionRecord(e){var o,r,l,s,a;const t=(e==null?void 0:e.schema)||[],i={"@collectionId":e==null?void 0:e.id,"@collectionName":e==null?void 0:e.name,id:"RECORD_ID",created:"2022-01-01 01:00:00",updated:"2022-01-01 23:59:59"};for(const f of t){let c=null;f.type==="number"?c=123:f.type==="date"?c="2022-01-01 10:00:00":f.type==="bool"?c=!0:f.type==="email"?c="test@example.com":f.type==="url"?c="https://example.com":f.type==="json"?c="JSON (array/object)":f.type==="file"?(c="filename.jpg",((o=f.options)==null?void 0:o.maxSelect)>1&&(c=[c])):f.type==="select"?(c=(l=(r=f.options)==null?void 0:r.values)==null?void 0:l[0],((s=f.options)==null?void 0:s.maxSelect)>1&&(c=[c])):f.type==="relation"||f.type==="user"?(c="RELATION_RECORD_ID",((a=f.options)==null?void 0:a.maxSelect)>1&&(c=[c])):c="test",i[f.name]=c}return i}static getFieldTypeIcon(e){switch(e==null?void 0:e.toLowerCase()){case"primary":return"ri-key-line";case"text":return"ri-text";case"number":return"ri-hashtag";case"date":return"ri-calendar-line";case"bool":return"ri-toggle-line";case"email":return"ri-mail-line";case"url":return"ri-link";case"select":return"ri-list-check";case"json":return"ri-braces-line";case"file":return"ri-image-line";case"relation":return"ri-mind-map";case"user":return"ri-user-line";default:return"ri-star-s-line"}}static getFieldValueType(e){var t;switch(e=e||{},e.type){case"bool":return"Boolean";case"number":return"Number";case"file":return"File";case"select":case"relation":case"user":return((t=e.options)==null?void 0:t.maxSelect)>1?"Array":"String";default:return"String"}}}const zf=Mi([]);function VO(n,e=4e3){return Hf(n,"info",e)}function hn(n,e=3e3){return Hf(n,"success",e)}function M0(n,e=4500){return Hf(n,"error",e)}function Hf(n,e,t){t=t||4e3;const i={message:n,type:e,duration:t,timeout:setTimeout(()=>{$0(i)},t)};zf.update(o=>(A0(o,i.message),B.pushOrReplaceByKey(o,i,"message"),o))}function $0(n){zf.update(e=>(A0(e,n),e))}function A0(n,e){let t;typeof e=="string"?t=B.findByKey(n,"message",e):t=e,t&&(clearTimeout(t.timeout),B.removeByKey(n,"message",t.message))}const go=Mi({});function Ui(n){go.set(n||{})}function D0(n){go.update(e=>(B.deleteByPath(e,n),e))}const qf=Mi({});function Ka(n){qf.set(n||{})}Tf.prototype.logout=function(n=!0){this.AuthStore.clear(),n&&Ss("/login")};Tf.prototype.errorResponseHandler=function(n,e=!0,t=""){var r,l;if(!n||!(n instanceof Error))return;const i=((r=n==null?void 0:n.response)==null?void 0:r.status)<<0||400,o=((l=n==null?void 0:n.response)==null?void 0:l.data)||{};if(e&&i!==404){let s=o.message||n.message||t;s&&M0(s)}if(B.isEmpty(o.data)||Ui(o.data),i===401)return this.cancelAllRequests(),this.logout();if(i===403)return this.cancelAllRequests(),Ss("/")};class z2 extends Mg{save(e,t){super.save(e,t),t instanceof Bo&&Ka(t)}clear(){super.clear(),Ka(null)}}const Se=new Tf("/","en-US",new z2("pb_admin_auth"));Se.AuthStore.model instanceof Bo&&Ka(Se.AuthStore.model);let Ja,Qi;const Za="app-tooltip";function Mu(n){return typeof n=="string"?{text:n,position:"bottom",hideOnClick:null}:n||{}}function zi(){return Qi=Qi||document.querySelector("."+Za),Qi||(Qi=document.createElement("div"),Qi.classList.add(Za),document.body.appendChild(Qi)),Qi}function O0(n,e){let t=zi();if(!t.classList.contains("active")||!(e!=null&&e.text)){Xa();return}t.textContent=e.text,t.className=Za+" active",e.class&&t.classList.add(e.class),t.style.top="0px",t.style.left="0px";let i=t.offsetHeight,o=t.offsetWidth,r=n.getBoundingClientRect(),l=0,s=0,a=5;e.position=="left"?(l=r.top+r.height/2-i/2,s=r.left-o-a):e.position=="right"?(l=r.top+r.height/2-i/2,s=r.right+a):e.position=="top"?(l=r.top-i-a,s=r.left+r.width/2-o/2):e.position=="top-left"?(l=r.top-i-a,s=r.left):e.position=="top-right"?(l=r.top-i-a,s=r.right-o):e.position=="bottom-left"?(l=r.top+r.height+a,s=r.left):e.position=="bottom-right"?(l=r.top+r.height+a,s=r.right-o):(l=r.top+r.height+a,s=r.left+r.width/2-o/2),s+o>document.documentElement.clientWidth&&(s=document.documentElement.clientWidth-o),s=s>=0?s:0,l+i>document.documentElement.clientHeight&&(l=document.documentElement.clientHeight-i),l=l>=0?l:0,t.style.top=l+"px",t.style.left=s+"px"}function Xa(){clearTimeout(Ja),zi().classList.remove("active"),zi().activeNode=void 0}function H2(n,e){zi().activeNode=n,clearTimeout(Ja),Ja=setTimeout(()=>{zi().classList.add("active"),O0(n,e)},isNaN(e.delay)?250:e.delay)}function St(n,e){let t=Mu(e);function i(){H2(n,t)}function o(){Xa()}return n.addEventListener("mouseenter",i),n.addEventListener("mouseleave",o),n.addEventListener("blur",o),(t.hideOnClick===!0||t.hideOnClick===null&&B.isFocusable(n))&&n.addEventListener("click",o),zi(),{update(r){var l,s;t=Mu(r),(s=(l=zi())==null?void 0:l.activeNode)!=null&&s.contains(n)&&O0(n,t)},destroy(){var r,l;(l=(r=zi())==null?void 0:r.activeNode)!=null&&l.contains(n)&&Xa(),n.removeEventListener("mouseenter",i),n.removeEventListener("mouseleave",o),n.removeEventListener("blur",o),n.removeEventListener("click",o)}}}function $u(n,e,t){const i=n.slice();return i[11]=e[t],i}const q2=n=>({}),Au=n=>({uniqueId:n[3]});function V2(n){let e=(n[11]||os)+"",t;return{c(){t=j(e)},m(i,o){w(i,t,o)},p(i,o){o&4&&e!==(e=(i[11]||os)+"")&&ge(t,e)},d(i){i&&k(t)}}}function B2(n){var i,o;let e=(((i=n[11])==null?void 0:i.message)||((o=n[11])==null?void 0:o.code)||os)+"",t;return{c(){t=j(e)},m(r,l){w(r,t,l)},p(r,l){var s,a;l&4&&e!==(e=(((s=r[11])==null?void 0:s.message)||((a=r[11])==null?void 0:a.code)||os)+"")&&ge(t,e)},d(r){r&&k(t)}}}function Du(n){let e,t;function i(l,s){return typeof l[11]=="object"?B2:V2}let o=i(n),r=o(n);return{c(){e=g("div"),r.c(),t=$(),p(e,"class","help-block help-block-error")},m(l,s){w(l,e,s),r.m(e,null),m(e,t)},p(l,s){o===(o=i(l))&&r?r.p(l,s):(r.d(1),r=o(l),r&&(r.c(),r.m(e,t)))},d(l){l&&k(e),r.d()}}}function U2(n){let e,t,i,o,r;const l=n[7].default,s=$n(l,n,n[6],Au);let a=n[2],f=[];for(let c=0;ct(5,i=b));let{$$slots:o={},$$scope:r}=e;const l="field_"+B.randomString(7);let{name:s=""}=e,{class:a=void 0}=e,f,c=[];function u(){D0(s)}di(()=>(f.addEventListener("change",u),()=>{f.removeEventListener("change",u)}));function d(b){ft.call(this,n,b)}function h(b){he[b?"unshift":"push"](()=>{f=b,t(1,f)})}return n.$$set=b=>{"name"in b&&t(4,s=b.name),"class"in b&&t(0,a=b.class),"$$scope"in b&&t(6,r=b.$$scope)},n.$$.update=()=>{n.$$.dirty&48&&t(2,c=B.toArray(B.getNestedVal(i,s)))},[a,f,c,l,s,i,r,o,d,h]}class je extends Ie{constructor(e){super(),Le(this,e,W2,U2,Ee,{name:4,class:0})}}function As(n){const e=n-1;return e*e*e+1}function rs(n,{delay:e=0,duration:t=400,easing:i=Vr}={}){const o=+getComputedStyle(n).opacity;return{delay:e,duration:t,easing:i,css:r=>`opacity: ${r*o}`}}function ti(n,{delay:e=0,duration:t=400,easing:i=As,x:o=0,y:r=0,opacity:l=0}={}){const s=getComputedStyle(n),a=+s.opacity,f=s.transform==="none"?"":s.transform,c=a*(1-l);return{delay:e,duration:t,easing:i,css:(u,d)=>` + transform: ${f} translate(${(1-u)*o}px, ${(1-u)*r}px); + opacity: ${a-c*d}`}}function fn(n,{delay:e=0,duration:t=400,easing:i=As}={}){const o=getComputedStyle(n),r=+o.opacity,l=parseFloat(o.height),s=parseFloat(o.paddingTop),a=parseFloat(o.paddingBottom),f=parseFloat(o.marginTop),c=parseFloat(o.marginBottom),u=parseFloat(o.borderTopWidth),d=parseFloat(o.borderBottomWidth);return{delay:e,duration:t,easing:i,css:h=>`overflow: hidden;opacity: ${Math.min(h*20,1)*r};height: ${h*l}px;padding-top: ${h*s}px;padding-bottom: ${h*a}px;margin-top: ${h*f}px;margin-bottom: ${h*c}px;border-top-width: ${h*u}px;border-bottom-width: ${h*d}px;`}}function Bn(n,{delay:e=0,duration:t=400,easing:i=As,start:o=0,opacity:r=0}={}){const l=getComputedStyle(n),s=+l.opacity,a=l.transform==="none"?"":l.transform,f=1-o,c=s*(1-r);return{delay:e,duration:t,easing:i,css:(u,d)=>` + transform: ${a} scale(${1-f*d}); + opacity: ${s-c*d} + `}}function Y2(n){let e,t,i,o;return{c(){e=g("input"),p(e,"type","text"),p(e,"id",n[8]),p(e,"placeholder",t=n[0]||n[1])},m(r,l){w(r,e,l),n[13](e),Me(e,n[7]),i||(o=X(e,"input",n[14]),i=!0)},p(r,l){l&3&&t!==(t=r[0]||r[1])&&p(e,"placeholder",t),l&128&&e.value!==r[7]&&Me(e,r[7])},i:le,o:le,d(r){r&&k(e),n[13](null),i=!1,o()}}}function G2(n){let e,t,i,o;function r(a){n[12](a)}var l=n[4];function s(a){let f={singleLine:!0,disableRequestKeys:!0,disableIndirectCollectionsKeys:!0,extraAutocompleteKeys:a[3],baseCollection:a[2],placeholder:a[0]||a[1]};return a[7]!==void 0&&(f.value=a[7]),{props:f}}return l&&(e=new l(s(n)),he.push(()=>Fe(e,"value",r)),e.$on("submit",n[10])),{c(){e&&V(e.$$.fragment),i=lt()},m(a,f){e&&H(e,a,f),w(a,i,f),o=!0},p(a,f){const c={};if(f&8&&(c.extraAutocompleteKeys=a[3]),f&4&&(c.baseCollection=a[2]),f&3&&(c.placeholder=a[0]||a[1]),!t&&f&128&&(t=!0,c.value=a[7],Re(()=>t=!1)),l!==(l=a[4])){if(e){Ae();const u=e;F(u.$$.fragment,1,0,()=>{q(u,1)}),De()}l?(e=new l(s(a)),he.push(()=>Fe(e,"value",r)),e.$on("submit",a[10]),V(e.$$.fragment),T(e.$$.fragment,1),H(e,i.parentNode,i)):e=null}else l&&e.$set(c)},i(a){o||(e&&T(e.$$.fragment,a),o=!0)},o(a){e&&F(e.$$.fragment,a),o=!1},d(a){a&&k(i),e&&q(e,a)}}}function Ou(n){let e,t,i,o,r,l,s=n[7]!==n[0]&&Tu();return{c(){s&&s.c(),e=$(),t=g("button"),t.innerHTML='Clear',p(t,"type","button"),p(t,"class","btn btn-secondary btn-sm btn-hint p-l-xs p-r-xs m-l-10")},m(a,f){s&&s.m(a,f),w(a,e,f),w(a,t,f),o=!0,r||(l=X(t,"click",n[15]),r=!0)},p(a,f){a[7]!==a[0]?s?f&129&&T(s,1):(s=Tu(),s.c(),T(s,1),s.m(e.parentNode,e)):s&&(Ae(),F(s,1,1,()=>{s=null}),De())},i(a){o||(T(s),Dt(()=>{i||(i=ct(t,ti,{duration:150,x:5},!0)),i.run(1)}),o=!0)},o(a){F(s),i||(i=ct(t,ti,{duration:150,x:5},!1)),i.run(0),o=!1},d(a){s&&s.d(a),a&&k(e),a&&k(t),a&&i&&i.end(),r=!1,l()}}}function Tu(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Search',p(e,"type","submit"),p(e,"class","btn btn-expanded btn-sm btn-warning")},m(o,r){w(o,e,r),i=!0},i(o){i||(Dt(()=>{t||(t=ct(e,ti,{duration:150,x:5},!0)),t.run(1)}),i=!0)},o(o){t||(t=ct(e,ti,{duration:150,x:5},!1)),t.run(0),i=!1},d(o){o&&k(e),o&&t&&t.end()}}}function K2(n){let e,t,i,o,r,l,s,a,f,c,u;const d=[G2,Y2],h=[];function b(_,y){return _[4]&&!_[5]?0:1}l=b(n),s=h[l]=d[l](n);let v=(n[0].length||n[7].length)&&Ou(n);return{c(){e=g("div"),t=g("form"),i=g("label"),o=g("i"),r=$(),s.c(),a=$(),v&&v.c(),p(o,"class","ri-search-line"),p(i,"for",n[8]),p(i,"class","m-l-10 txt-xl"),p(t,"class","searchbar"),p(e,"class","searchbar-wrapper")},m(_,y){w(_,e,y),m(e,t),m(t,i),m(i,o),m(t,r),h[l].m(t,null),m(t,a),v&&v.m(t,null),f=!0,c||(u=[X(t,"submit",Gt(n[10])),X(e,"click",Vn(n[11]))],c=!0)},p(_,[y]){let S=l;l=b(_),l===S?h[l].p(_,y):(Ae(),F(h[S],1,1,()=>{h[S]=null}),De(),s=h[l],s?s.p(_,y):(s=h[l]=d[l](_),s.c()),T(s,1),s.m(t,a)),_[0].length||_[7].length?v?(v.p(_,y),y&129&&T(v,1)):(v=Ou(_),v.c(),T(v,1),v.m(t,null)):v&&(Ae(),F(v,1,1,()=>{v=null}),De())},i(_){f||(T(s),T(v),f=!0)},o(_){F(s),F(v),f=!1},d(_){_&&k(e),h[l].d(),v&&v.d(),c=!1,rt(u)}}}function J2(n,e,t){const i=yn(),o="search_"+B.randomString(7);let{value:r=""}=e,{placeholder:l='Search filter, ex. created > "2022-01-01"...'}=e,{autocompleteCollection:s=new En}=e,{extraAutocompleteKeys:a=[]}=e,f,c=!1,u,d="";function h(M=!0){t(7,d=""),M&&(u==null||u.focus()),i("clear")}function b(){t(0,r=d),i("submit",r)}async function v(){f||c||(t(5,c=!0),t(4,f=(await _i(()=>import("./FilterAutocompleteInput.15d21df7.js"),[])).default),t(5,c=!1))}di(()=>{v()});function _(M){ft.call(this,n,M)}function y(M){d=M,t(7,d),t(0,r)}function S(M){he[M?"unshift":"push"](()=>{u=M,t(6,u)})}function C(){d=this.value,t(7,d),t(0,r)}const x=()=>{h(!1),b()};return n.$$set=M=>{"value"in M&&t(0,r=M.value),"placeholder"in M&&t(1,l=M.placeholder),"autocompleteCollection"in M&&t(2,s=M.autocompleteCollection),"extraAutocompleteKeys"in M&&t(3,a=M.extraAutocompleteKeys)},n.$$.update=()=>{n.$$.dirty&1&&typeof r=="string"&&t(7,d=r)},[r,l,s,a,f,c,u,d,o,h,b,_,y,S,C,x]}class Ds extends Ie{constructor(e){super(),Le(this,e,J2,K2,Ee,{value:0,placeholder:1,autocompleteCollection:2,extraAutocompleteKeys:3})}}function Z2(n){let e,t,i,o,r;const l=n[6].default,s=$n(l,n,n[5],null);return{c(){e=g("th"),s&&s.c(),p(e,"tabindex","0"),p(e,"class",t="col-sort "+n[1]),ne(e,"col-sort-disabled",n[3]),ne(e,"sort-active",n[0]==="-"+n[2]||n[0]==="+"+n[2]),ne(e,"sort-desc",n[0]==="-"+n[2]),ne(e,"sort-asc",n[0]==="+"+n[2])},m(a,f){w(a,e,f),s&&s.m(e,null),i=!0,o||(r=[X(e,"click",n[7]),X(e,"keydown",n[8])],o=!0)},p(a,[f]){s&&s.p&&(!i||f&32)&&Dn(s,l,a,a[5],i?An(l,a[5],f,null):On(a[5]),null),(!i||f&2&&t!==(t="col-sort "+a[1]))&&p(e,"class",t),f&10&&ne(e,"col-sort-disabled",a[3]),f&7&&ne(e,"sort-active",a[0]==="-"+a[2]||a[0]==="+"+a[2]),f&7&&ne(e,"sort-desc",a[0]==="-"+a[2]),f&7&&ne(e,"sort-asc",a[0]==="+"+a[2])},i(a){i||(T(s,a),i=!0)},o(a){F(s,a),i=!1},d(a){a&&k(e),s&&s.d(a),o=!1,rt(r)}}}function X2(n,e,t){let{$$slots:i={},$$scope:o}=e,{class:r=""}=e,{name:l}=e,{sort:s=""}=e,{disable:a=!1}=e;function f(){a||("-"+l===s?t(0,s="+"+l):t(0,s="-"+l))}const c=()=>f(),u=d=>{(d.code==="Enter"||d.code==="Space")&&(d.preventDefault(),f())};return n.$$set=d=>{"class"in d&&t(1,r=d.class),"name"in d&&t(2,l=d.name),"sort"in d&&t(0,s=d.sort),"disable"in d&&t(3,a=d.disable),"$$scope"in d&&t(5,o=d.$$scope)},[s,r,l,a,f,o,i,c,u]}class en extends Ie{constructor(e){super(),Le(this,e,X2,Z2,Ee,{class:1,name:2,sort:0,disable:3})}}function Q2(n){let e;return{c(){e=g("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function ek(n){let e,t=B.formatToUTCDate(n[0])+"",i,o,r,l,s;return{c(){e=g("span"),i=j(t),o=j(" UTC"),p(e,"class","txt")},m(a,f){w(a,e,f),m(e,i),m(e,o),l||(s=Xe(r=St.call(null,e,B.formatToLocalDate(n[0])+" Local")),l=!0)},p(a,f){f&1&&t!==(t=B.formatToUTCDate(a[0])+"")&&ge(i,t),r&&Yn(r.update)&&f&1&&r.update.call(null,B.formatToLocalDate(a[0])+" Local")},d(a){a&&k(e),l=!1,s()}}}function tk(n){let e;function t(r,l){return r[0]?ek:Q2}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,[l]){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},i:le,o:le,d(r){o.d(r),r&&k(e)}}}function nk(n,e,t){let{date:i=""}=e;return n.$$set=o=>{"date"in o&&t(0,i=o.date)},[i]}class Ci extends Ie{constructor(e){super(),Le(this,e,nk,tk,Ee,{date:0})}}function Eu(n,e,t){const i=n.slice();return i[21]=e[t],i}function ik(n){let e;return{c(){e=g("div"),e.innerHTML=` + method`,p(e,"class","col-header-content")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function ok(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="url",p(t,"class",B.getFieldTypeIcon("url")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function rk(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="referer",p(t,"class",B.getFieldTypeIcon("url")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function lk(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="status",p(t,"class",B.getFieldTypeIcon("number")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function sk(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="created",p(t,"class",B.getFieldTypeIcon("date")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function Pu(n){let e;function t(r,l){return r[6]?fk:ak}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,l){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},d(r){o.d(r),r&&k(e)}}}function ak(n){var s;let e,t,i,o,r,l=((s=n[0])==null?void 0:s.length)&&Fu(n);return{c(){e=g("tr"),t=g("td"),i=g("h6"),i.textContent="No logs found.",o=$(),l&&l.c(),r=$(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,f){w(a,e,f),m(e,t),m(t,i),m(t,o),l&&l.m(t,null),m(e,r)},p(a,f){var c;(c=a[0])!=null&&c.length?l?l.p(a,f):(l=Fu(a),l.c(),l.m(t,null)):l&&(l.d(1),l=null)},d(a){a&&k(e),l&&l.d()}}}function fk(n){let e;return{c(){e=g("tr"),e.innerHTML=` + `},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function Fu(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[18]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function Lu(n){let e;return{c(){e=g("i"),p(e,"class","ri-error-warning-line txt-danger m-l-5 m-r-5"),p(e,"title","Error")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Iu(n,e){var oe,J,$e;let t,i,o,r=((oe=e[21].method)==null?void 0:oe.toUpperCase())+"",l,s,a,f,c,u=e[21].url+"",d,h,b,v,_,y,S=(e[21].referer||"N/A")+"",C,x,M,A,O,D=e[21].status+"",E,P,I,R,G,U,z,K,Y,W,te=(((J=e[21].meta)==null?void 0:J.errorMessage)||(($e=e[21].meta)==null?void 0:$e.errorData))&&Lu();R=new Ci({props:{date:e[21].created}});function ce(){return e[16](e[21])}function ve(...ee){return e[17](e[21],...ee)}return{key:n,first:null,c(){t=g("tr"),i=g("td"),o=g("span"),l=j(r),a=$(),f=g("td"),c=g("span"),d=j(u),b=$(),te&&te.c(),v=$(),_=g("td"),y=g("span"),C=j(S),M=$(),A=g("td"),O=g("span"),E=j(D),P=$(),I=g("td"),V(R.$$.fragment),G=$(),U=g("td"),U.innerHTML='',z=$(),p(o,"class",s="label txt-uppercase "+e[9][e[21].method.toLowerCase()]),p(i,"class","col-type-text col-field-method min-width"),p(c,"class","txt txt-ellipsis"),p(c,"title",h=e[21].url),p(f,"class","col-type-text col-field-url"),p(y,"class","txt txt-ellipsis"),p(y,"title",x=e[21].referer),ne(y,"txt-hint",!e[21].referer),p(_,"class","col-type-text col-field-referer"),p(O,"class","label"),ne(O,"label-danger",e[21].status>=400),p(A,"class","col-type-number col-field-status"),p(I,"class","col-type-date col-field-created"),p(U,"class","col-type-action min-width"),p(t,"tabindex","0"),p(t,"class","row-handle"),this.first=t},m(ee,_e){w(ee,t,_e),m(t,i),m(i,o),m(o,l),m(t,a),m(t,f),m(f,c),m(c,d),m(f,b),te&&te.m(f,null),m(t,v),m(t,_),m(_,y),m(y,C),m(t,M),m(t,A),m(A,O),m(O,E),m(t,P),m(t,I),H(R,I,null),m(t,G),m(t,U),m(t,z),K=!0,Y||(W=[X(t,"click",ce),X(t,"keydown",ve)],Y=!0)},p(ee,_e){var ie,ye,Ne;e=ee,(!K||_e&8)&&r!==(r=((ie=e[21].method)==null?void 0:ie.toUpperCase())+"")&&ge(l,r),(!K||_e&8&&s!==(s="label txt-uppercase "+e[9][e[21].method.toLowerCase()]))&&p(o,"class",s),(!K||_e&8)&&u!==(u=e[21].url+"")&&ge(d,u),(!K||_e&8&&h!==(h=e[21].url))&&p(c,"title",h),((ye=e[21].meta)==null?void 0:ye.errorMessage)||((Ne=e[21].meta)==null?void 0:Ne.errorData)?te||(te=Lu(),te.c(),te.m(f,null)):te&&(te.d(1),te=null),(!K||_e&8)&&S!==(S=(e[21].referer||"N/A")+"")&&ge(C,S),(!K||_e&8&&x!==(x=e[21].referer))&&p(y,"title",x),_e&8&&ne(y,"txt-hint",!e[21].referer),(!K||_e&8)&&D!==(D=e[21].status+"")&&ge(E,D),_e&8&&ne(O,"label-danger",e[21].status>=400);const fe={};_e&8&&(fe.date=e[21].created),R.$set(fe)},i(ee){K||(T(R.$$.fragment,ee),K=!0)},o(ee){F(R.$$.fragment,ee),K=!1},d(ee){ee&&k(t),te&&te.d(),q(R),Y=!1,rt(W)}}}function Ru(n){let e,t,i=n[3].length+"",o,r,l;return{c(){e=g("small"),t=j("Showing "),o=j(i),r=j(" of "),l=j(n[4]),p(e,"class","block txt-hint txt-right m-t-sm")},m(s,a){w(s,e,a),m(e,t),m(e,o),m(e,r),m(e,l)},p(s,a){a&8&&i!==(i=s[3].length+"")&&ge(o,i),a&16&&ge(l,s[4])},d(s){s&&k(e)}}}function Nu(n){let e,t,i,o,r=n[4]-n[3].length+"",l,s,a,f;return{c(){e=g("div"),t=g("button"),i=g("span"),o=j("Load more ("),l=j(r),s=j(")"),p(i,"class","txt"),p(t,"type","button"),p(t,"class","btn btn-lg btn-secondary btn-expanded"),ne(t,"btn-loading",n[6]),ne(t,"btn-disabled",n[6]),p(e,"class","block txt-center m-t-xs")},m(c,u){w(c,e,u),m(e,t),m(t,i),m(i,o),m(i,l),m(i,s),a||(f=X(t,"click",n[19]),a=!0)},p(c,u){u&24&&r!==(r=c[4]-c[3].length+"")&&ge(l,r),u&64&&ne(t,"btn-loading",c[6]),u&64&&ne(t,"btn-disabled",c[6])},d(c){c&&k(e),a=!1,f()}}}function ck(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y,S,C,x,M,A,O=[],D=new Map,E,P,I,R;function G(ie){n[11](ie)}let U={disable:!0,class:"col-field-method",name:"method",$$slots:{default:[ik]},$$scope:{ctx:n}};n[1]!==void 0&&(U.sort=n[1]),r=new en({props:U}),he.push(()=>Fe(r,"sort",G));function z(ie){n[12](ie)}let K={disable:!0,class:"col-type-text col-field-url",name:"url",$$slots:{default:[ok]},$$scope:{ctx:n}};n[1]!==void 0&&(K.sort=n[1]),a=new en({props:K}),he.push(()=>Fe(a,"sort",z));function Y(ie){n[13](ie)}let W={disable:!0,class:"col-type-text col-field-referer",name:"referer",$$slots:{default:[rk]},$$scope:{ctx:n}};n[1]!==void 0&&(W.sort=n[1]),u=new en({props:W}),he.push(()=>Fe(u,"sort",Y));function te(ie){n[14](ie)}let ce={disable:!0,class:"col-type-number col-field-status",name:"status",$$slots:{default:[lk]},$$scope:{ctx:n}};n[1]!==void 0&&(ce.sort=n[1]),b=new en({props:ce}),he.push(()=>Fe(b,"sort",te));function ve(ie){n[15](ie)}let oe={disable:!0,class:"col-type-date col-field-created",name:"created",$$slots:{default:[sk]},$$scope:{ctx:n}};n[1]!==void 0&&(oe.sort=n[1]),y=new en({props:oe}),he.push(()=>Fe(y,"sort",ve));let J=n[3];const $e=ie=>ie[21].id;for(let ie=0;iel=!1)),r.$set(Ne);const Pe={};ye&16777216&&(Pe.$$scope={dirty:ye,ctx:ie}),!f&&ye&2&&(f=!0,Pe.sort=ie[1],Re(()=>f=!1)),a.$set(Pe);const ze={};ye&16777216&&(ze.$$scope={dirty:ye,ctx:ie}),!d&&ye&2&&(d=!0,ze.sort=ie[1],Re(()=>d=!1)),u.$set(ze);const se={};ye&16777216&&(se.$$scope={dirty:ye,ctx:ie}),!v&&ye&2&&(v=!0,se.sort=ie[1],Re(()=>v=!1)),b.$set(se);const re={};ye&16777216&&(re.$$scope={dirty:ye,ctx:ie}),!S&&ye&2&&(S=!0,re.sort=ie[1],Re(()=>S=!1)),y.$set(re),ye&841&&(J=ie[3],Ae(),O=st(O,ye,$e,1,ie,J,D,A,Pt,Iu,null,Eu),De(),!J.length&&ee?ee.p(ie,ye):J.length?ee&&(ee.d(1),ee=null):(ee=Pu(ie),ee.c(),ee.m(A,null))),ye&64&&ne(t,"table-loading",ie[6]),ie[3].length?_e?_e.p(ie,ye):(_e=Ru(ie),_e.c(),_e.m(P.parentNode,P)):_e&&(_e.d(1),_e=null),ie[3].length&&ie[7]?fe?fe.p(ie,ye):(fe=Nu(ie),fe.c(),fe.m(I.parentNode,I)):fe&&(fe.d(1),fe=null)},i(ie){if(!R){T(r.$$.fragment,ie),T(a.$$.fragment,ie),T(u.$$.fragment,ie),T(b.$$.fragment,ie),T(y.$$.fragment,ie);for(let ye=0;ye{D<=1&&b(),t(6,d=!1),t(3,f=f.concat(E.items)),t(5,c=E.page),t(4,u=E.totalItems),o("load",f)}).catch(E=>{E!==null&&(t(6,d=!1),console.warn(E),b(),Se.errorResponseHandler(E,!1))})}function b(){t(3,f=[]),t(5,c=1),t(4,u=0)}function v(D){a=D,t(1,a)}function _(D){a=D,t(1,a)}function y(D){a=D,t(1,a)}function S(D){a=D,t(1,a)}function C(D){a=D,t(1,a)}const x=D=>o("select",D),M=(D,E)=>{E.code==="Enter"&&(E.preventDefault(),o("select",D))},A=()=>t(0,l=""),O=()=>h(c+1);return n.$$set=D=>{"filter"in D&&t(0,l=D.filter),"presets"in D&&t(10,s=D.presets),"sort"in D&&t(1,a=D.sort)},n.$$.update=()=>{n.$$.dirty&1027&&(typeof a!="undefined"||typeof l!="undefined"||typeof s!="undefined")&&(b(),h(1)),n.$$.dirty&24&&t(7,i=u>f.length)},[l,a,h,f,u,c,d,i,o,r,s,v,_,y,S,C,x,M,A,O]}class dk extends Ie{constructor(e){super(),Le(this,e,uk,ck,Ee,{filter:0,presets:10,sort:1,load:2})}get load(){return this.$$.ctx[2]}}/*! + * Chart.js v3.8.0 + * https://www.chartjs.org + * (c) 2022 Chart.js Contributors + * Released under the MIT License + */const T0=function(){return typeof window=="undefined"?function(n){return n()}:window.requestAnimationFrame}();function E0(n,e,t){const i=t||(l=>Array.prototype.slice.call(l));let o=!1,r=[];return function(...l){r=i(l),o||(o=!0,T0.call(window,()=>{o=!1,n.apply(e,r)}))}}function pk(n,e){let t;return function(...i){return e?(clearTimeout(t),t=setTimeout(n,e,i)):n.apply(this,i),e}}const hk=n=>n==="start"?"left":n==="end"?"right":"center",ju=(n,e,t)=>n==="start"?e:n==="end"?t:(e+t)/2;function mi(){}const mk=function(){let n=0;return function(){return n++}}();function xt(n){return n===null||typeof n=="undefined"}function Et(n){if(Array.isArray&&Array.isArray(n))return!0;const e=Object.prototype.toString.call(n);return e.slice(0,7)==="[object"&&e.slice(-6)==="Array]"}function dt(n){return n!==null&&Object.prototype.toString.call(n)==="[object Object]"}const Ht=n=>(typeof n=="number"||n instanceof Number)&&isFinite(+n);function Rn(n,e){return Ht(n)?n:e}function ht(n,e){return typeof n=="undefined"?e:n}const bk=(n,e)=>typeof n=="string"&&n.endsWith("%")?parseFloat(n)/100:n/e,P0=(n,e)=>typeof n=="string"&&n.endsWith("%")?parseFloat(n)/100*e:+n;function Rt(n,e,t){if(n&&typeof n.call=="function")return n.apply(t,e)}function Ct(n,e,t,i){let o,r,l;if(Et(n))if(r=n.length,i)for(o=r-1;o>=0;o--)e.call(t,n[o],o);else for(o=0;ot;)n=n[e.slice(t,i)],t=i+1,i=zu(e,t);return n}function Vf(n){return n.charAt(0).toUpperCase()+n.slice(1)}const Un=n=>typeof n!="undefined",Vi=n=>typeof n=="function",Hu=(n,e)=>{if(n.size!==e.size)return!1;for(const t of n)if(!e.has(t))return!1;return!0};function kk(n){return n.type==="mouseup"||n.type==="click"||n.type==="contextmenu"}const jt=Math.PI,Tt=2*jt,wk=Tt+jt,as=Number.POSITIVE_INFINITY,Sk=jt/180,Nt=jt/2,fr=jt/4,qu=jt*2/3,zn=Math.log10,ai=Math.sign;function Vu(n){const e=Math.round(n);n=$r(n,e,n/1e3)?e:n;const t=Math.pow(10,Math.floor(zn(n))),i=n/t;return(i<=1?1:i<=2?2:i<=5?5:10)*t}function Ck(n){const e=[],t=Math.sqrt(n);let i;for(i=1;io-r).pop(),e}function Ir(n){return!isNaN(parseFloat(n))&&isFinite(n)}function $r(n,e,t){return Math.abs(n-e)=n}function L0(n,e,t){let i,o,r;for(i=0,o=n.length;ia&&f=Math.min(e,t)-i&&n<=Math.max(e,t)+i}const kl=n=>n===0||n===1,Uu=(n,e,t)=>-(Math.pow(2,10*(n-=1))*Math.sin((n-e)*Tt/t)),Wu=(n,e,t)=>Math.pow(2,-10*n)*Math.sin((n-e)*Tt/t)+1,Ar={linear:n=>n,easeInQuad:n=>n*n,easeOutQuad:n=>-n*(n-2),easeInOutQuad:n=>(n/=.5)<1?.5*n*n:-.5*(--n*(n-2)-1),easeInCubic:n=>n*n*n,easeOutCubic:n=>(n-=1)*n*n+1,easeInOutCubic:n=>(n/=.5)<1?.5*n*n*n:.5*((n-=2)*n*n+2),easeInQuart:n=>n*n*n*n,easeOutQuart:n=>-((n-=1)*n*n*n-1),easeInOutQuart:n=>(n/=.5)<1?.5*n*n*n*n:-.5*((n-=2)*n*n*n-2),easeInQuint:n=>n*n*n*n*n,easeOutQuint:n=>(n-=1)*n*n*n*n+1,easeInOutQuint:n=>(n/=.5)<1?.5*n*n*n*n*n:.5*((n-=2)*n*n*n*n+2),easeInSine:n=>-Math.cos(n*Nt)+1,easeOutSine:n=>Math.sin(n*Nt),easeInOutSine:n=>-.5*(Math.cos(jt*n)-1),easeInExpo:n=>n===0?0:Math.pow(2,10*(n-1)),easeOutExpo:n=>n===1?1:-Math.pow(2,-10*n)+1,easeInOutExpo:n=>kl(n)?n:n<.5?.5*Math.pow(2,10*(n*2-1)):.5*(-Math.pow(2,-10*(n*2-1))+2),easeInCirc:n=>n>=1?n:-(Math.sqrt(1-n*n)-1),easeOutCirc:n=>Math.sqrt(1-(n-=1)*n),easeInOutCirc:n=>(n/=.5)<1?-.5*(Math.sqrt(1-n*n)-1):.5*(Math.sqrt(1-(n-=2)*n)+1),easeInElastic:n=>kl(n)?n:Uu(n,.075,.3),easeOutElastic:n=>kl(n)?n:Wu(n,.075,.3),easeInOutElastic(n){return kl(n)?n:n<.5?.5*Uu(n*2,.1125,.45):.5+.5*Wu(n*2-1,.1125,.45)},easeInBack(n){return n*n*((1.70158+1)*n-1.70158)},easeOutBack(n){return(n-=1)*n*((1.70158+1)*n+1.70158)+1},easeInOutBack(n){let e=1.70158;return(n/=.5)<1?.5*(n*n*(((e*=1.525)+1)*n-e)):.5*((n-=2)*n*(((e*=1.525)+1)*n+e)+2)},easeInBounce:n=>1-Ar.easeOutBounce(1-n),easeOutBounce(n){return n<1/2.75?7.5625*n*n:n<2/2.75?7.5625*(n-=1.5/2.75)*n+.75:n<2.5/2.75?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375},easeInOutBounce:n=>n<.5?Ar.easeInBounce(n*2)*.5:Ar.easeOutBounce(n*2-1)*.5+.5};/*! + * @kurkle/color v0.2.1 + * https://github.com/kurkle/color#readme + * (c) 2022 Jukka Kurkela + * Released under the MIT License + */function Jr(n){return n+.5|0}const Ni=(n,e,t)=>Math.max(Math.min(n,t),e);function yr(n){return Ni(Jr(n*2.55),0,255)}function Hi(n){return Ni(Jr(n*255),0,255)}function yi(n){return Ni(Jr(n/2.55)/100,0,1)}function Yu(n){return Ni(Jr(n*100),0,100)}const In={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},ef=[..."0123456789ABCDEF"],Ak=n=>ef[n&15],Dk=n=>ef[(n&240)>>4]+ef[n&15],wl=n=>(n&240)>>4===(n&15),Ok=n=>wl(n.r)&&wl(n.g)&&wl(n.b)&&wl(n.a);function Tk(n){var e=n.length,t;return n[0]==="#"&&(e===4||e===5?t={r:255&In[n[1]]*17,g:255&In[n[2]]*17,b:255&In[n[3]]*17,a:e===5?In[n[4]]*17:255}:(e===7||e===9)&&(t={r:In[n[1]]<<4|In[n[2]],g:In[n[3]]<<4|In[n[4]],b:In[n[5]]<<4|In[n[6]],a:e===9?In[n[7]]<<4|In[n[8]]:255})),t}const Ek=(n,e)=>n<255?e(n):"";function Pk(n){var e=Ok(n)?Ak:Dk;return n?"#"+e(n.r)+e(n.g)+e(n.b)+Ek(n.a,e):void 0}const Fk=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function R0(n,e,t){const i=e*Math.min(t,1-t),o=(r,l=(r+n/30)%12)=>t-i*Math.max(Math.min(l-3,9-l,1),-1);return[o(0),o(8),o(4)]}function Lk(n,e,t){const i=(o,r=(o+n/60)%6)=>t-t*e*Math.max(Math.min(r,4-r,1),0);return[i(5),i(3),i(1)]}function Ik(n,e,t){const i=R0(n,1,.5);let o;for(e+t>1&&(o=1/(e+t),e*=o,t*=o),o=0;o<3;o++)i[o]*=1-e-t,i[o]+=e;return i}function Rk(n,e,t,i,o){return n===o?(e-t)/i+(e.5?c/(2-r-l):c/(r+l),a=Rk(t,i,o,c,r),a=a*60+.5),[a|0,f||0,s]}function Wf(n,e,t,i){return(Array.isArray(e)?n(e[0],e[1],e[2]):n(e,t,i)).map(Hi)}function Yf(n,e,t){return Wf(R0,n,e,t)}function Nk(n,e,t){return Wf(Ik,n,e,t)}function jk(n,e,t){return Wf(Lk,n,e,t)}function N0(n){return(n%360+360)%360}function zk(n){const e=Fk.exec(n);let t=255,i;if(!e)return;e[5]!==i&&(t=e[6]?yr(+e[5]):Hi(+e[5]));const o=N0(+e[2]),r=+e[3]/100,l=+e[4]/100;return e[1]==="hwb"?i=Nk(o,r,l):e[1]==="hsv"?i=jk(o,r,l):i=Yf(o,r,l),{r:i[0],g:i[1],b:i[2],a:t}}function Hk(n,e){var t=Uf(n);t[0]=N0(t[0]+e),t=Yf(t),n.r=t[0],n.g=t[1],n.b=t[2]}function qk(n){if(!n)return;const e=Uf(n),t=e[0],i=Yu(e[1]),o=Yu(e[2]);return n.a<255?`hsla(${t}, ${i}%, ${o}%, ${yi(n.a)})`:`hsl(${t}, ${i}%, ${o}%)`}const Gu={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Ku={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function Vk(){const n={},e=Object.keys(Ku),t=Object.keys(Gu);let i,o,r,l,s;for(i=0;i>16&255,r>>8&255,r&255]}return n}let Sl;function Bk(n){Sl||(Sl=Vk(),Sl.transparent=[0,0,0,0]);const e=Sl[n.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:e.length===4?e[3]:255}}const Uk=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function Wk(n){const e=Uk.exec(n);let t=255,i,o,r;if(!!e){if(e[7]!==i){const l=+e[7];t=e[8]?yr(l):Ni(l*255,0,255)}return i=+e[1],o=+e[3],r=+e[5],i=255&(e[2]?yr(i):Ni(i,0,255)),o=255&(e[4]?yr(o):Ni(o,0,255)),r=255&(e[6]?yr(r):Ni(r,0,255)),{r:i,g:o,b:r,a:t}}}function Yk(n){return n&&(n.a<255?`rgba(${n.r}, ${n.g}, ${n.b}, ${yi(n.a)})`:`rgb(${n.r}, ${n.g}, ${n.b})`)}const fa=n=>n<=.0031308?n*12.92:Math.pow(n,1/2.4)*1.055-.055,Oo=n=>n<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4);function Gk(n,e,t){const i=Oo(yi(n.r)),o=Oo(yi(n.g)),r=Oo(yi(n.b));return{r:Hi(fa(i+t*(Oo(yi(e.r))-i))),g:Hi(fa(o+t*(Oo(yi(e.g))-o))),b:Hi(fa(r+t*(Oo(yi(e.b))-r))),a:n.a+t*(e.a-n.a)}}function Cl(n,e,t){if(n){let i=Uf(n);i[e]=Math.max(0,Math.min(i[e]+i[e]*t,e===0?360:1)),i=Yf(i),n.r=i[0],n.g=i[1],n.b=i[2]}}function j0(n,e){return n&&Object.assign(e||{},n)}function Ju(n){var e={r:0,g:0,b:0,a:255};return Array.isArray(n)?n.length>=3&&(e={r:n[0],g:n[1],b:n[2],a:255},n.length>3&&(e.a=Hi(n[3]))):(e=j0(n,{r:0,g:0,b:0,a:1}),e.a=Hi(e.a)),e}function Kk(n){return n.charAt(0)==="r"?Wk(n):zk(n)}class fs{constructor(e){if(e instanceof fs)return e;const t=typeof e;let i;t==="object"?i=Ju(e):t==="string"&&(i=Tk(e)||Bk(e)||Kk(e)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var e=j0(this._rgb);return e&&(e.a=yi(e.a)),e}set rgb(e){this._rgb=Ju(e)}rgbString(){return this._valid?Yk(this._rgb):void 0}hexString(){return this._valid?Pk(this._rgb):void 0}hslString(){return this._valid?qk(this._rgb):void 0}mix(e,t){if(e){const i=this.rgb,o=e.rgb;let r;const l=t===r?.5:t,s=2*l-1,a=i.a-o.a,f=((s*a===-1?s:(s+a)/(1+s*a))+1)/2;r=1-f,i.r=255&f*i.r+r*o.r+.5,i.g=255&f*i.g+r*o.g+.5,i.b=255&f*i.b+r*o.b+.5,i.a=l*i.a+(1-l)*o.a,this.rgb=i}return this}interpolate(e,t){return e&&(this._rgb=Gk(this._rgb,e._rgb,t)),this}clone(){return new fs(this.rgb)}alpha(e){return this._rgb.a=Hi(e),this}clearer(e){const t=this._rgb;return t.a*=1-e,this}greyscale(){const e=this._rgb,t=Jr(e.r*.3+e.g*.59+e.b*.11);return e.r=e.g=e.b=t,this}opaquer(e){const t=this._rgb;return t.a*=1+e,this}negate(){const e=this._rgb;return e.r=255-e.r,e.g=255-e.g,e.b=255-e.b,this}lighten(e){return Cl(this._rgb,2,e),this}darken(e){return Cl(this._rgb,2,-e),this}saturate(e){return Cl(this._rgb,1,e),this}desaturate(e){return Cl(this._rgb,1,-e),this}rotate(e){return Hk(this._rgb,e),this}}function z0(n){return new fs(n)}function H0(n){if(n&&typeof n=="object"){const e=n.toString();return e==="[object CanvasPattern]"||e==="[object CanvasGradient]"}return!1}function Zu(n){return H0(n)?n:z0(n)}function ca(n){return H0(n)?n:z0(n).saturate(.5).darken(.1).hexString()}const mo=Object.create(null),tf=Object.create(null);function Dr(n,e){if(!e)return n;const t=e.split(".");for(let i=0,o=t.length;it.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,i)=>ca(i.backgroundColor),this.hoverBorderColor=(t,i)=>ca(i.borderColor),this.hoverColor=(t,i)=>ca(i.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(e)}set(e,t){return ua(this,e,t)}get(e){return Dr(this,e)}describe(e,t){return ua(tf,e,t)}override(e,t){return ua(mo,e,t)}route(e,t,i,o){const r=Dr(this,e),l=Dr(this,i),s="_"+t;Object.defineProperties(r,{[s]:{value:r[t],writable:!0},[t]:{enumerable:!0,get(){const a=this[s],f=l[o];return dt(a)?Object.assign({},f,a):ht(a,f)},set(a){this[s]=a}}})}}var mt=new Jk({_scriptable:n=>!n.startsWith("on"),_indexable:n=>n!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}});function Zk(n){return!n||xt(n.size)||xt(n.family)?null:(n.style?n.style+" ":"")+(n.weight?n.weight+" ":"")+n.size+"px "+n.family}function cs(n,e,t,i,o){let r=e[o];return r||(r=e[o]=n.measureText(o).width,t.push(o)),r>i&&(i=r),i}function Xk(n,e,t,i){i=i||{};let o=i.data=i.data||{},r=i.garbageCollect=i.garbageCollect||[];i.font!==e&&(o=i.data={},r=i.garbageCollect=[],i.font=e),n.save(),n.font=e;let l=0;const s=t.length;let a,f,c,u,d;for(a=0;at.length){for(a=0;a0&&n.stroke()}}function jr(n,e,t){return t=t||.5,!e||n&&n.x>e.left-t&&n.xe.top-t&&n.y0&&r.strokeColor!=="";let a,f;for(n.save(),n.font=o.string,tw(n,r),a=0;a+n||0;function Jf(n,e){const t={},i=dt(e),o=i?Object.keys(e):e,r=dt(n)?i?l=>ht(n[l],n[e[l]]):l=>n[l]:()=>n;for(const l of o)t[l]=lw(r(l));return t}function q0(n){return Jf(n,{top:"y",right:"x",bottom:"y",left:"x"})}function jo(n){return Jf(n,["topLeft","topRight","bottomLeft","bottomRight"])}function Wn(n){const e=q0(n);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Mn(n,e){n=n||{},e=e||mt.font;let t=ht(n.size,e.size);typeof t=="string"&&(t=parseInt(t,10));let i=ht(n.style,e.style);i&&!(""+i).match(ow)&&(console.warn('Invalid font style specified: "'+i+'"'),i="");const o={family:ht(n.family,e.family),lineHeight:rw(ht(n.lineHeight,e.lineHeight),t),size:t,style:i,weight:ht(n.weight,e.weight),string:""};return o.string=Zk(o),o}function xl(n,e,t,i){let o=!0,r,l,s;for(r=0,l=n.length;rt&&s===0?0:s+a;return{min:l(i,-Math.abs(r)),max:l(o,r)}}function Wi(n,e){return Object.assign(Object.create(n),e)}function Zf(n,e,t){t=t||(l=>n[l]1;)r=o+i>>1,t(r)?o=r:i=r;return{lo:o,hi:i}}const ao=(n,e,t)=>Zf(n,t,i=>n[i][e]Zf(n,t,i=>n[i][e]>=t);function fw(n,e,t){let i=0,o=n.length;for(;ii&&n[o-1]>t;)o--;return i>0||o{const i="_onData"+Vf(t),o=n[t];Object.defineProperty(n,t,{configurable:!0,enumerable:!1,value(...r){const l=o.apply(this,r);return n._chartjs.listeners.forEach(s=>{typeof s[i]=="function"&&s[i](...r)}),l}})})}function Qu(n,e){const t=n._chartjs;if(!t)return;const i=t.listeners,o=i.indexOf(e);o!==-1&&i.splice(o,1),!(i.length>0)&&(V0.forEach(r=>{delete n[r]}),delete n._chartjs)}function B0(n){const e=new Set;let t,i;for(t=0,i=n.length;tn[0]){Un(i)||(i=G0("_fallback",n));const r={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:n,_rootScopes:t,_fallback:i,_getTarget:o,override:l=>Xf([l,...n],e,t,i)};return new Proxy(r,{deleteProperty(l,s){return delete l[s],delete l._keys,delete n[0][s],!0},get(l,s){return W0(l,s,()=>_w(s,e,n,l))},getOwnPropertyDescriptor(l,s){return Reflect.getOwnPropertyDescriptor(l._scopes[0],s)},getPrototypeOf(){return Reflect.getPrototypeOf(n[0])},has(l,s){return td(l).includes(s)},ownKeys(l){return td(l)},set(l,s,a){const f=l._storage||(l._storage=o());return l[s]=f[s]=a,delete l._keys,!0}})}function Wo(n,e,t,i){const o={_cacheable:!1,_proxy:n,_context:e,_subProxy:t,_stack:new Set,_descriptors:U0(n,i),setContext:r=>Wo(n,r,t,i),override:r=>Wo(n.override(r),e,t,i)};return new Proxy(o,{deleteProperty(r,l){return delete r[l],delete n[l],!0},get(r,l,s){return W0(r,l,()=>dw(r,l,s))},getOwnPropertyDescriptor(r,l){return r._descriptors.allKeys?Reflect.has(n,l)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(n,l)},getPrototypeOf(){return Reflect.getPrototypeOf(n)},has(r,l){return Reflect.has(n,l)},ownKeys(){return Reflect.ownKeys(n)},set(r,l,s){return n[l]=s,delete r[l],!0}})}function U0(n,e={scriptable:!0,indexable:!0}){const{_scriptable:t=e.scriptable,_indexable:i=e.indexable,_allKeys:o=e.allKeys}=n;return{allKeys:o,scriptable:t,indexable:i,isScriptable:Vi(t)?t:()=>t,isIndexable:Vi(i)?i:()=>i}}const uw=(n,e)=>n?n+Vf(e):e,Qf=(n,e)=>dt(e)&&n!=="adapters"&&(Object.getPrototypeOf(e)===null||e.constructor===Object);function W0(n,e,t){if(Object.prototype.hasOwnProperty.call(n,e))return n[e];const i=t();return n[e]=i,i}function dw(n,e,t){const{_proxy:i,_context:o,_subProxy:r,_descriptors:l}=n;let s=i[e];return Vi(s)&&l.isScriptable(e)&&(s=pw(e,s,n,t)),Et(s)&&s.length&&(s=hw(e,s,n,l.isIndexable)),Qf(e,s)&&(s=Wo(s,o,r&&r[e],l)),s}function pw(n,e,t,i){const{_proxy:o,_context:r,_subProxy:l,_stack:s}=t;if(s.has(n))throw new Error("Recursion detected: "+Array.from(s).join("->")+"->"+n);return s.add(n),e=e(r,l||i),s.delete(n),Qf(n,e)&&(e=ec(o._scopes,o,n,e)),e}function hw(n,e,t,i){const{_proxy:o,_context:r,_subProxy:l,_descriptors:s}=t;if(Un(r.index)&&i(n))e=e[r.index%e.length];else if(dt(e[0])){const a=e,f=o._scopes.filter(c=>c!==a);e=[];for(const c of a){const u=ec(f,o,n,c);e.push(Wo(u,r,l&&l[n],s))}}return e}function Y0(n,e,t){return Vi(n)?n(e,t):n}const mw=(n,e)=>n===!0?e:typeof n=="string"?qi(e,n):void 0;function bw(n,e,t,i,o){for(const r of e){const l=mw(t,r);if(l){n.add(l);const s=Y0(l._fallback,t,o);if(Un(s)&&s!==t&&s!==i)return s}else if(l===!1&&Un(i)&&t!==i)return null}return!1}function ec(n,e,t,i){const o=e._rootScopes,r=Y0(e._fallback,t,i),l=[...n,...o],s=new Set;s.add(i);let a=ed(s,l,t,r||t,i);return a===null||Un(r)&&r!==t&&(a=ed(s,l,r,a,i),a===null)?!1:Xf(Array.from(s),[""],o,r,()=>gw(e,t,i))}function ed(n,e,t,i,o){for(;t;)t=bw(n,e,t,i,o);return t}function gw(n,e,t){const i=n._getTarget();e in i||(i[e]={});const o=i[e];return Et(o)&&dt(t)?t:o}function _w(n,e,t,i){let o;for(const r of e)if(o=G0(uw(r,n),t),Un(o))return Qf(n,o)?ec(t,i,n,o):o}function G0(n,e){for(const t of e){if(!t)continue;const i=t[n];if(Un(i))return i}}function td(n){let e=n._keys;return e||(e=n._keys=vw(n._scopes)),e}function vw(n){const e=new Set;for(const t of n)for(const i of Object.keys(t).filter(o=>!o.startsWith("_")))e.add(i);return Array.from(e)}function K0(n,e,t,i){const{iScale:o}=n,{key:r="r"}=this._parsing,l=new Array(i);let s,a,f,c;for(s=0,a=i;sen==="x"?"y":"x";function kw(n,e,t,i){const o=n.skip?e:n,r=e,l=t.skip?e:t,s=Qa(r,o),a=Qa(l,r);let f=s/(s+a),c=a/(s+a);f=isNaN(f)?0:f,c=isNaN(c)?0:c;const u=i*f,d=i*c;return{previous:{x:r.x-u*(l.x-o.x),y:r.y-u*(l.y-o.y)},next:{x:r.x+d*(l.x-o.x),y:r.y+d*(l.y-o.y)}}}function ww(n,e,t){const i=n.length;let o,r,l,s,a,f=Yo(n,0);for(let c=0;c!f.skip)),e.cubicInterpolationMode==="monotone")Cw(n,o);else{let f=i?n[n.length-1]:n[0];for(r=0,l=n.length;rwindow.getComputedStyle(n,null);function $w(n,e){return Os(n).getPropertyValue(e)}const Aw=["top","right","bottom","left"];function po(n,e,t){const i={};t=t?"-"+t:"";for(let o=0;o<4;o++){const r=Aw[o];i[r]=parseFloat(n[e+"-"+r+t])||0}return i.width=i.left+i.right,i.height=i.top+i.bottom,i}const Dw=(n,e,t)=>(n>0||e>0)&&(!t||!t.shadowRoot);function Ow(n,e){const t=n.touches,i=t&&t.length?t[0]:n,{offsetX:o,offsetY:r}=i;let l=!1,s,a;if(Dw(o,r,n.target))s=o,a=r;else{const f=e.getBoundingClientRect();s=i.clientX-f.left,a=i.clientY-f.top,l=!0}return{x:s,y:a,box:l}}function oo(n,e){if("native"in n)return n;const{canvas:t,currentDevicePixelRatio:i}=e,o=Os(t),r=o.boxSizing==="border-box",l=po(o,"padding"),s=po(o,"border","width"),{x:a,y:f,box:c}=Ow(n,t),u=l.left+(c&&s.left),d=l.top+(c&&s.top);let{width:h,height:b}=e;return r&&(h-=l.width+s.width,b-=l.height+s.height),{x:Math.round((a-u)/h*t.width/i),y:Math.round((f-d)/b*t.height/i)}}function Tw(n,e,t){let i,o;if(e===void 0||t===void 0){const r=tc(n);if(!r)e=n.clientWidth,t=n.clientHeight;else{const l=r.getBoundingClientRect(),s=Os(r),a=po(s,"border","width"),f=po(s,"padding");e=l.width-f.width-a.width,t=l.height-f.height-a.height,i=ps(s.maxWidth,r,"clientWidth"),o=ps(s.maxHeight,r,"clientHeight")}}return{width:e,height:t,maxWidth:i||as,maxHeight:o||as}}const da=n=>Math.round(n*10)/10;function Ew(n,e,t,i){const o=Os(n),r=po(o,"margin"),l=ps(o.maxWidth,n,"clientWidth")||as,s=ps(o.maxHeight,n,"clientHeight")||as,a=Tw(n,e,t);let{width:f,height:c}=a;if(o.boxSizing==="content-box"){const u=po(o,"border","width"),d=po(o,"padding");f-=d.width+u.width,c-=d.height+u.height}return f=Math.max(0,f-r.width),c=Math.max(0,i?Math.floor(f/i):c-r.height),f=da(Math.min(f,l,a.maxWidth)),c=da(Math.min(c,s,a.maxHeight)),f&&!c&&(c=da(f/2)),{width:f,height:c}}function nd(n,e,t){const i=e||1,o=Math.floor(n.height*i),r=Math.floor(n.width*i);n.height=o/i,n.width=r/i;const l=n.canvas;return l.style&&(t||!l.style.height&&!l.style.width)&&(l.style.height=`${n.height}px`,l.style.width=`${n.width}px`),n.currentDevicePixelRatio!==i||l.height!==o||l.width!==r?(n.currentDevicePixelRatio=i,l.height=o,l.width=r,n.ctx.setTransform(i,0,0,i,0,0),!0):!1}const Pw=function(){let n=!1;try{const e={get passive(){return n=!0,!1}};window.addEventListener("test",null,e),window.removeEventListener("test",null,e)}catch{}return n}();function id(n,e){const t=$w(n,e),i=t&&t.match(/^(\d+)(\.\d+)?px$/);return i?+i[1]:void 0}function ro(n,e,t,i){return{x:n.x+t*(e.x-n.x),y:n.y+t*(e.y-n.y)}}function Fw(n,e,t,i){return{x:n.x+t*(e.x-n.x),y:i==="middle"?t<.5?n.y:e.y:i==="after"?t<1?n.y:e.y:t>0?e.y:n.y}}function Lw(n,e,t,i){const o={x:n.cp2x,y:n.cp2y},r={x:e.cp1x,y:e.cp1y},l=ro(n,o,t),s=ro(o,r,t),a=ro(r,e,t),f=ro(l,s,t),c=ro(s,a,t);return ro(f,c,t)}const od=new Map;function Iw(n,e){e=e||{};const t=n+JSON.stringify(e);let i=od.get(t);return i||(i=new Intl.NumberFormat(n,e),od.set(t,i)),i}function Zr(n,e,t){return Iw(e,t).format(n)}const Rw=function(n,e){return{x(t){return n+n+e-t},setWidth(t){e=t},textAlign(t){return t==="center"?t:t==="right"?"left":"right"},xPlus(t,i){return t-i},leftForLtr(t,i){return t-i}}},Nw=function(){return{x(n){return n},setWidth(n){},textAlign(n){return n},xPlus(n,e){return n+e},leftForLtr(n,e){return n}}};function pa(n,e,t){return n?Rw(e,t):Nw()}function jw(n,e){let t,i;(e==="ltr"||e==="rtl")&&(t=n.canvas.style,i=[t.getPropertyValue("direction"),t.getPropertyPriority("direction")],t.setProperty("direction",e,"important"),n.prevTextDirection=i)}function zw(n,e){e!==void 0&&(delete n.prevTextDirection,n.canvas.style.setProperty("direction",e[0],e[1]))}function X0(n){return n==="angle"?{between:Rr,compare:Mk,normalize:Cn}:{between:Nr,compare:(e,t)=>e-t,normalize:e=>e}}function rd({start:n,end:e,count:t,loop:i,style:o}){return{start:n%t,end:e%t,loop:i&&(e-n+1)%t===0,style:o}}function Hw(n,e,t){const{property:i,start:o,end:r}=t,{between:l,normalize:s}=X0(i),a=e.length;let{start:f,end:c,loop:u}=n,d,h;if(u){for(f+=a,c+=a,d=0,h=a;da(o,C,y)&&s(o,C)!==0,M=()=>s(r,y)===0||a(r,C,y),A=()=>v||x(),O=()=>!v||M();for(let D=c,E=c;D<=u;++D)S=e[D%l],!S.skip&&(y=f(S[i]),y!==C&&(v=a(y,o,r),_===null&&A()&&(_=s(y,o)===0?D:E),_!==null&&O()&&(b.push(rd({start:_,end:D,loop:d,count:l,style:h})),_=null),E=D,C=y));return _!==null&&b.push(rd({start:_,end:u,loop:d,count:l,style:h})),b}function e1(n,e){const t=[],i=n.segments;for(let o=0;oo&&n[r%e].skip;)r--;return r%=e,{start:o,end:r}}function Vw(n,e,t,i){const o=n.length,r=[];let l=e,s=n[e],a;for(a=e+1;a<=t;++a){const f=n[a%o];f.skip||f.stop?s.skip||(i=!1,r.push({start:e%o,end:(a-1)%o,loop:i}),e=l=f.stop?a:null):(l=a,s.skip&&(e=a)),s=f}return l!==null&&r.push({start:e%o,end:l%o,loop:i}),r}function Bw(n,e){const t=n.points,i=n.options.spanGaps,o=t.length;if(!o)return[];const r=!!n._loop,{start:l,end:s}=qw(t,o,r,i);if(i===!0)return ld(n,[{start:l,end:s,loop:r}],t,e);const a=ss({chart:e,initial:t.initial,numSteps:l,currentStep:Math.min(i-t.start,l)}))}_refresh(){this._request||(this._running=!0,this._request=T0.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(e=Date.now()){let t=0;this._charts.forEach((i,o)=>{if(!i.running||!i.items.length)return;const r=i.items;let l=r.length-1,s=!1,a;for(;l>=0;--l)a=r[l],a._active?(a._total>i.duration&&(i.duration=a._total),a.tick(e),s=!0):(r[l]=r[r.length-1],r.pop());s&&(o.draw(),this._notify(o,i,e,"progress")),r.length||(i.running=!1,this._notify(o,i,e,"complete"),i.initial=!1),t+=r.length}),this._lastDate=e,t===0&&(this._running=!1)}_getAnims(e){const t=this._charts;let i=t.get(e);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},t.set(e,i)),i}listen(e,t,i){this._getAnims(e).listeners[t].push(i)}add(e,t){!t||!t.length||this._getAnims(e).items.push(...t)}has(e){return this._getAnims(e).items.length>0}start(e){const t=this._charts.get(e);!t||(t.running=!0,t.start=Date.now(),t.duration=t.items.reduce((i,o)=>Math.max(i,o._duration),0),this._refresh())}running(e){if(!this._running)return!1;const t=this._charts.get(e);return!(!t||!t.running||!t.items.length)}stop(e){const t=this._charts.get(e);if(!t||!t.items.length)return;const i=t.items;let o=i.length-1;for(;o>=0;--o)i[o].cancel();t.items=[],this._notify(e,t,Date.now(),"complete")}remove(e){return this._charts.delete(e)}}var bi=new Yw;const ad="transparent",Gw={boolean(n,e,t){return t>.5?e:n},color(n,e,t){const i=Zu(n||ad),o=i.valid&&Zu(e||ad);return o&&o.valid?o.mix(i,t).hexString():e},number(n,e,t){return n+(e-n)*t}};class Kw{constructor(e,t,i,o){const r=t[i];o=xl([e.to,o,r,e.from]);const l=xl([e.from,r,o]);this._active=!0,this._fn=e.fn||Gw[e.type||typeof l],this._easing=Ar[e.easing]||Ar.linear,this._start=Math.floor(Date.now()+(e.delay||0)),this._duration=this._total=Math.floor(e.duration),this._loop=!!e.loop,this._target=t,this._prop=i,this._from=l,this._to=o,this._promises=void 0}active(){return this._active}update(e,t,i){if(this._active){this._notify(!1);const o=this._target[this._prop],r=i-this._start,l=this._duration-r;this._start=i,this._duration=Math.floor(Math.max(l,e.duration)),this._total+=r,this._loop=!!e.loop,this._to=xl([e.to,t,o,e.from]),this._from=xl([e.from,o,t])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(e){const t=e-this._start,i=this._duration,o=this._prop,r=this._from,l=this._loop,s=this._to;let a;if(this._active=r!==s&&(l||t1?2-a:a,a=this._easing(Math.min(1,Math.max(0,a))),this._target[o]=this._fn(r,s,a)}wait(){const e=this._promises||(this._promises=[]);return new Promise((t,i)=>{e.push({res:t,rej:i})})}_notify(e){const t=e?"res":"rej",i=this._promises||[];for(let o=0;on!=="onProgress"&&n!=="onComplete"&&n!=="fn"});mt.set("animations",{colors:{type:"color",properties:Zw},numbers:{type:"number",properties:Jw}});mt.describe("animations",{_fallback:"animation"});mt.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:n=>n|0}}}});class t1{constructor(e,t){this._chart=e,this._properties=new Map,this.configure(t)}configure(e){if(!dt(e))return;const t=this._properties;Object.getOwnPropertyNames(e).forEach(i=>{const o=e[i];if(!dt(o))return;const r={};for(const l of Xw)r[l]=o[l];(Et(o.properties)&&o.properties||[i]).forEach(l=>{(l===i||!t.has(l))&&t.set(l,r)})})}_animateOptions(e,t){const i=t.options,o=e3(e,i);if(!o)return[];const r=this._createAnimations(o,i);return i.$shared&&Qw(e.options.$animations,i).then(()=>{e.options=i},()=>{}),r}_createAnimations(e,t){const i=this._properties,o=[],r=e.$animations||(e.$animations={}),l=Object.keys(t),s=Date.now();let a;for(a=l.length-1;a>=0;--a){const f=l[a];if(f.charAt(0)==="$")continue;if(f==="options"){o.push(...this._animateOptions(e,t));continue}const c=t[f];let u=r[f];const d=i.get(f);if(u)if(d&&u.active()){u.update(d,c,s);continue}else u.cancel();if(!d||!d.duration){e[f]=c;continue}r[f]=u=new Kw(d,e,f,c),o.push(u)}return o}update(e,t){if(this._properties.size===0){Object.assign(e,t);return}const i=this._createAnimations(e,t);if(i.length)return bi.add(this._chart,i),!0}}function Qw(n,e){const t=[],i=Object.keys(e);for(let o=0;o0||!t&&r<0)return o.index}return null}function pd(n,e){const{chart:t,_cachedMeta:i}=n,o=t._stacks||(t._stacks={}),{iScale:r,vScale:l,index:s}=i,a=r.axis,f=l.axis,c=o3(r,l,i),u=e.length;let d;for(let h=0;ht[i].axis===e).shift()}function s3(n,e){return Wi(n,{active:!1,dataset:void 0,datasetIndex:e,index:e,mode:"default",type:"dataset"})}function a3(n,e,t){return Wi(n,{active:!1,dataIndex:e,parsed:void 0,raw:void 0,element:t,index:e,mode:"default",type:"data"})}function cr(n,e){const t=n.controller.index,i=n.vScale&&n.vScale.axis;if(!!i){e=e||n._parsed;for(const o of e){const r=o._stacks;if(!r||r[i]===void 0||r[i][t]===void 0)return;delete r[i][t]}}}const ma=n=>n==="reset"||n==="none",hd=(n,e)=>e?n:Object.assign({},n),f3=(n,e,t)=>n&&!e.hidden&&e._stacked&&{keys:n1(t,!0),values:null};class hi{constructor(e,t){this.chart=e,this._ctx=e.ctx,this.index=t,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.initialize()}initialize(){const e=this._cachedMeta;this.configure(),this.linkScales(),e._stacked=ud(e.vScale,e),this.addElements()}updateIndex(e){this.index!==e&&cr(this._cachedMeta),this.index=e}linkScales(){const e=this.chart,t=this._cachedMeta,i=this.getDataset(),o=(u,d,h,b)=>u==="x"?d:u==="r"?b:h,r=t.xAxisID=ht(i.xAxisID,ha(e,"x")),l=t.yAxisID=ht(i.yAxisID,ha(e,"y")),s=t.rAxisID=ht(i.rAxisID,ha(e,"r")),a=t.indexAxis,f=t.iAxisID=o(a,r,l,s),c=t.vAxisID=o(a,l,r,s);t.xScale=this.getScaleForId(r),t.yScale=this.getScaleForId(l),t.rScale=this.getScaleForId(s),t.iScale=this.getScaleForId(f),t.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(e){return this.chart.scales[e]}_getOtherScale(e){const t=this._cachedMeta;return e===t.iScale?t.vScale:t.iScale}reset(){this._update("reset")}_destroy(){const e=this._cachedMeta;this._data&&Qu(this._data,this),e._stacked&&cr(e)}_dataCheck(){const e=this.getDataset(),t=e.data||(e.data=[]),i=this._data;if(dt(t))this._data=i3(t);else if(i!==t){if(i){Qu(i,this);const o=this._cachedMeta;cr(o),o._parsed=[]}t&&Object.isExtensible(t)&&cw(t,this),this._syncList=[],this._data=t}}addElements(){const e=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(e.dataset=new this.datasetElementType)}buildOrUpdateElements(e){const t=this._cachedMeta,i=this.getDataset();let o=!1;this._dataCheck();const r=t._stacked;t._stacked=ud(t.vScale,t),t.stack!==i.stack&&(o=!0,cr(t),t.stack=i.stack),this._resyncElements(e),(o||r!==t._stacked)&&pd(this,t._parsed)}configure(){const e=this.chart.config,t=e.datasetScopeKeys(this._type),i=e.getOptionScopes(this.getDataset(),t,!0);this.options=e.createResolver(i,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(e,t){const{_cachedMeta:i,_data:o}=this,{iScale:r,_stacked:l}=i,s=r.axis;let a=e===0&&t===o.length?!0:i._sorted,f=e>0&&i._parsed[e-1],c,u,d;if(this._parsing===!1)i._parsed=o,i._sorted=!0,d=o;else{Et(o[e])?d=this.parseArrayData(i,o,e,t):dt(o[e])?d=this.parseObjectData(i,o,e,t):d=this.parsePrimitiveData(i,o,e,t);const h=()=>u[s]===null||f&&u[s]v||u=0;--d)if(!b()){this.updateRangeFromParsed(f,e,h,a);break}}return f}getAllParsedValues(e){const t=this._cachedMeta._parsed,i=[];let o,r,l;for(o=0,r=t.length;o=0&&ethis.getContext(i,o),v=f.resolveNamedOptions(d,h,b,u);return v.$shared&&(v.$shared=a,r[l]=Object.freeze(hd(v,a))),v}_resolveAnimations(e,t,i){const o=this.chart,r=this._cachedDataOpts,l=`animation-${t}`,s=r[l];if(s)return s;let a;if(o.options.animation!==!1){const c=this.chart.config,u=c.datasetAnimationScopeKeys(this._type,t),d=c.getOptionScopes(this.getDataset(),u);a=c.createResolver(d,this.getContext(e,i,t))}const f=new t1(o,a&&a.animations);return a&&a._cacheable&&(r[l]=Object.freeze(f)),f}getSharedOptions(e){if(!!e.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},e))}includeOptions(e,t){return!t||ma(e)||this.chart._animationsDisabled}updateElement(e,t,i,o){ma(o)?Object.assign(e,i):this._resolveAnimations(t,o).update(e,i)}updateSharedOptions(e,t,i){e&&!ma(t)&&this._resolveAnimations(void 0,t).update(e,i)}_setStyle(e,t,i,o){e.active=o;const r=this.getStyle(t,o);this._resolveAnimations(t,i,o).update(e,{options:!o&&this.getSharedOptions(r)||r})}removeHoverStyle(e,t,i){this._setStyle(e,i,"active",!1)}setHoverStyle(e,t,i){this._setStyle(e,i,"active",!0)}_removeDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!1)}_setDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!0)}_resyncElements(e){const t=this._data,i=this._cachedMeta.data;for(const[s,a,f]of this._syncList)this[s](a,f);this._syncList=[];const o=i.length,r=t.length,l=Math.min(r,o);l&&this.parse(0,l),r>o?this._insertElements(o,r-o,e):r{for(f.length+=t,s=f.length-1;s>=l;s--)f[s]=f[s-t]};for(a(r),s=e;so-r))}return n._cache.$bar}function u3(n){const e=n.iScale,t=c3(e,n.type);let i=e._length,o,r,l,s;const a=()=>{l===32767||l===-32768||(Un(s)&&(i=Math.min(i,Math.abs(l-s)||i)),s=l)};for(o=0,r=t.length;o0?o[n-1]:null,s=nMath.abs(s)&&(a=s,f=l),e[t.axis]=f,e._custom={barStart:a,barEnd:f,start:o,end:r,min:l,max:s}}function i1(n,e,t,i){return Et(n)?h3(n,e,t,i):e[t.axis]=t.parse(n,i),e}function md(n,e,t,i){const o=n.iScale,r=n.vScale,l=o.getLabels(),s=o===r,a=[];let f,c,u,d;for(f=t,c=t+i;f=t?1:-1)}function b3(n){let e,t,i,o,r;return n.horizontal?(e=n.base>n.x,t="left",i="right"):(e=n.base=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}getLabelAndValue(e){const t=this._cachedMeta,{xScale:i,yScale:o}=t,r=this.getParsed(e),l=i.getLabelForValue(r.x),s=o.getLabelForValue(r.y),a=r._custom;return{label:t.label,value:"("+l+", "+s+(a?", "+a:"")+")"}}update(e){const t=this._cachedMeta.data;this.updateElements(t,0,t.length,e)}updateElements(e,t,i,o){const r=o==="reset",{iScale:l,vScale:s}=this._cachedMeta,a=this.resolveDataElementOptions(t,o),f=this.getSharedOptions(a),c=this.includeOptions(o,f),u=l.axis,d=s.axis;for(let h=t;hRr(C,s,a,!0)?1:Math.max(x,x*t,M,M*t),b=(C,x,M)=>Rr(C,s,a,!0)?-1:Math.min(x,x*t,M,M*t),v=h(0,f,u),_=h(Nt,c,d),y=b(jt,f,u),S=b(jt+Nt,c,d);i=(v-y)/2,o=(_-S)/2,r=-(v+y)/2,l=-(_+S)/2}return{ratioX:i,ratioY:o,offsetX:r,offsetY:l}}class Xr extends hi{constructor(e,t){super(e,t),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(e,t){const i=this.getDataset().data,o=this._cachedMeta;if(this._parsing===!1)o._parsed=i;else{let r=a=>+i[a];if(dt(i[e])){const{key:a="value"}=this._parsing;r=f=>+qi(i[f],a)}let l,s;for(l=e,s=e+t;l0&&!isNaN(e)?Tt*(Math.abs(e)/t):0}getLabelAndValue(e){const t=this._cachedMeta,i=this.chart,o=i.data.labels||[],r=Zr(t._parsed[e],i.options.locale);return{label:o[e]||"",value:r}}getMaxBorderWidth(e){let t=0;const i=this.chart;let o,r,l,s,a;if(!e){for(o=0,r=i.data.datasets.length;on!=="spacing",_indexable:n=>n!=="spacing"};Xr.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(n){const e=n.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:t}}=n.legend.options;return e.labels.map((i,o)=>{const l=n.getDatasetMeta(0).controller.getStyle(o);return{text:i,fillStyle:l.backgroundColor,strokeStyle:l.borderColor,lineWidth:l.borderWidth,pointStyle:t,hidden:!n.getDataVisibility(o),index:o}})}return[]}},onClick(n,e,t){t.chart.toggleDataVisibility(e.index),t.chart.update()}},tooltip:{callbacks:{title(){return""},label(n){let e=n.label;const t=": "+n.formattedValue;return Et(e)?(e=e.slice(),e[0]+=t):e+=t,e}}}}};class Qr extends hi{initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(e){const t=this._cachedMeta,{dataset:i,data:o=[],_dataset:r}=t,l=this.chart._animationsDisabled;let{start:s,count:a}=k3(t,o,l);this._drawStart=s,this._drawCount=a,w3(t)&&(s=0,a=o.length),i._chart=this.chart,i._datasetIndex=this.index,i._decimated=!!r._decimated,i.points=o;const f=this.resolveDatasetElementOptions(e);this.options.showLine||(f.borderWidth=0),f.segment=this.options.segment,this.updateElement(i,void 0,{animated:!l,options:f},e),this.updateElements(o,s,a,e)}updateElements(e,t,i,o){const r=o==="reset",{iScale:l,vScale:s,_stacked:a,_dataset:f}=this._cachedMeta,c=this.resolveDataElementOptions(t,o),u=this.getSharedOptions(c),d=this.includeOptions(o,u),h=l.axis,b=s.axis,{spanGaps:v,segment:_}=this.options,y=Ir(v)?v:Number.POSITIVE_INFINITY,S=this.chart._animationsDisabled||r||o==="none";let C=t>0&&this.getParsed(t-1);for(let x=t;x0&&Math.abs(A[h]-C[h])>y,_&&(O.parsed=A,O.raw=f.data[x]),d&&(O.options=u||this.resolveDataElementOptions(x,M.active?"active":o)),S||this.updateElement(M,x,O,o),C=A}this.updateSharedOptions(u,o,c)}getMaxOverflow(){const e=this._cachedMeta,t=e.dataset,i=t.options&&t.options.borderWidth||0,o=e.data||[];if(!o.length)return i;const r=o[0].size(this.resolveDataElementOptions(0)),l=o[o.length-1].size(this.resolveDataElementOptions(o.length-1));return Math.max(i,r,l)/2}draw(){const e=this._cachedMeta;e.dataset.updateControlPoints(this.chart.chartArea,e.iScale.axis),super.draw()}}Qr.id="line";Qr.defaults={datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1};Qr.overrides={scales:{_index_:{type:"category"},_value_:{type:"linear"}}};function k3(n,e,t){const i=e.length;let o=0,r=i;if(n._sorted){const{iScale:l,_parsed:s}=n,a=l.axis,{min:f,max:c,minDefined:u,maxDefined:d}=l.getUserBounds();u&&(o=sn(Math.min(ao(s,l.axis,f).lo,t?i:ao(e,a,l.getPixelForValue(f)).lo),0,i-1)),d?r=sn(Math.max(ao(s,l.axis,c).hi+1,t?0:ao(e,a,l.getPixelForValue(c)).hi+1),o,i)-o:r=i-o}return{start:o,count:r}}function w3(n){const{xScale:e,yScale:t,_scaleRanges:i}=n,o={xmin:e.min,xmax:e.max,ymin:t.min,ymax:t.max};if(!i)return n._scaleRanges=o,!0;const r=i.xmin!==e.min||i.xmax!==e.max||i.ymin!==t.min||i.ymax!==t.max;return Object.assign(i,o),r}class oc extends hi{constructor(e,t){super(e,t),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(e){const t=this._cachedMeta,i=this.chart,o=i.data.labels||[],r=Zr(t._parsed[e].r,i.options.locale);return{label:o[e]||"",value:r}}parseObjectData(e,t,i,o){return K0.bind(this)(e,t,i,o)}update(e){const t=this._cachedMeta.data;this._updateRadius(),this.updateElements(t,0,t.length,e)}getMinMax(){const e=this._cachedMeta,t={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return e.data.forEach((i,o)=>{const r=this.getParsed(o).r;!isNaN(r)&&this.chart.getDataVisibility(o)&&(rt.max&&(t.max=r))}),t}_updateRadius(){const e=this.chart,t=e.chartArea,i=e.options,o=Math.min(t.right-t.left,t.bottom-t.top),r=Math.max(o/2,0),l=Math.max(i.cutoutPercentage?r/100*i.cutoutPercentage:1,0),s=(r-l)/e.getVisibleDatasetCount();this.outerRadius=r-s*this.index,this.innerRadius=this.outerRadius-s}updateElements(e,t,i,o){const r=o==="reset",l=this.chart,a=l.options.animation,f=this._cachedMeta.rScale,c=f.xCenter,u=f.yCenter,d=f.getIndexAngle(0)-.5*jt;let h=d,b;const v=360/this.countVisibleElements();for(b=0;b{!isNaN(this.getParsed(o).r)&&this.chart.getDataVisibility(o)&&t++}),t}_computeAngle(e,t,i){return this.chart.getDataVisibility(e)?Xn(this.resolveDataElementOptions(e,t).angle||i):0}}oc.id="polarArea";oc.defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0};oc.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(n){const e=n.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:t}}=n.legend.options;return e.labels.map((i,o)=>{const l=n.getDatasetMeta(0).controller.getStyle(o);return{text:i,fillStyle:l.backgroundColor,strokeStyle:l.borderColor,lineWidth:l.borderWidth,pointStyle:t,hidden:!n.getDataVisibility(o),index:o}})}return[]}},onClick(n,e,t){t.chart.toggleDataVisibility(e.index),t.chart.update()}},tooltip:{callbacks:{title(){return""},label(n){return n.chart.data.labels[n.dataIndex]+": "+n.formattedValue}}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};class o1 extends Xr{}o1.id="pie";o1.defaults={cutout:0,rotation:0,circumference:360,radius:"100%"};class rc extends hi{getLabelAndValue(e){const t=this._cachedMeta.vScale,i=this.getParsed(e);return{label:t.getLabels()[e],value:""+t.getLabelForValue(i[t.axis])}}parseObjectData(e,t,i,o){return K0.bind(this)(e,t,i,o)}update(e){const t=this._cachedMeta,i=t.dataset,o=t.data||[],r=t.iScale.getLabels();if(i.points=o,e!=="resize"){const l=this.resolveDatasetElementOptions(e);this.options.showLine||(l.borderWidth=0);const s={_loop:!0,_fullLoop:r.length===o.length,options:l};this.updateElement(i,void 0,s,e)}this.updateElements(o,0,o.length,e)}updateElements(e,t,i,o){const r=this._cachedMeta.rScale,l=o==="reset";for(let s=t;s{a[l](e[t],o)&&(r.push({element:a,datasetIndex:f,index:c}),s=s||a.inRange(e.x,e.y,o))}),i&&!s?[]:r}var $3={evaluateInteractionItems:el,modes:{index(n,e,t,i){const o=oo(e,n),r=t.axis||"x",l=t.includeInvisible||!1,s=t.intersect?ga(n,o,r,i,l):_a(n,o,r,!1,i,l),a=[];return s.length?(n.getSortedVisibleDatasetMetas().forEach(f=>{const c=s[0].index,u=f.data[c];u&&!u.skip&&a.push({element:u,datasetIndex:f.index,index:c})}),a):[]},dataset(n,e,t,i){const o=oo(e,n),r=t.axis||"xy",l=t.includeInvisible||!1;let s=t.intersect?ga(n,o,r,i,l):_a(n,o,r,!1,i,l);if(s.length>0){const a=s[0].datasetIndex,f=n.getDatasetMeta(a).data;s=[];for(let c=0;ct.pos===e)}function vd(n,e){return n.filter(t=>l1.indexOf(t.pos)===-1&&t.box.axis===e)}function dr(n,e){return n.sort((t,i)=>{const o=e?i:t,r=e?t:i;return o.weight===r.weight?o.index-r.index:o.weight-r.weight})}function A3(n){const e=[];let t,i,o,r,l,s;for(t=0,i=(n||[]).length;tf.box.fullSize),!0),i=dr(ur(e,"left"),!0),o=dr(ur(e,"right")),r=dr(ur(e,"top"),!0),l=dr(ur(e,"bottom")),s=vd(e,"x"),a=vd(e,"y");return{fullSize:t,leftAndTop:i.concat(r),rightAndBottom:o.concat(a).concat(l).concat(s),chartArea:ur(e,"chartArea"),vertical:i.concat(o).concat(a),horizontal:r.concat(l).concat(s)}}function yd(n,e,t,i){return Math.max(n[t],e[t])+Math.max(n[i],e[i])}function s1(n,e){n.top=Math.max(n.top,e.top),n.left=Math.max(n.left,e.left),n.bottom=Math.max(n.bottom,e.bottom),n.right=Math.max(n.right,e.right)}function E3(n,e,t,i){const{pos:o,box:r}=t,l=n.maxPadding;if(!dt(o)){t.size&&(n[o]-=t.size);const u=i[t.stack]||{size:0,count:1};u.size=Math.max(u.size,t.horizontal?r.height:r.width),t.size=u.size/u.count,n[o]+=t.size}r.getPadding&&s1(l,r.getPadding());const s=Math.max(0,e.outerWidth-yd(l,n,"left","right")),a=Math.max(0,e.outerHeight-yd(l,n,"top","bottom")),f=s!==n.w,c=a!==n.h;return n.w=s,n.h=a,t.horizontal?{same:f,other:c}:{same:c,other:f}}function P3(n){const e=n.maxPadding;function t(i){const o=Math.max(e[i]-n[i],0);return n[i]+=o,o}n.y+=t("top"),n.x+=t("left"),t("right"),t("bottom")}function F3(n,e){const t=e.maxPadding;function i(o){const r={left:0,top:0,right:0,bottom:0};return o.forEach(l=>{r[l]=Math.max(e[l],t[l])}),r}return i(n?["left","right"]:["top","bottom"])}function kr(n,e,t,i){const o=[];let r,l,s,a,f,c;for(r=0,l=n.length,f=0;r{typeof v.beforeLayout=="function"&&v.beforeLayout()});const c=a.reduce((v,_)=>_.box.options&&_.box.options.display===!1?v:v+1,0)||1,u=Object.freeze({outerWidth:e,outerHeight:t,padding:o,availableWidth:r,availableHeight:l,vBoxMaxWidth:r/2/c,hBoxMaxHeight:l/2}),d=Object.assign({},o);s1(d,Wn(i));const h=Object.assign({maxPadding:d,w:r,h:l,x:o.left,y:o.top},o),b=O3(a.concat(f),u);kr(s.fullSize,h,u,b),kr(a,h,u,b),kr(f,h,u,b)&&kr(a,h,u,b),P3(h),kd(s.leftAndTop,h,u,b),h.x+=h.w,h.y+=h.h,kd(s.rightAndBottom,h,u,b),n.chartArea={left:h.left,top:h.top,right:h.left+h.w,bottom:h.top+h.h,height:h.h,width:h.w},Ct(s.chartArea,v=>{const _=v.box;Object.assign(_,n.chartArea),_.update(h.w,h.h,{left:0,top:0,right:0,bottom:0})})}};class a1{acquireContext(e,t){}releaseContext(e){return!1}addEventListener(e,t,i){}removeEventListener(e,t,i){}getDevicePixelRatio(){return 1}getMaximumSize(e,t,i,o){return t=Math.max(0,t||e.width),i=i||e.height,{width:t,height:Math.max(0,o?Math.floor(t/o):i)}}isAttached(e){return!0}updateConfig(e){}}class L3 extends a1{acquireContext(e){return e&&e.getContext&&e.getContext("2d")||null}updateConfig(e){e.options.animation=!1}}const Gl="$chartjs",I3={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},wd=n=>n===null||n==="";function R3(n,e){const t=n.style,i=n.getAttribute("height"),o=n.getAttribute("width");if(n[Gl]={initial:{height:i,width:o,style:{display:t.display,height:t.height,width:t.width}}},t.display=t.display||"block",t.boxSizing=t.boxSizing||"border-box",wd(o)){const r=id(n,"width");r!==void 0&&(n.width=r)}if(wd(i))if(n.style.height==="")n.height=n.width/(e||2);else{const r=id(n,"height");r!==void 0&&(n.height=r)}return n}const f1=Pw?{passive:!0}:!1;function N3(n,e,t){n.addEventListener(e,t,f1)}function j3(n,e,t){n.canvas.removeEventListener(e,t,f1)}function z3(n,e){const t=I3[n.type]||n.type,{x:i,y:o}=oo(n,e);return{type:t,chart:e,native:n,x:i!==void 0?i:null,y:o!==void 0?o:null}}function hs(n,e){for(const t of n)if(t===e||t.contains(e))return!0}function H3(n,e,t){const i=n.canvas,o=new MutationObserver(r=>{let l=!1;for(const s of r)l=l||hs(s.addedNodes,i),l=l&&!hs(s.removedNodes,i);l&&t()});return o.observe(document,{childList:!0,subtree:!0}),o}function q3(n,e,t){const i=n.canvas,o=new MutationObserver(r=>{let l=!1;for(const s of r)l=l||hs(s.removedNodes,i),l=l&&!hs(s.addedNodes,i);l&&t()});return o.observe(document,{childList:!0,subtree:!0}),o}const zr=new Map;let Sd=0;function c1(){const n=window.devicePixelRatio;n!==Sd&&(Sd=n,zr.forEach((e,t)=>{t.currentDevicePixelRatio!==n&&e()}))}function V3(n,e){zr.size||window.addEventListener("resize",c1),zr.set(n,e)}function B3(n){zr.delete(n),zr.size||window.removeEventListener("resize",c1)}function U3(n,e,t){const i=n.canvas,o=i&&tc(i);if(!o)return;const r=E0((s,a)=>{const f=o.clientWidth;t(s,a),f{const a=s[0],f=a.contentRect.width,c=a.contentRect.height;f===0&&c===0||r(f,c)});return l.observe(o),V3(n,r),l}function va(n,e,t){t&&t.disconnect(),e==="resize"&&B3(n)}function W3(n,e,t){const i=n.canvas,o=E0(r=>{n.ctx!==null&&t(z3(r,n))},n,r=>{const l=r[0];return[l,l.offsetX,l.offsetY]});return N3(i,e,o),o}class Y3 extends a1{acquireContext(e,t){const i=e&&e.getContext&&e.getContext("2d");return i&&i.canvas===e?(R3(e,t),i):null}releaseContext(e){const t=e.canvas;if(!t[Gl])return!1;const i=t[Gl].initial;["height","width"].forEach(r=>{const l=i[r];xt(l)?t.removeAttribute(r):t.setAttribute(r,l)});const o=i.style||{};return Object.keys(o).forEach(r=>{t.style[r]=o[r]}),t.width=t.width,delete t[Gl],!0}addEventListener(e,t,i){this.removeEventListener(e,t);const o=e.$proxies||(e.$proxies={}),l={attach:H3,detach:q3,resize:U3}[t]||W3;o[t]=l(e,t,i)}removeEventListener(e,t){const i=e.$proxies||(e.$proxies={}),o=i[t];if(!o)return;({attach:va,detach:va,resize:va}[t]||j3)(e,t,o),i[t]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,t,i,o){return Ew(e,t,i,o)}isAttached(e){const t=tc(e);return!!(t&&t.isConnected)}}function G3(n){return!Z0()||typeof OffscreenCanvas!="undefined"&&n instanceof OffscreenCanvas?L3:Y3}class $i{constructor(){this.x=void 0,this.y=void 0,this.active=!1,this.options=void 0,this.$animations=void 0}tooltipPosition(e){const{x:t,y:i}=this.getProps(["x","y"],e);return{x:t,y:i}}hasValue(){return Ir(this.x)&&Ir(this.y)}getProps(e,t){const i=this.$animations;if(!t||!i)return this;const o={};return e.forEach(r=>{o[r]=i[r]&&i[r].active()?i[r]._to:this[r]}),o}}$i.defaults={};$i.defaultRoutes=void 0;const u1={values(n){return Et(n)?n:""+n},numeric(n,e,t){if(n===0)return"0";const i=this.chart.options.locale;let o,r=n;if(t.length>1){const f=Math.max(Math.abs(t[0].value),Math.abs(t[t.length-1].value));(f<1e-4||f>1e15)&&(o="scientific"),r=K3(n,t)}const l=zn(Math.abs(r)),s=Math.max(Math.min(-1*Math.floor(l),20),0),a={notation:o,minimumFractionDigits:s,maximumFractionDigits:s};return Object.assign(a,this.options.ticks.format),Zr(n,i,a)},logarithmic(n,e,t){if(n===0)return"0";const i=n/Math.pow(10,Math.floor(zn(n)));return i===1||i===2||i===5?u1.numeric.call(this,n,e,t):""}};function K3(n,e){let t=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;return Math.abs(t)>=1&&n!==Math.floor(n)&&(t=n-Math.floor(n)),t}var Ts={formatters:u1};mt.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",grace:0,grid:{display:!0,lineWidth:1,drawBorder:!0,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(n,e)=>e.lineWidth,tickColor:(n,e)=>e.color,offset:!1,borderDash:[],borderDashOffset:0,borderWidth:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Ts.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}});mt.route("scale.ticks","color","","color");mt.route("scale.grid","color","","borderColor");mt.route("scale.grid","borderColor","","borderColor");mt.route("scale.title","color","","color");mt.describe("scale",{_fallback:!1,_scriptable:n=>!n.startsWith("before")&&!n.startsWith("after")&&n!=="callback"&&n!=="parser",_indexable:n=>n!=="borderDash"&&n!=="tickBorderDash"});mt.describe("scales",{_fallback:"scale"});mt.describe("scale.ticks",{_scriptable:n=>n!=="backdropPadding"&&n!=="callback",_indexable:n=>n!=="backdropPadding"});function J3(n,e){const t=n.options.ticks,i=t.maxTicksLimit||Z3(n),o=t.major.enabled?Q3(e):[],r=o.length,l=o[0],s=o[r-1],a=[];if(r>i)return eS(e,a,o,r/i),a;const f=X3(o,e,i);if(r>0){let c,u;const d=r>1?Math.round((s-l)/(r-1)):null;for(Dl(e,a,f,xt(d)?0:l-d,l),c=0,u=r-1;co)return a}return Math.max(o,1)}function Q3(n){const e=[];let t,i;for(t=0,i=n.length;tn==="left"?"right":n==="right"?"left":n,Cd=(n,e,t)=>e==="top"||e==="left"?n[e]+t:n[e]-t;function xd(n,e){const t=[],i=n.length/e,o=n.length;let r=0;for(;rl+s)))return a}function oS(n,e){Ct(n,t=>{const i=t.gc,o=i.length/2;let r;if(o>e){for(r=0;ri?i:t,i=o&&t>i?t:i,{min:Rn(t,Rn(i,t)),max:Rn(i,Rn(t,i))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const e=this.chart.data;return this.options.labels||(this.isHorizontal()?e.xLabels:e.yLabels)||e.labels||[]}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){Rt(this.options.beforeUpdate,[this])}update(e,t,i){const{beginAtZero:o,grace:r,ticks:l}=this.options,s=l.sampleSize;this.beforeUpdate(),this.maxWidth=e,this.maxHeight=t,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=sw(this,r,o),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const a=s=r||i<=1||!this.isHorizontal()){this.labelRotation=o;return}const c=this._getLabelSizes(),u=c.widest.width,d=c.highest.height,h=sn(this.chart.width-u,0,this.maxWidth);s=e.offset?this.maxWidth/i:h/(i-1),u+6>s&&(s=h/(i-(e.offset?.5:1)),a=this.maxHeight-pr(e.grid)-t.padding-Md(e.title,this.chart.options.font),f=Math.sqrt(u*u+d*d),l=Bf(Math.min(Math.asin(sn((c.highest.height+6)/s,-1,1)),Math.asin(sn(a/f,-1,1))-Math.asin(sn(d/f,-1,1)))),l=Math.max(o,Math.min(r,l))),this.labelRotation=l}afterCalculateLabelRotation(){Rt(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){Rt(this.options.beforeFit,[this])}fit(){const e={width:0,height:0},{chart:t,options:{ticks:i,title:o,grid:r}}=this,l=this._isVisible(),s=this.isHorizontal();if(l){const a=Md(o,t.options.font);if(s?(e.width=this.maxWidth,e.height=pr(r)+a):(e.height=this.maxHeight,e.width=pr(r)+a),i.display&&this.ticks.length){const{first:f,last:c,widest:u,highest:d}=this._getLabelSizes(),h=i.padding*2,b=Xn(this.labelRotation),v=Math.cos(b),_=Math.sin(b);if(s){const y=i.mirror?0:_*u.width+v*d.height;e.height=Math.min(this.maxHeight,e.height+y+h)}else{const y=i.mirror?0:v*u.width+_*d.height;e.width=Math.min(this.maxWidth,e.width+y+h)}this._calculatePadding(f,c,_,v)}}this._handleMargins(),s?(this.width=this._length=t.width-this._margins.left-this._margins.right,this.height=e.height):(this.width=e.width,this.height=this._length=t.height-this._margins.top-this._margins.bottom)}_calculatePadding(e,t,i,o){const{ticks:{align:r,padding:l},position:s}=this.options,a=this.labelRotation!==0,f=s!=="top"&&this.axis==="x";if(this.isHorizontal()){const c=this.getPixelForTick(0)-this.left,u=this.right-this.getPixelForTick(this.ticks.length-1);let d=0,h=0;a?f?(d=o*e.width,h=i*t.height):(d=i*e.height,h=o*t.width):r==="start"?h=t.width:r==="end"?d=e.width:r!=="inner"&&(d=e.width/2,h=t.width/2),this.paddingLeft=Math.max((d-c+l)*this.width/(this.width-c),0),this.paddingRight=Math.max((h-u+l)*this.width/(this.width-u),0)}else{let c=t.height/2,u=e.height/2;r==="start"?(c=0,u=e.height):r==="end"&&(c=t.height,u=0),this.paddingTop=c+l,this.paddingBottom=u+l}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){Rt(this.options.afterFit,[this])}isHorizontal(){const{axis:e,position:t}=this.options;return t==="top"||t==="bottom"||e==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(e){this.beforeTickToLabelConversion(),this.generateTickLabels(e);let t,i;for(t=0,i=e.length;t({width:r[O]||0,height:l[O]||0});return{first:A(0),last:A(t-1),widest:A(x),highest:A(M),widths:r,heights:l}}getLabelForValue(e){return e}getPixelForValue(e,t){return NaN}getValueForPixel(e){}getPixelForTick(e){const t=this.ticks;return e<0||e>t.length-1?null:this.getPixelForValue(t[e].value)}getPixelForDecimal(e){this._reversePixels&&(e=1-e);const t=this._startPixel+e*this._length;return $k(this._alignToPixels?eo(this.chart,t,0):t)}getDecimalForPixel(e){const t=(e-this._startPixel)/this._length;return this._reversePixels?1-t:t}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:e,max:t}=this;return e<0&&t<0?t:e>0&&t>0?e:0}getContext(e){const t=this.ticks||[];if(e>=0&&es*o?s/i:a/o:a*o0}_computeGridLineItems(e){const t=this.axis,i=this.chart,o=this.options,{grid:r,position:l}=o,s=r.offset,a=this.isHorizontal(),c=this.ticks.length+(s?1:0),u=pr(r),d=[],h=r.setContext(this.getContext()),b=h.drawBorder?h.borderWidth:0,v=b/2,_=function(z){return eo(i,z,b)};let y,S,C,x,M,A,O,D,E,P,I,R;if(l==="top")y=_(this.bottom),A=this.bottom-u,D=y-v,P=_(e.top)+v,R=e.bottom;else if(l==="bottom")y=_(this.top),P=e.top,R=_(e.bottom)-v,A=y+v,D=this.top+u;else if(l==="left")y=_(this.right),M=this.right-u,O=y-v,E=_(e.left)+v,I=e.right;else if(l==="right")y=_(this.left),E=e.left,I=_(e.right)-v,M=y+v,O=this.left+u;else if(t==="x"){if(l==="center")y=_((e.top+e.bottom)/2+.5);else if(dt(l)){const z=Object.keys(l)[0],K=l[z];y=_(this.chart.scales[z].getPixelForValue(K))}P=e.top,R=e.bottom,A=y+v,D=A+u}else if(t==="y"){if(l==="center")y=_((e.left+e.right)/2);else if(dt(l)){const z=Object.keys(l)[0],K=l[z];y=_(this.chart.scales[z].getPixelForValue(K))}M=y-v,O=M-u,E=e.left,I=e.right}const G=ht(o.ticks.maxTicksLimit,c),U=Math.max(1,Math.ceil(c/G));for(S=0;Sr.value===e);return o>=0?t.setContext(this.getContext(o)).lineWidth:0}drawGrid(e){const t=this.options.grid,i=this.ctx,o=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(e));let r,l;const s=(a,f,c)=>{!c.width||!c.color||(i.save(),i.lineWidth=c.width,i.strokeStyle=c.color,i.setLineDash(c.borderDash||[]),i.lineDashOffset=c.borderDashOffset,i.beginPath(),i.moveTo(a.x,a.y),i.lineTo(f.x,f.y),i.stroke(),i.restore())};if(t.display)for(r=0,l=o.length;r{this.draw(o)}}]:[{z:i,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:i+1,draw:()=>{this.drawBorder()}},{z:t,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(e){const t=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",o=[];let r,l;for(r=0,l=t.length;r{const i=t.split("."),o=i.pop(),r=[n].concat(i).join("."),l=e[t].split("."),s=l.pop(),a=l.join(".");mt.route(r,o,a,s)})}function uS(n){return"id"in n&&"defaults"in n}class dS{constructor(){this.controllers=new Ol(hi,"datasets",!0),this.elements=new Ol($i,"elements"),this.plugins=new Ol(Object,"plugins"),this.scales=new Ol(_o,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...e){this._each("register",e)}remove(...e){this._each("unregister",e)}addControllers(...e){this._each("register",e,this.controllers)}addElements(...e){this._each("register",e,this.elements)}addPlugins(...e){this._each("register",e,this.plugins)}addScales(...e){this._each("register",e,this.scales)}getController(e){return this._get(e,this.controllers,"controller")}getElement(e){return this._get(e,this.elements,"element")}getPlugin(e){return this._get(e,this.plugins,"plugin")}getScale(e){return this._get(e,this.scales,"scale")}removeControllers(...e){this._each("unregister",e,this.controllers)}removeElements(...e){this._each("unregister",e,this.elements)}removePlugins(...e){this._each("unregister",e,this.plugins)}removeScales(...e){this._each("unregister",e,this.scales)}_each(e,t,i){[...t].forEach(o=>{const r=i||this._getRegistryForType(o);i||r.isForType(o)||r===this.plugins&&o.id?this._exec(e,r,o):Ct(o,l=>{const s=i||this._getRegistryForType(l);this._exec(e,s,l)})})}_exec(e,t,i){const o=Vf(e);Rt(i["before"+o],[],i),t[e](i),Rt(i["after"+o],[],i)}_getRegistryForType(e){for(let t=0;tr.filter(s=>!l.some(a=>s.plugin.id===a.plugin.id));this._notify(o(t,i),e,"stop"),this._notify(o(i,t),e,"start")}}function hS(n){const e=[],t=Object.keys(wi.plugins.items);for(let o=0;o{const a=i[s];if(!dt(a))return console.error(`Invalid scale configuration for scale: ${s}`);if(a._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${s}`);const f=lf(s,a),c=vS(f,o),u=t.scales||{};r[f]=r[f]||s,l[s]=Mr(Object.create(null),[{axis:f},a,u[f],u[c]])}),n.data.datasets.forEach(s=>{const a=s.type||n.type,f=s.indexAxis||rf(a,e),u=(mo[a]||{}).scales||{};Object.keys(u).forEach(d=>{const h=_S(d,f),b=s[h+"AxisID"]||r[h]||h;l[b]=l[b]||Object.create(null),Mr(l[b],[{axis:h},i[b],u[d]])})}),Object.keys(l).forEach(s=>{const a=l[s];Mr(a,[mt.scales[a.type],mt.scale])}),l}function d1(n){const e=n.options||(n.options={});e.plugins=ht(e.plugins,{}),e.scales=kS(n,e)}function p1(n){return n=n||{},n.datasets=n.datasets||[],n.labels=n.labels||[],n}function wS(n){return n=n||{},n.data=p1(n.data),d1(n),n}const $d=new Map,h1=new Set;function Tl(n,e){let t=$d.get(n);return t||(t=e(),$d.set(n,t),h1.add(t)),t}const hr=(n,e,t)=>{const i=qi(e,t);i!==void 0&&n.add(i)};class SS{constructor(e){this._config=wS(e),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(e){this._config.type=e}get data(){return this._config.data}set data(e){this._config.data=p1(e)}get options(){return this._config.options}set options(e){this._config.options=e}get plugins(){return this._config.plugins}update(){const e=this._config;this.clearCache(),d1(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return Tl(e,()=>[[`datasets.${e}`,""]])}datasetAnimationScopeKeys(e,t){return Tl(`${e}.transition.${t}`,()=>[[`datasets.${e}.transitions.${t}`,`transitions.${t}`],[`datasets.${e}`,""]])}datasetElementScopeKeys(e,t){return Tl(`${e}-${t}`,()=>[[`datasets.${e}.elements.${t}`,`datasets.${e}`,`elements.${t}`,""]])}pluginScopeKeys(e){const t=e.id,i=this.type;return Tl(`${i}-plugin-${t}`,()=>[[`plugins.${t}`,...e.additionalOptionScopes||[]]])}_cachedScopes(e,t){const i=this._scopeCache;let o=i.get(e);return(!o||t)&&(o=new Map,i.set(e,o)),o}getOptionScopes(e,t,i){const{options:o,type:r}=this,l=this._cachedScopes(e,i),s=l.get(t);if(s)return s;const a=new Set;t.forEach(c=>{e&&(a.add(e),c.forEach(u=>hr(a,e,u))),c.forEach(u=>hr(a,o,u)),c.forEach(u=>hr(a,mo[r]||{},u)),c.forEach(u=>hr(a,mt,u)),c.forEach(u=>hr(a,tf,u))});const f=Array.from(a);return f.length===0&&f.push(Object.create(null)),h1.has(t)&&l.set(t,f),f}chartOptionScopes(){const{options:e,type:t}=this;return[e,mo[t]||{},mt.datasets[t]||{},{type:t},mt,tf]}resolveNamedOptions(e,t,i,o=[""]){const r={$shared:!0},{resolver:l,subPrefixes:s}=Ad(this._resolverCache,e,o);let a=l;if(xS(l,t)){r.$shared=!1,i=Vi(i)?i():i;const f=this.createResolver(e,i,s);a=Wo(l,i,f)}for(const f of t)r[f]=a[f];return r}createResolver(e,t,i=[""],o){const{resolver:r}=Ad(this._resolverCache,e,i);return dt(t)?Wo(r,t,void 0,o):r}}function Ad(n,e,t){let i=n.get(e);i||(i=new Map,n.set(e,i));const o=t.join();let r=i.get(o);return r||(r={resolver:Xf(e,t),subPrefixes:t.filter(s=>!s.toLowerCase().includes("hover"))},i.set(o,r)),r}const CS=n=>dt(n)&&Object.getOwnPropertyNames(n).reduce((e,t)=>e||Vi(n[t]),!1);function xS(n,e){const{isScriptable:t,isIndexable:i}=U0(n);for(const o of e){const r=t(o),l=i(o),s=(l||r)&&n[o];if(r&&(Vi(s)||CS(s))||l&&Et(s))return!0}return!1}var MS="3.8.0";const $S=["top","bottom","left","right","chartArea"];function Dd(n,e){return n==="top"||n==="bottom"||$S.indexOf(n)===-1&&e==="x"}function Od(n,e){return function(t,i){return t[n]===i[n]?t[e]-i[e]:t[n]-i[n]}}function Td(n){const e=n.chart,t=e.options.animation;e.notifyPlugins("afterRender"),Rt(t&&t.onComplete,[n],e)}function AS(n){const e=n.chart,t=e.options.animation;Rt(t&&t.onProgress,[n],e)}function m1(n){return Z0()&&typeof n=="string"?n=document.getElementById(n):n&&n.length&&(n=n[0]),n&&n.canvas&&(n=n.canvas),n}const ms={},b1=n=>{const e=m1(n);return Object.values(ms).filter(t=>t.canvas===e).pop()};function DS(n,e,t){const i=Object.keys(n);for(const o of i){const r=+o;if(r>=e){const l=n[o];delete n[o],(t>0||r>e)&&(n[r+t]=l)}}}function OS(n,e,t,i){return!t||n.type==="mouseout"?null:i?e:n}class bs{constructor(e,t){const i=this.config=new SS(t),o=m1(e),r=b1(o);if(r)throw new Error("Canvas is already in use. Chart with ID '"+r.id+"' must be destroyed before the canvas can be reused.");const l=i.createResolver(i.chartOptionScopes(),this.getContext());this.platform=new(i.platform||G3(o)),this.platform.updateConfig(i);const s=this.platform.acquireContext(o,l.aspectRatio),a=s&&s.canvas,f=a&&a.height,c=a&&a.width;if(this.id=mk(),this.ctx=s,this.canvas=a,this.width=c,this.height=f,this._options=l,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new pS,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=pk(u=>this.update(u),l.resizeDelay||0),this._dataChanges=[],ms[this.id]=this,!s||!a){console.error("Failed to create chart: can't acquire context from the given item");return}bi.listen(this,"complete",Td),bi.listen(this,"progress",AS),this._initialize(),this.attached&&this.update()}get aspectRatio(){const{options:{aspectRatio:e,maintainAspectRatio:t},width:i,height:o,_aspectRatio:r}=this;return xt(e)?t&&r?r:o?i/o:null:e}get data(){return this.config.data}set data(e){this.config.data=e}get options(){return this._options}set options(e){this.config.options=e}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():nd(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Xu(this.canvas,this.ctx),this}stop(){return bi.stop(this),this}resize(e,t){bi.running(this)?this._resizeBeforeDraw={width:e,height:t}:this._resize(e,t)}_resize(e,t){const i=this.options,o=this.canvas,r=i.maintainAspectRatio&&this.aspectRatio,l=this.platform.getMaximumSize(o,e,t,r),s=i.devicePixelRatio||this.platform.getDevicePixelRatio(),a=this.width?"resize":"attach";this.width=l.width,this.height=l.height,this._aspectRatio=this.aspectRatio,nd(this,s,!0)&&(this.notifyPlugins("resize",{size:l}),Rt(i.onResize,[this,l],this),this.attached&&this._doResize(a)&&this.render())}ensureScalesHaveIDs(){const t=this.options.scales||{};Ct(t,(i,o)=>{i.id=o})}buildOrUpdateScales(){const e=this.options,t=e.scales,i=this.scales,o=Object.keys(i).reduce((l,s)=>(l[s]=!1,l),{});let r=[];t&&(r=r.concat(Object.keys(t).map(l=>{const s=t[l],a=lf(l,s),f=a==="r",c=a==="x";return{options:s,dposition:f?"chartArea":c?"bottom":"left",dtype:f?"radialLinear":c?"category":"linear"}}))),Ct(r,l=>{const s=l.options,a=s.id,f=lf(a,s),c=ht(s.type,l.dtype);(s.position===void 0||Dd(s.position,f)!==Dd(l.dposition))&&(s.position=l.dposition),o[a]=!0;let u=null;if(a in i&&i[a].type===c)u=i[a];else{const d=wi.getScale(c);u=new d({id:a,type:c,ctx:this.ctx,chart:this}),i[u.id]=u}u.init(s,e)}),Ct(o,(l,s)=>{l||delete i[s]}),Ct(i,l=>{Al.configure(this,l,l.options),Al.addBox(this,l)})}_updateMetasets(){const e=this._metasets,t=this.data.datasets.length,i=e.length;if(e.sort((o,r)=>o.index-r.index),i>t){for(let o=t;ot.length&&delete this._stacks,e.forEach((i,o)=>{t.filter(r=>r===i._dataset).length===0&&this._destroyDatasetMeta(o)})}buildOrUpdateControllers(){const e=[],t=this.data.datasets;let i,o;for(this._removeUnreferencedMetasets(),i=0,o=t.length;i{this.getDatasetMeta(t).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(e){const t=this.config;t.update();const i=this._options=t.createResolver(t.chartOptionScopes(),this.getContext()),o=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:e,cancelable:!0})===!1)return;const r=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let l=0;for(let f=0,c=this.data.datasets.length;f{f.reset()}),this._updateDatasets(e),this.notifyPlugins("afterUpdate",{mode:e}),this._layers.sort(Od("z","_idx"));const{_active:s,_lastEvent:a}=this;a?this._eventHandler(a,!0):s.length&&this._updateHoverStyles(s,s,!0),this.render()}_updateScales(){Ct(this.scales,e=>{Al.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const e=this.options,t=new Set(Object.keys(this._listeners)),i=new Set(e.events);(!Hu(t,i)||!!this._responsiveListeners!==e.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:e}=this,t=this._getUniformDataChanges()||[];for(const{method:i,start:o,count:r}of t){const l=i==="_removeElements"?-r:r;DS(e,o,l)}}_getUniformDataChanges(){const e=this._dataChanges;if(!e||!e.length)return;this._dataChanges=[];const t=this.data.datasets.length,i=r=>new Set(e.filter(l=>l[0]===r).map((l,s)=>s+","+l.splice(1).join(","))),o=i(0);for(let r=1;rr.split(",")).map(r=>({method:r[1],start:+r[2],count:+r[3]}))}_updateLayout(e){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;Al.update(this,this.width,this.height,e);const t=this.chartArea,i=t.width<=0||t.height<=0;this._layers=[],Ct(this.boxes,o=>{i&&o.position==="chartArea"||(o.configure&&o.configure(),this._layers.push(...o._layers()))},this),this._layers.forEach((o,r)=>{o._idx=r}),this.notifyPlugins("afterLayout")}_updateDatasets(e){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:e,cancelable:!0})!==!1){for(let t=0,i=this.data.datasets.length;t=0;--t)this._drawDataset(e[t]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(e){const t=this.ctx,i=e._clip,o=!i.disabled,r=this.chartArea,l={meta:e,index:e.index,cancelable:!0};this.notifyPlugins("beforeDatasetDraw",l)!==!1&&(o&&Gf(t,{left:i.left===!1?0:r.left-i.left,right:i.right===!1?this.width:r.right+i.right,top:i.top===!1?0:r.top-i.top,bottom:i.bottom===!1?this.height:r.bottom+i.bottom}),e.controller.draw(),o&&Kf(t),l.cancelable=!1,this.notifyPlugins("afterDatasetDraw",l))}isPointInArea(e){return jr(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,t,i,o){const r=$3.modes[t];return typeof r=="function"?r(this,e,i,o):[]}getDatasetMeta(e){const t=this.data.datasets[e],i=this._metasets;let o=i.filter(r=>r&&r._dataset===t).pop();return o||(o={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:t&&t.order||0,index:e,_dataset:t,_parsed:[],_sorted:!1},i.push(o)),o}getContext(){return this.$context||(this.$context=Wi(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(e){const t=this.data.datasets[e];if(!t)return!1;const i=this.getDatasetMeta(e);return typeof i.hidden=="boolean"?!i.hidden:!t.hidden}setDatasetVisibility(e,t){const i=this.getDatasetMeta(e);i.hidden=!t}toggleDataVisibility(e){this._hiddenIndices[e]=!this._hiddenIndices[e]}getDataVisibility(e){return!this._hiddenIndices[e]}_updateVisibility(e,t,i){const o=i?"show":"hide",r=this.getDatasetMeta(e),l=r.controller._resolveAnimations(void 0,o);Un(t)?(r.data[t].hidden=!i,this.update()):(this.setDatasetVisibility(e,i),l.update(r,{visible:i}),this.update(s=>s.datasetIndex===e?o:void 0))}hide(e,t){this._updateVisibility(e,t,!1)}show(e,t){this._updateVisibility(e,t,!0)}_destroyDatasetMeta(e){const t=this._metasets[e];t&&t.controller&&t.controller._destroy(),delete this._metasets[e]}_stop(){let e,t;for(this.stop(),bi.remove(this),e=0,t=this.data.datasets.length;e{t.addEventListener(this,r,l),e[r]=l},o=(r,l,s)=>{r.offsetX=l,r.offsetY=s,this._eventHandler(r)};Ct(this.options.events,r=>i(r,o))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const e=this._responsiveListeners,t=this.platform,i=(a,f)=>{t.addEventListener(this,a,f),e[a]=f},o=(a,f)=>{e[a]&&(t.removeEventListener(this,a,f),delete e[a])},r=(a,f)=>{this.canvas&&this.resize(a,f)};let l;const s=()=>{o("attach",s),this.attached=!0,this.resize(),i("resize",r),i("detach",l)};l=()=>{this.attached=!1,o("resize",r),this._stop(),this._resize(0,0),i("attach",s)},t.isAttached(this.canvas)?s():l()}unbindEvents(){Ct(this._listeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._listeners={},Ct(this._responsiveListeners,(e,t)=>{this.platform.removeEventListener(this,t,e)}),this._responsiveListeners=void 0}updateHoverStyle(e,t,i){const o=i?"set":"remove";let r,l,s,a;for(t==="dataset"&&(r=this.getDatasetMeta(e[0].datasetIndex),r.controller["_"+o+"DatasetHoverStyle"]()),s=0,a=e.length;s{const s=this.getDatasetMeta(r);if(!s)throw new Error("No dataset found at index "+r);return{datasetIndex:r,element:s.data[l],index:l}});!ls(i,t)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,t))}notifyPlugins(e,t,i){return this._plugins.notify(this,e,t,i)}_updateHoverStyles(e,t,i){const o=this.options.hover,r=(a,f)=>a.filter(c=>!f.some(u=>c.datasetIndex===u.datasetIndex&&c.index===u.index)),l=r(t,e),s=i?e:r(e,t);l.length&&this.updateHoverStyle(l,o.mode,!1),s.length&&o.mode&&this.updateHoverStyle(s,o.mode,!0)}_eventHandler(e,t){const i={event:e,replay:t,cancelable:!0,inChartArea:this.isPointInArea(e)},o=l=>(l.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins("beforeEvent",i,o)===!1)return;const r=this._handleEvent(e,t,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,o),(r||i.changed)&&this.render(),this}_handleEvent(e,t,i){const{_active:o=[],options:r}=this,l=t,s=this._getActiveElements(e,o,i,l),a=kk(e),f=OS(e,this._lastEvent,i,a);i&&(this._lastEvent=null,Rt(r.onHover,[e,s,this],this),a&&Rt(r.onClick,[e,s,this],this));const c=!ls(s,o);return(c||t)&&(this._active=s,this._updateHoverStyles(s,o,t)),this._lastEvent=f,c}_getActiveElements(e,t,i,o){if(e.type==="mouseout")return[];if(!i)return t;const r=this.options.hover;return this.getElementsAtEventForMode(e,r.mode,r,o)}}const Ed=()=>Ct(bs.instances,n=>n._plugins.invalidate()),Oi=!0;Object.defineProperties(bs,{defaults:{enumerable:Oi,value:mt},instances:{enumerable:Oi,value:ms},overrides:{enumerable:Oi,value:mo},registry:{enumerable:Oi,value:wi},version:{enumerable:Oi,value:MS},getChart:{enumerable:Oi,value:b1},register:{enumerable:Oi,value:(...n)=>{wi.add(...n),Ed()}},unregister:{enumerable:Oi,value:(...n)=>{wi.remove(...n),Ed()}}});function g1(n,e,t){const{startAngle:i,pixelMargin:o,x:r,y:l,outerRadius:s,innerRadius:a}=e;let f=o/s;n.beginPath(),n.arc(r,l,s,i-f,t+f),a>o?(f=o/a,n.arc(r,l,a,t+f,i-f,!0)):n.arc(r,l,o,t+Nt,i-Nt),n.closePath(),n.clip()}function TS(n){return Jf(n,["outerStart","outerEnd","innerStart","innerEnd"])}function ES(n,e,t,i){const o=TS(n.options.borderRadius),r=(t-e)/2,l=Math.min(r,i*e/2),s=a=>{const f=(t-Math.min(r,a))*i/2;return sn(a,0,Math.min(r,f))};return{outerStart:s(o.outerStart),outerEnd:s(o.outerEnd),innerStart:sn(o.innerStart,0,l),innerEnd:sn(o.innerEnd,0,l)}}function To(n,e,t,i){return{x:t+n*Math.cos(e),y:i+n*Math.sin(e)}}function sf(n,e,t,i,o){const{x:r,y:l,startAngle:s,pixelMargin:a,innerRadius:f}=e,c=Math.max(e.outerRadius+i+t-a,0),u=f>0?f+i+t+a:0;let d=0;const h=o-s;if(i){const K=f>0?f-i:0,Y=c>0?c-i:0,W=(K+Y)/2,te=W!==0?h*W/(W+i):h;d=(h-te)/2}const b=Math.max(.001,h*c-t/jt)/c,v=(h-b)/2,_=s+v+d,y=o-v-d,{outerStart:S,outerEnd:C,innerStart:x,innerEnd:M}=ES(e,u,c,y-_),A=c-S,O=c-C,D=_+S/A,E=y-C/O,P=u+x,I=u+M,R=_+x/P,G=y-M/I;if(n.beginPath(),n.arc(r,l,c,D,E),C>0){const K=To(O,E,r,l);n.arc(K.x,K.y,C,E,y+Nt)}const U=To(I,y,r,l);if(n.lineTo(U.x,U.y),M>0){const K=To(I,G,r,l);n.arc(K.x,K.y,M,y+Nt,G+Math.PI)}if(n.arc(r,l,u,y-M/u,_+x/u,!0),x>0){const K=To(P,R,r,l);n.arc(K.x,K.y,x,R+Math.PI,_-Nt)}const z=To(A,_,r,l);if(n.lineTo(z.x,z.y),S>0){const K=To(A,D,r,l);n.arc(K.x,K.y,S,_-Nt,D)}n.closePath()}function PS(n,e,t,i){const{fullCircles:o,startAngle:r,circumference:l}=e;let s=e.endAngle;if(o){sf(n,e,t,i,r+Tt);for(let a=0;a=Tt||Rr(r,s,a),v=Nr(l,f+d,c+d);return b&&v}getCenterPoint(e){const{x:t,y:i,startAngle:o,endAngle:r,innerRadius:l,outerRadius:s}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius","circumference"],e),{offset:a,spacing:f}=this.options,c=(o+r)/2,u=(l+s+f+a)/2;return{x:t+Math.cos(c)*u,y:i+Math.sin(c)*u}}tooltipPosition(e){return this.getCenterPoint(e)}draw(e){const{options:t,circumference:i}=this,o=(t.offset||0)/2,r=(t.spacing||0)/2;if(this.pixelMargin=t.borderAlign==="inner"?.33:0,this.fullCircles=i>Tt?Math.floor(i/Tt):0,i===0||this.innerRadius<0||this.outerRadius<0)return;e.save();let l=0;if(o){l=o/2;const a=(this.startAngle+this.endAngle)/2;e.translate(Math.cos(a)*l,Math.sin(a)*l),this.circumference>=jt&&(l=o)}e.fillStyle=t.backgroundColor,e.strokeStyle=t.borderColor;const s=PS(e,this,l,r);LS(e,this,l,r,s),e.restore()}}sc.id="arc";sc.defaults={borderAlign:"center",borderColor:"#fff",borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0};sc.defaultRoutes={backgroundColor:"backgroundColor"};function _1(n,e,t=e){n.lineCap=ht(t.borderCapStyle,e.borderCapStyle),n.setLineDash(ht(t.borderDash,e.borderDash)),n.lineDashOffset=ht(t.borderDashOffset,e.borderDashOffset),n.lineJoin=ht(t.borderJoinStyle,e.borderJoinStyle),n.lineWidth=ht(t.borderWidth,e.borderWidth),n.strokeStyle=ht(t.borderColor,e.borderColor)}function IS(n,e,t){n.lineTo(t.x,t.y)}function RS(n){return n.stepped?Qk:n.tension||n.cubicInterpolationMode==="monotone"?ew:IS}function v1(n,e,t={}){const i=n.length,{start:o=0,end:r=i-1}=t,{start:l,end:s}=e,a=Math.max(o,l),f=Math.min(r,s),c=os&&r>s;return{count:i,start:a,loop:e.loop,ilen:f(l+(f?s-x:x))%r,C=()=>{v!==_&&(n.lineTo(c,_),n.lineTo(c,v),n.lineTo(c,y))};for(a&&(h=o[S(0)],n.moveTo(h.x,h.y)),d=0;d<=s;++d){if(h=o[S(d)],h.skip)continue;const x=h.x,M=h.y,A=x|0;A===b?(M_&&(_=M),c=(u*c+x)/++u):(C(),n.lineTo(x,M),b=A,u=0,v=_=M),y=M}C()}function af(n){const e=n.options,t=e.borderDash&&e.borderDash.length;return!n._decimated&&!n._loop&&!e.tension&&e.cubicInterpolationMode!=="monotone"&&!e.stepped&&!t?jS:NS}function zS(n){return n.stepped?Fw:n.tension||n.cubicInterpolationMode==="monotone"?Lw:ro}function HS(n,e,t,i){let o=e._path;o||(o=e._path=new Path2D,e.path(o,t,i)&&o.closePath()),_1(n,e.options),n.stroke(o)}function qS(n,e,t,i){const{segments:o,options:r}=e,l=af(e);for(const s of o)_1(n,r,s.style),n.beginPath(),l(n,e,s,{start:t,end:t+i-1})&&n.closePath(),n.stroke()}const VS=typeof Path2D=="function";function BS(n,e,t,i){VS&&!e.options.segment?HS(n,e,t,i):qS(n,e,t,i)}class Yi extends $i{constructor(e){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,e&&Object.assign(this,e)}updateControlPoints(e,t){const i=this.options;if((i.tension||i.cubicInterpolationMode==="monotone")&&!i.stepped&&!this._pointsUpdated){const o=i.spanGaps?this._loop:this._fullLoop;Mw(this._points,i,e,o,t),this._pointsUpdated=!0}}set points(e){this._points=e,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Bw(this,this.options.segment))}first(){const e=this.segments,t=this.points;return e.length&&t[e[0].start]}last(){const e=this.segments,t=this.points,i=e.length;return i&&t[e[i-1].end]}interpolate(e,t){const i=this.options,o=e[t],r=this.points,l=e1(this,{property:t,start:o,end:o});if(!l.length)return;const s=[],a=zS(i);let f,c;for(f=0,c=l.length;fn!=="borderDash"&&n!=="fill"};function Pd(n,e,t,i){const o=n.options,{[t]:r}=n.getProps([t],i);return Math.abs(e-r){s=fc(l,s,o);const a=o[l],f=o[s];i!==null?(r.push({x:a.x,y:i}),r.push({x:f.x,y:i})):t!==null&&(r.push({x:t,y:a.y}),r.push({x:t,y:f.y}))}),r}function fc(n,e,t){for(;e>n;e--){const i=t[e];if(!isNaN(i.x)&&!isNaN(i.y))break}return e}function Fd(n,e,t,i){return n&&e?i(n[t],e[t]):n?n[t]:e?e[t]:0}function k1(n,e){let t=[],i=!1;return Et(n)?(i=!0,t=n):t=ZS(n,e),t.length?new Yi({points:t,options:{tension:0},_loop:i,_fullLoop:i}):null}function XS(n,e,t){let o=n[e].fill;const r=[e];let l;if(!t)return o;for(;o!==!1&&r.indexOf(o)===-1;){if(!Ht(o))return o;if(l=n[o],!l)return!1;if(l.visible)return o;r.push(o),o=l.fill}return!1}function QS(n,e,t){const i=i4(n);if(dt(i))return isNaN(i.value)?!1:i;let o=parseFloat(i);return Ht(o)&&Math.floor(o)===o?e4(i[0],e,o,t):["origin","start","end","stack","shape"].indexOf(i)>=0&&i}function e4(n,e,t,i){return(n==="-"||n==="+")&&(t=e+t),t===e||t<0||t>=i?!1:t}function t4(n,e){let t=null;return n==="start"?t=e.bottom:n==="end"?t=e.top:dt(n)?t=e.getPixelForValue(n.value):e.getBasePixel&&(t=e.getBasePixel()),t}function n4(n,e,t){let i;return n==="start"?i=t:n==="end"?i=e.options.reverse?e.min:e.max:dt(n)?i=n.value:i=e.getBaseValue(),i}function i4(n){const e=n.options,t=e.fill;let i=ht(t&&t.target,t);return i===void 0&&(i=!!e.backgroundColor),i===!1||i===null?!1:i===!0?"origin":i}function o4(n){const{scale:e,index:t,line:i}=n,o=[],r=i.segments,l=i.points,s=r4(e,t);s.push(k1({x:null,y:e.bottom},i));for(let a=0;a=0;--l){const s=o[l].$filler;!s||(s.line.updateControlPoints(r,s.axis),i&&wa(n.ctx,s,r))}},beforeDatasetsDraw(n,e,t){if(t.drawTime!=="beforeDatasetsDraw")return;const i=n.getSortedVisibleDatasetMetas();for(let o=i.length-1;o>=0;--o){const r=i[o].$filler;r&&wa(n.ctx,r,n.chartArea)}},beforeDatasetDraw(n,e,t){const i=e.meta.$filler;!i||i.fill===!1||t.drawTime!=="beforeDatasetDraw"||wa(n.ctx,i,n.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const Or={average(n){if(!n.length)return!1;let e,t,i=0,o=0,r=0;for(e=0,t=n.length;e-1?n.split(` +`):n}function b4(n,e){const{element:t,datasetIndex:i,index:o}=e,r=n.getDatasetMeta(i).controller,{label:l,value:s}=r.getLabelAndValue(o);return{chart:n,label:l,parsed:r.getParsed(o),raw:n.data.datasets[i].data[o],formattedValue:s,dataset:r.getDataset(),dataIndex:o,datasetIndex:i,element:t}}function Nd(n,e){const t=n.chart.ctx,{body:i,footer:o,title:r}=n,{boxWidth:l,boxHeight:s}=e,a=Mn(e.bodyFont),f=Mn(e.titleFont),c=Mn(e.footerFont),u=r.length,d=o.length,h=i.length,b=Wn(e.padding);let v=b.height,_=0,y=i.reduce((x,M)=>x+M.before.length+M.lines.length+M.after.length,0);if(y+=n.beforeBody.length+n.afterBody.length,u&&(v+=u*f.lineHeight+(u-1)*e.titleSpacing+e.titleMarginBottom),y){const x=e.displayColors?Math.max(s,a.lineHeight):a.lineHeight;v+=h*x+(y-h)*a.lineHeight+(y-1)*e.bodySpacing}d&&(v+=e.footerMarginTop+d*c.lineHeight+(d-1)*e.footerSpacing);let S=0;const C=function(x){_=Math.max(_,t.measureText(x).width+S)};return t.save(),t.font=f.string,Ct(n.title,C),t.font=a.string,Ct(n.beforeBody.concat(n.afterBody),C),S=e.displayColors?l+2+e.boxPadding:0,Ct(i,x=>{Ct(x.before,C),Ct(x.lines,C),Ct(x.after,C)}),S=0,t.font=c.string,Ct(n.footer,C),t.restore(),_+=b.width,{width:_,height:v}}function g4(n,e){const{y:t,height:i}=e;return tn.height-i/2?"bottom":"center"}function _4(n,e,t,i){const{x:o,width:r}=i,l=t.caretSize+t.caretPadding;if(n==="left"&&o+r+l>e.width||n==="right"&&o-r-l<0)return!0}function v4(n,e,t,i){const{x:o,width:r}=t,{width:l,chartArea:{left:s,right:a}}=n;let f="center";return i==="center"?f=o<=(s+a)/2?"left":"right":o<=r/2?f="left":o>=l-r/2&&(f="right"),_4(f,n,e,t)&&(f="center"),f}function jd(n,e,t){const i=t.yAlign||e.yAlign||g4(n,t);return{xAlign:t.xAlign||e.xAlign||v4(n,e,t,i),yAlign:i}}function y4(n,e){let{x:t,width:i}=n;return e==="right"?t-=i:e==="center"&&(t-=i/2),t}function k4(n,e,t){let{y:i,height:o}=n;return e==="top"?i+=t:e==="bottom"?i-=o+t:i-=o/2,i}function zd(n,e,t,i){const{caretSize:o,caretPadding:r,cornerRadius:l}=n,{xAlign:s,yAlign:a}=t,f=o+r,{topLeft:c,topRight:u,bottomLeft:d,bottomRight:h}=jo(l);let b=y4(e,s);const v=k4(e,a,f);return a==="center"?s==="left"?b+=f:s==="right"&&(b-=f):s==="left"?b-=Math.max(c,d)+o:s==="right"&&(b+=Math.max(u,h)+o),{x:sn(b,0,i.width-e.width),y:sn(v,0,i.height-e.height)}}function El(n,e,t){const i=Wn(t.padding);return e==="center"?n.x+n.width/2:e==="right"?n.x+n.width-i.right:n.x+i.left}function Hd(n){return oi([],gi(n))}function w4(n,e,t){return Wi(n,{tooltip:e,tooltipItems:t,type:"tooltip"})}function qd(n,e){const t=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return t?n.override(t):n}class cf extends $i{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart||e._chart,this._chart=this.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const e=this._cachedAnimations;if(e)return e;const t=this.chart,i=this.options.setContext(this.getContext()),o=i.enabled&&t.options.animation&&i.animations,r=new t1(this.chart,o);return o._cacheable&&(this._cachedAnimations=Object.freeze(r)),r}getContext(){return this.$context||(this.$context=w4(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){const{callbacks:i}=t,o=i.beforeTitle.apply(this,[e]),r=i.title.apply(this,[e]),l=i.afterTitle.apply(this,[e]);let s=[];return s=oi(s,gi(o)),s=oi(s,gi(r)),s=oi(s,gi(l)),s}getBeforeBody(e,t){return Hd(t.callbacks.beforeBody.apply(this,[e]))}getBody(e,t){const{callbacks:i}=t,o=[];return Ct(e,r=>{const l={before:[],lines:[],after:[]},s=qd(i,r);oi(l.before,gi(s.beforeLabel.call(this,r))),oi(l.lines,s.label.call(this,r)),oi(l.after,gi(s.afterLabel.call(this,r))),o.push(l)}),o}getAfterBody(e,t){return Hd(t.callbacks.afterBody.apply(this,[e]))}getFooter(e,t){const{callbacks:i}=t,o=i.beforeFooter.apply(this,[e]),r=i.footer.apply(this,[e]),l=i.afterFooter.apply(this,[e]);let s=[];return s=oi(s,gi(o)),s=oi(s,gi(r)),s=oi(s,gi(l)),s}_createItems(e){const t=this._active,i=this.chart.data,o=[],r=[],l=[];let s=[],a,f;for(a=0,f=t.length;ae.filter(c,u,d,i))),e.itemSort&&(s=s.sort((c,u)=>e.itemSort(c,u,i))),Ct(s,c=>{const u=qd(e.callbacks,c);o.push(u.labelColor.call(this,c)),r.push(u.labelPointStyle.call(this,c)),l.push(u.labelTextColor.call(this,c))}),this.labelColors=o,this.labelPointStyles=r,this.labelTextColors=l,this.dataPoints=s,s}update(e,t){const i=this.options.setContext(this.getContext()),o=this._active;let r,l=[];if(!o.length)this.opacity!==0&&(r={opacity:0});else{const s=Or[i.position].call(this,o,this._eventPosition);l=this._createItems(i),this.title=this.getTitle(l,i),this.beforeBody=this.getBeforeBody(l,i),this.body=this.getBody(l,i),this.afterBody=this.getAfterBody(l,i),this.footer=this.getFooter(l,i);const a=this._size=Nd(this,i),f=Object.assign({},s,a),c=jd(this.chart,i,f),u=zd(i,f,c,this.chart);this.xAlign=c.xAlign,this.yAlign=c.yAlign,r={opacity:1,x:u.x,y:u.y,width:a.width,height:a.height,caretX:s.x,caretY:s.y}}this._tooltipItems=l,this.$context=void 0,r&&this._resolveAnimations().update(this,r),e&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:t})}drawCaret(e,t,i,o){const r=this.getCaretPosition(e,i,o);t.lineTo(r.x1,r.y1),t.lineTo(r.x2,r.y2),t.lineTo(r.x3,r.y3)}getCaretPosition(e,t,i){const{xAlign:o,yAlign:r}=this,{caretSize:l,cornerRadius:s}=i,{topLeft:a,topRight:f,bottomLeft:c,bottomRight:u}=jo(s),{x:d,y:h}=e,{width:b,height:v}=t;let _,y,S,C,x,M;return r==="center"?(x=h+v/2,o==="left"?(_=d,y=_-l,C=x+l,M=x-l):(_=d+b,y=_+l,C=x-l,M=x+l),S=_):(o==="left"?y=d+Math.max(a,c)+l:o==="right"?y=d+b-Math.max(f,u)-l:y=this.caretX,r==="top"?(C=h,x=C-l,_=y-l,S=y+l):(C=h+v,x=C+l,_=y+l,S=y-l),M=C),{x1:_,x2:y,x3:S,y1:C,y2:x,y3:M}}drawTitle(e,t,i){const o=this.title,r=o.length;let l,s,a;if(r){const f=pa(i.rtl,this.x,this.width);for(e.x=El(this,i.titleAlign,i),t.textAlign=f.textAlign(i.titleAlign),t.textBaseline="middle",l=Mn(i.titleFont),s=i.titleSpacing,t.fillStyle=i.titleColor,t.font=l.string,a=0;aC!==0)?(e.beginPath(),e.fillStyle=r.multiKeyBackground,ds(e,{x:_,y:v,w:f,h:a,radius:S}),e.fill(),e.stroke(),e.fillStyle=l.backgroundColor,e.beginPath(),ds(e,{x:y,y:v+1,w:f-2,h:a-2,radius:S}),e.fill()):(e.fillStyle=r.multiKeyBackground,e.fillRect(_,v,f,a),e.strokeRect(_,v,f,a),e.fillStyle=l.backgroundColor,e.fillRect(y,v+1,f-2,a-2))}e.fillStyle=this.labelTextColors[i]}drawBody(e,t,i){const{body:o}=this,{bodySpacing:r,bodyAlign:l,displayColors:s,boxHeight:a,boxWidth:f,boxPadding:c}=i,u=Mn(i.bodyFont);let d=u.lineHeight,h=0;const b=pa(i.rtl,this.x,this.width),v=function(D){t.fillText(D,b.x(e.x+h),e.y+d/2),e.y+=d+r},_=b.textAlign(l);let y,S,C,x,M,A,O;for(t.textAlign=l,t.textBaseline="middle",t.font=u.string,e.x=El(this,_,i),t.fillStyle=i.bodyColor,Ct(this.beforeBody,v),h=s&&_!=="right"?l==="center"?f/2+c:f+2+c:0,x=0,A=o.length;x0&&t.stroke()}_updateAnimationTarget(e){const t=this.chart,i=this.$animations,o=i&&i.x,r=i&&i.y;if(o||r){const l=Or[e.position].call(this,this._active,this._eventPosition);if(!l)return;const s=this._size=Nd(this,e),a=Object.assign({},l,this._size),f=jd(t,e,a),c=zd(e,a,f,t);(o._to!==c.x||r._to!==c.y)&&(this.xAlign=f.xAlign,this.yAlign=f.yAlign,this.width=s.width,this.height=s.height,this.caretX=l.x,this.caretY=l.y,this._resolveAnimations().update(this,c))}}_willRender(){return!!this.opacity}draw(e){const t=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(t);const o={width:this.width,height:this.height},r={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const l=Wn(t.padding),s=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;t.enabled&&s&&(e.save(),e.globalAlpha=i,this.drawBackground(r,e,o,t),jw(e,t.textDirection),r.y+=l.top,this.drawTitle(r,e,t),this.drawBody(r,e,t),this.drawFooter(r,e,t),zw(e,t.textDirection),e.restore())}getActiveElements(){return this._active||[]}setActiveElements(e,t){const i=this._active,o=e.map(({datasetIndex:s,index:a})=>{const f=this.chart.getDatasetMeta(s);if(!f)throw new Error("Cannot find a dataset at index "+s);return{datasetIndex:s,element:f.data[a],index:a}}),r=!ls(i,o),l=this._positionChanged(o,t);(r||l)&&(this._active=o,this._eventPosition=t,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(e,t,i=!0){if(t&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const o=this.options,r=this._active||[],l=this._getActiveElements(e,r,t,i),s=this._positionChanged(l,e),a=t||!ls(l,r)||s;return a&&(this._active=l,(o.enabled||o.external)&&(this._eventPosition={x:e.x,y:e.y},this.update(!0,t))),a}_getActiveElements(e,t,i,o){const r=this.options;if(e.type==="mouseout")return[];if(!o)return t;const l=this.chart.getElementsAtEventForMode(e,r.mode,r,i);return r.reverse&&l.reverse(),l}_positionChanged(e,t){const{caretX:i,caretY:o,options:r}=this,l=Or[r.position].call(this,e,t);return l!==!1&&(i!==l.x||o!==l.y)}}cf.positioners=Or;var S4={id:"tooltip",_element:cf,positioners:Or,afterInit(n,e,t){t&&(n.tooltip=new cf({chart:n,options:t}))},beforeUpdate(n,e,t){n.tooltip&&n.tooltip.initialize(t)},reset(n,e,t){n.tooltip&&n.tooltip.initialize(t)},afterDraw(n){const e=n.tooltip;if(e&&e._willRender()){const t={tooltip:e};if(n.notifyPlugins("beforeTooltipDraw",t)===!1)return;e.draw(n.ctx),n.notifyPlugins("afterTooltipDraw",t)}},afterEvent(n,e){if(n.tooltip){const t=e.replay;n.tooltip.handleEvent(e.event,t,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(n,e)=>e.bodyFont.size,boxWidth:(n,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:{beforeTitle:mi,title(n){if(n.length>0){const e=n[0],t=e.chart.data.labels,i=t?t.length:0;if(this&&this.options&&this.options.mode==="dataset")return e.dataset.label||"";if(e.label)return e.label;if(i>0&&e.dataIndexn!=="filter"&&n!=="itemSort"&&n!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};const C4=(n,e,t,i)=>(typeof e=="string"?(t=n.push(e)-1,i.unshift({index:t,label:e})):isNaN(e)&&(t=null),t);function x4(n,e,t,i){const o=n.indexOf(e);if(o===-1)return C4(n,e,t,i);const r=n.lastIndexOf(e);return o!==r?t:o}const M4=(n,e)=>n===null?null:sn(Math.round(n),0,e);class uf extends _o{constructor(e){super(e),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(e){const t=this._addedLabels;if(t.length){const i=this.getLabels();for(const{index:o,label:r}of t)i[o]===r&&i.splice(o,1);this._addedLabels=[]}super.init(e)}parse(e,t){if(xt(e))return null;const i=this.getLabels();return t=isFinite(t)&&i[t]===e?t:x4(i,e,ht(t,e),this._addedLabels),M4(t,i.length-1)}determineDataLimits(){const{minDefined:e,maxDefined:t}=this.getUserBounds();let{min:i,max:o}=this.getMinMax(!0);this.options.bounds==="ticks"&&(e||(i=0),t||(o=this.getLabels().length-1)),this.min=i,this.max=o}buildTicks(){const e=this.min,t=this.max,i=this.options.offset,o=[];let r=this.getLabels();r=e===0&&t===r.length-1?r:r.slice(e,t+1),this._valueRange=Math.max(r.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let l=e;l<=t;l++)o.push({value:l});return o}getLabelForValue(e){const t=this.getLabels();return e>=0&&et.length-1?null:this.getPixelForValue(t[e].value)}getValueForPixel(e){return Math.round(this._startValue+this.getDecimalForPixel(e)*this._valueRange)}getBasePixel(){return this.bottom}}uf.id="category";uf.defaults={ticks:{callback:uf.prototype.getLabelForValue}};function $4(n,e){const t=[],{bounds:o,step:r,min:l,max:s,precision:a,count:f,maxTicks:c,maxDigits:u,includeBounds:d}=n,h=r||1,b=c-1,{min:v,max:_}=e,y=!xt(l),S=!xt(s),C=!xt(f),x=(_-v)/(u+1);let M=Vu((_-v)/b/h)*h,A,O,D,E;if(M<1e-14&&!y&&!S)return[{value:v},{value:_}];E=Math.ceil(_/M)-Math.floor(v/M),E>b&&(M=Vu(E*M/b/h)*h),xt(a)||(A=Math.pow(10,a),M=Math.ceil(M*A)/A),o==="ticks"?(O=Math.floor(v/M)*M,D=Math.ceil(_/M)*M):(O=v,D=_),y&&S&&r&&xk((s-l)/r,M/1e3)?(E=Math.round(Math.min((s-l)/M,c)),M=(s-l)/E,O=l,D=s):C?(O=y?l:O,D=S?s:D,E=f-1,M=(D-O)/E):(E=(D-O)/M,$r(E,Math.round(E),M/1e3)?E=Math.round(E):E=Math.ceil(E));const P=Math.max(Bu(M),Bu(O));A=Math.pow(10,xt(a)?P:a),O=Math.round(O*A)/A,D=Math.round(D*A)/A;let I=0;for(y&&(d&&O!==l?(t.push({value:l}),Oo=t?o:a,s=a=>r=i?r:a;if(e){const a=ai(o),f=ai(r);a<0&&f<0?s(0):a>0&&f>0&&l(0)}if(o===r){let a=1;(r>=Number.MAX_SAFE_INTEGER||o<=Number.MIN_SAFE_INTEGER)&&(a=Math.abs(r*.05)),s(r+a),e||l(o-a)}this.min=o,this.max=r}getTickLimit(){const e=this.options.ticks;let{maxTicksLimit:t,stepSize:i}=e,o;return i?(o=Math.ceil(this.max/i)-Math.floor(this.min/i)+1,o>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${i} would result generating up to ${o} ticks. Limiting to 1000.`),o=1e3)):(o=this.computeTickLimit(),t=t||11),t&&(o=Math.min(t,o)),o}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const e=this.options,t=e.ticks;let i=this.getTickLimit();i=Math.max(2,i);const o={maxTicks:i,bounds:e.bounds,min:e.min,max:e.max,precision:t.precision,step:t.stepSize,count:t.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:t.minRotation||0,includeBounds:t.includeBounds!==!1},r=this._range||this,l=$4(o,r);return e.bounds==="ticks"&&L0(l,this,"value"),e.reverse?(l.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),l}configure(){const e=this.ticks;let t=this.min,i=this.max;if(super.configure(),this.options.offset&&e.length){const o=(i-t)/Math.max(e.length-1,1)/2;t-=o,i+=o}this._startValue=t,this._endValue=i,this._valueRange=i-t}getLabelForValue(e){return Zr(e,this.chart.options.locale,this.options.ticks.format)}}class cc extends gs{determineDataLimits(){const{min:e,max:t}=this.getMinMax(!0);this.min=Ht(e)?e:0,this.max=Ht(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){const e=this.isHorizontal(),t=e?this.width:this.height,i=Xn(this.options.ticks.minRotation),o=(e?Math.sin(i):Math.cos(i))||.001,r=this._resolveTickFontOptions(0);return Math.ceil(t/Math.min(40,r.lineHeight/o))}getPixelForValue(e){return e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getValueForPixel(e){return this._startValue+this.getDecimalForPixel(e)*this._valueRange}}cc.id="linear";cc.defaults={ticks:{callback:Ts.formatters.numeric}};function Bd(n){return n/Math.pow(10,Math.floor(zn(n)))===1}function A4(n,e){const t=Math.floor(zn(e.max)),i=Math.ceil(e.max/Math.pow(10,t)),o=[];let r=Rn(n.min,Math.pow(10,Math.floor(zn(e.min)))),l=Math.floor(zn(r)),s=Math.floor(r/Math.pow(10,l)),a=l<0?Math.pow(10,Math.abs(l)):1;do o.push({value:r,major:Bd(r)}),++s,s===10&&(s=1,++l,a=l>=0?1:a),r=Math.round(s*Math.pow(10,l)*a)/a;while(l0?i:null}determineDataLimits(){const{min:e,max:t}=this.getMinMax(!0);this.min=Ht(e)?Math.max(0,e):null,this.max=Ht(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:e,maxDefined:t}=this.getUserBounds();let i=this.min,o=this.max;const r=a=>i=e?i:a,l=a=>o=t?o:a,s=(a,f)=>Math.pow(10,Math.floor(zn(a))+f);i===o&&(i<=0?(r(1),l(10)):(r(s(i,-1)),l(s(o,1)))),i<=0&&r(s(o,-1)),o<=0&&l(s(i,1)),this._zero&&this.min!==this._suggestedMin&&i===s(this.min,0)&&r(s(i,-1)),this.min=i,this.max=o}buildTicks(){const e=this.options,t={min:this._userMin,max:this._userMax},i=A4(t,this);return e.bounds==="ticks"&&L0(i,this,"value"),e.reverse?(i.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),i}getLabelForValue(e){return e===void 0?"0":Zr(e,this.chart.options.locale,this.options.ticks.format)}configure(){const e=this.min;super.configure(),this._startValue=zn(e),this._valueRange=zn(this.max)-zn(e)}getPixelForValue(e){return(e===void 0||e===0)&&(e=this.min),e===null||isNaN(e)?NaN:this.getPixelForDecimal(e===this.min?0:(zn(e)-this._startValue)/this._valueRange)}getValueForPixel(e){const t=this.getDecimalForPixel(e);return Math.pow(10,this._startValue+t*this._valueRange)}}S1.id="logarithmic";S1.defaults={ticks:{callback:Ts.formatters.logarithmic,major:{enabled:!0}}};function df(n){const e=n.ticks;if(e.display&&n.display){const t=Wn(e.backdropPadding);return ht(e.font&&e.font.size,mt.font.size)+t.height}return 0}function D4(n,e,t){return t=Et(t)?t:[t],{w:Xk(n,e.string,t),h:t.length*e.lineHeight}}function Ud(n,e,t,i,o){return n===i||n===o?{start:e-t/2,end:e+t/2}:no?{start:e-t,end:e}:{start:e,end:e+t}}function O4(n){const e={l:n.left+n._padding.left,r:n.right-n._padding.right,t:n.top+n._padding.top,b:n.bottom-n._padding.bottom},t=Object.assign({},e),i=[],o=[],r=n._pointLabels.length,l=n.options.pointLabels,s=l.centerPointLabels?jt/r:0;for(let a=0;ae.r&&(s=(i.end-e.r)/r,n.r=Math.max(n.r,e.r+s)),o.starte.b&&(a=(o.end-e.b)/l,n.b=Math.max(n.b,e.b+a))}function E4(n,e,t){const i=[],o=n._pointLabels.length,r=n.options,l=df(r)/2,s=n.drawingArea,a=r.pointLabels.centerPointLabels?jt/o:0;for(let f=0;f270||t<90)&&(n-=e),n}function I4(n,e){const{ctx:t,options:{pointLabels:i}}=n;for(let o=e-1;o>=0;o--){const r=i.setContext(n.getPointLabelContext(o)),l=Mn(r.font),{x:s,y:a,textAlign:f,left:c,top:u,right:d,bottom:h}=n._pointLabelItems[o],{backdropColor:b}=r;if(!xt(b)){const v=jo(r.borderRadius),_=Wn(r.backdropPadding);t.fillStyle=b;const y=c-_.left,S=u-_.top,C=d-c+_.width,x=h-u+_.height;Object.values(v).some(M=>M!==0)?(t.beginPath(),ds(t,{x:y,y:S,w:C,h:x,radius:v}),t.fill()):t.fillRect(y,S,C,x)}us(t,n._pointLabels[o],s,a+l.lineHeight/2,l,{color:r.color,textAlign:f,textBaseline:"middle"})}}function C1(n,e,t,i){const{ctx:o}=n;if(t)o.arc(n.xCenter,n.yCenter,e,0,Tt);else{let r=n.getPointPosition(0,e);o.moveTo(r.x,r.y);for(let l=1;l{const o=Rt(this.options.pointLabels.callback,[t,i],this);return o||o===0?o:""}).filter((t,i)=>this.chart.getDataVisibility(i))}fit(){const e=this.options;e.display&&e.pointLabels.display?O4(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(e,t,i,o){this.xCenter+=Math.floor((e-t)/2),this.yCenter+=Math.floor((i-o)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(e,t,i,o))}getIndexAngle(e){const t=Tt/(this._pointLabels.length||1),i=this.options.startAngle||0;return Cn(e*t+Xn(i))}getDistanceFromCenterForValue(e){if(xt(e))return NaN;const t=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-e)*t:(e-this.min)*t}getValueForDistanceFromCenter(e){if(xt(e))return NaN;const t=e/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-t:this.min+t}getPointLabelContext(e){const t=this._pointLabels||[];if(e>=0&&e{if(c!==0){s=this.getDistanceFromCenterForValue(f.value);const u=o.setContext(this.getContext(c-1));R4(this,u,s,r)}}),i.display){for(e.save(),l=r-1;l>=0;l--){const f=i.setContext(this.getPointLabelContext(l)),{color:c,lineWidth:u}=f;!u||!c||(e.lineWidth=u,e.strokeStyle=c,e.setLineDash(f.borderDash),e.lineDashOffset=f.borderDashOffset,s=this.getDistanceFromCenterForValue(t.ticks.reverse?this.min:this.max),a=this.getPointPosition(l,s),e.beginPath(),e.moveTo(this.xCenter,this.yCenter),e.lineTo(a.x,a.y),e.stroke())}e.restore()}}drawBorder(){}drawLabels(){const e=this.ctx,t=this.options,i=t.ticks;if(!i.display)return;const o=this.getIndexAngle(0);let r,l;e.save(),e.translate(this.xCenter,this.yCenter),e.rotate(o),e.textAlign="center",e.textBaseline="middle",this.ticks.forEach((s,a)=>{if(a===0&&!t.reverse)return;const f=i.setContext(this.getContext(a)),c=Mn(f.font);if(r=this.getDistanceFromCenterForValue(this.ticks[a].value),f.showLabelBackdrop){e.font=c.string,l=e.measureText(s.label).width,e.fillStyle=f.backdropColor;const u=Wn(f.backdropPadding);e.fillRect(-l/2-u.left,-r-c.size/2-u.top,l+u.width,c.size+u.height)}us(e,s.label,0,-r,c,{color:f.color})}),e.restore()}drawTitle(){}}Ps.id="radialLinear";Ps.defaults={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Ts.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(n){return n},padding:5,centerPointLabels:!1}};Ps.defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};Ps.descriptors={angleLines:{_fallback:"grid"}};const Fs={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},_n=Object.keys(Fs);function j4(n,e){return n-e}function Wd(n,e){if(xt(e))return null;const t=n._adapter,{parser:i,round:o,isoWeekday:r}=n._parseOpts;let l=e;return typeof i=="function"&&(l=i(l)),Ht(l)||(l=typeof i=="string"?t.parse(l,i):t.parse(l)),l===null?null:(o&&(l=o==="week"&&(Ir(r)||r===!0)?t.startOf(l,"isoWeek",r):t.startOf(l,o)),+l)}function Yd(n,e,t,i){const o=_n.length;for(let r=_n.indexOf(n);r=_n.indexOf(t);r--){const l=_n[r];if(Fs[l].common&&n._adapter.diff(o,i,l)>=e-1)return l}return _n[t?_n.indexOf(t):0]}function H4(n){for(let e=_n.indexOf(n)+1,t=_n.length;e=e?t[i]:t[o];n[r]=!0}}function q4(n,e,t,i){const o=n._adapter,r=+o.startOf(e[0].value,i),l=e[e.length-1].value;let s,a;for(s=r;s<=l;s=+o.add(s,1,i))a=t[s],a>=0&&(e[a].major=!0);return e}function Kd(n,e,t){const i=[],o={},r=e.length;let l,s;for(l=0;l+e.value))}initOffsets(e){let t=0,i=0,o,r;this.options.offset&&e.length&&(o=this.getDecimalForValue(e[0]),e.length===1?t=1-o:t=(this.getDecimalForValue(e[1])-o)/2,r=this.getDecimalForValue(e[e.length-1]),e.length===1?i=r:i=(r-this.getDecimalForValue(e[e.length-2]))/2);const l=e.length<3?.5:.25;t=sn(t,0,l),i=sn(i,0,l),this._offsets={start:t,end:i,factor:1/(t+1+i)}}_generate(){const e=this._adapter,t=this.min,i=this.max,o=this.options,r=o.time,l=r.unit||Yd(r.minUnit,t,i,this._getLabelCapacity(t)),s=ht(r.stepSize,1),a=l==="week"?r.isoWeekday:!1,f=Ir(a)||a===!0,c={};let u=t,d,h;if(f&&(u=+e.startOf(u,"isoWeek",a)),u=+e.startOf(u,f?"day":l),e.diff(i,t,l)>1e5*s)throw new Error(t+" and "+i+" are too far apart with stepSize of "+s+" "+l);const b=o.ticks.source==="data"&&this.getDataTimestamps();for(d=u,h=0;dv-_).map(v=>+v)}getLabelForValue(e){const t=this._adapter,i=this.options.time;return i.tooltipFormat?t.format(e,i.tooltipFormat):t.format(e,i.displayFormats.datetime)}_tickFormatFunction(e,t,i,o){const r=this.options,l=r.time.displayFormats,s=this._unit,a=this._majorUnit,f=s&&l[s],c=a&&l[a],u=i[t],d=a&&c&&u&&u.major,h=this._adapter.format(e,o||(d?c:f)),b=r.ticks.callback;return b?Rt(b,[h,t,i],this):h}generateTickLabels(e){let t,i,o;for(t=0,i=e.length;t0?s:1}getDataTimestamps(){let e=this._cache.data||[],t,i;if(e.length)return e;const o=this.getMatchingVisibleMetas();if(this._normalized&&o.length)return this._cache.data=o[0].controller.getAllParsedValues(this);for(t=0,i=o.length;t=n[i].pos&&e<=n[o].pos&&({lo:i,hi:o}=ao(n,"pos",e)),{pos:r,time:s}=n[i],{pos:l,time:a}=n[o]):(e>=n[i].time&&e<=n[o].time&&({lo:i,hi:o}=ao(n,"time",e)),{time:r,pos:s}=n[i],{time:l,pos:a}=n[o]);const f=l-r;return f?s+(a-s)*(e-r)/f:s}class x1 extends tl{constructor(e){super(e),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const e=this._getTimestampsForTable(),t=this._table=this.buildLookupTable(e);this._minPos=Pl(t,this.min),this._tableRange=Pl(t,this.max)-this._minPos,super.initOffsets(e)}buildLookupTable(e){const{min:t,max:i}=this,o=[],r=[];let l,s,a,f,c;for(l=0,s=e.length;l=t&&f<=i&&o.push(f);if(o.length<2)return[{time:t,pos:0},{time:i,pos:1}];for(l=0,s=o.length;l{t||(t=ct(e,Bn,{duration:150},!0)),t.run(1)}),i=!0)},o(o){t||(t=ct(e,Bn,{duration:150},!1)),t.run(0),i=!1},d(o){o&&k(e),o&&t&&t.end()}}}function B4(n){let e,t,i=n[1]===1?"log":"logs",o;return{c(){e=j(n[1]),t=$(),o=j(i)},m(r,l){w(r,e,l),w(r,t,l),w(r,o,l)},p(r,l){l&2&&ge(e,r[1]),l&2&&i!==(i=r[1]===1?"log":"logs")&&ge(o,i)},d(r){r&&k(e),r&&k(t),r&&k(o)}}}function U4(n){let e;return{c(){e=j("Loading...")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function W4(n){let e,t,i,o,r,l,s=n[2]&&Jd();function a(u,d){return u[2]?U4:B4}let f=a(n),c=f(n);return{c(){e=g("div"),s&&s.c(),t=$(),i=g("canvas"),o=$(),r=g("div"),c.c(),p(i,"class","chart-canvas svelte-vh4sl8"),_c(i,"height","250px"),_c(i,"width","100%"),p(e,"class","chart-wrapper svelte-vh4sl8"),ne(e,"loading",n[2]),p(r,"class","txt-hint m-t-xs txt-right")},m(u,d){w(u,e,d),s&&s.m(e,null),m(e,t),m(e,i),n[8](i),w(u,o,d),w(u,r,d),c.m(r,null),l=!0},p(u,[d]){u[2]?s?d&4&&T(s,1):(s=Jd(),s.c(),T(s,1),s.m(e,t)):s&&(Ae(),F(s,1,1,()=>{s=null}),De()),d&4&&ne(e,"loading",u[2]),f===(f=a(u))&&c?c.p(u,d):(c.d(1),c=f(u),c&&(c.c(),c.m(r,null)))},i(u){l||(T(s),l=!0)},o(u){F(s),l=!1},d(u){u&&k(e),s&&s.d(),n[8](null),u&&k(o),u&&k(r),c.d()}}}function Y4(n,e,t){let{filter:i=""}=e,{presets:o=""}=e,r,l,s=[],a=0,f=!1;async function c(){return t(2,f=!0),Se.Logs.getRequestsStats({filter:[o,i].filter(Boolean).join("&&")}).then(h=>{u();for(let b of h)s.push({x:B.getDateTime(b.date).toLocal().toJSDate(),y:b.total}),t(1,a+=b.total);s.push({x:new Date,y:void 0})}).catch(h=>{h!==null&&(u(),console.warn(h),Se.errorResponseHandler(h,!1))}).finally(()=>{t(2,f=!1)})}function u(){t(1,a=0),t(7,s=[])}di(()=>(bs.register(Yi,Es,Qr,cc,tl,m4,S4),t(6,l=new bs(r,{type:"line",data:{datasets:[{label:"Total requests",data:s,borderColor:"#ef4565",pointBackgroundColor:"#ef4565",backgroundColor:"rgb(239,69,101,0.05)",borderWidth:2,pointBorderWidth:0,fill:!0}]},options:{animation:!1,interaction:{intersect:!1,mode:"index"},scales:{y:{beginAtZero:!0,grid:{color:"#edf0f3",borderColor:"#dee3e8"},ticks:{precision:0,maxTicksLimit:6,autoSkip:!0,color:"#666f75"}},x:{type:"time",time:{unit:"hour",tooltipFormat:"DD h a"},grid:{borderColor:"#dee3e8",color:h=>h.tick.major?"#edf0f3":""},ticks:{maxTicksLimit:15,autoSkip:!0,maxRotation:0,major:{enabled:!0},color:h=>h.tick.major?"#16161a":"#666f75"}}},plugins:{legend:{display:!1}}}})),()=>l==null?void 0:l.destroy()));function d(h){he[h?"unshift":"push"](()=>{r=h,t(0,r)})}return n.$$set=h=>{"filter"in h&&t(3,i=h.filter),"presets"in h&&t(4,o=h.presets)},n.$$.update=()=>{n.$$.dirty&24&&(typeof i!="undefined"||typeof o!="undefined")&&c(),n.$$.dirty&192&&typeof s!="undefined"&&l&&(t(6,l.data.datasets[0].data=s,l),l.update())},[r,a,f,i,o,c,l,s,d]}class G4 extends Ie{constructor(e){super(),Le(this,e,Y4,W4,Ee,{filter:3,presets:4,load:5})}get load(){return this.$$.ctx[5]}}var Zd=typeof globalThis!="undefined"?globalThis:typeof window!="undefined"?window:typeof global!="undefined"?global:typeof self!="undefined"?self:{},M1={exports:{}};(function(n){var e=typeof window!="undefined"?window:typeof WorkerGlobalScope!="undefined"&&self instanceof WorkerGlobalScope?self:{};/** + * Prism: Lightweight, robust, elegant syntax highlighting + * + * @license MIT + * @author Lea Verou + * @namespace + * @public + */var t=function(i){var o=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,r=0,l={},s={manual:i.Prism&&i.Prism.manual,disableWorkerMessageHandler:i.Prism&&i.Prism.disableWorkerMessageHandler,util:{encode:function S(C){return C instanceof a?new a(C.type,S(C.content),C.alias):Array.isArray(C)?C.map(S):C.replace(/&/g,"&").replace(/"+A.content+""};function f(S,C,x,M){S.lastIndex=C;var A=S.exec(x);if(A&&M&&A[1]){var O=A[1].length;A.index+=O,A[0]=A[0].slice(O)}return A}function c(S,C,x,M,A,O){for(var D in x)if(!(!x.hasOwnProperty(D)||!x[D])){var E=x[D];E=Array.isArray(E)?E:[E];for(var P=0;P=O.reach);te+=W.value.length,W=W.next){var ce=W.value;if(C.length>S.length)return;if(!(ce instanceof a)){var ve=1,oe;if(U){if(oe=f(Y,te,S,G),!oe||oe.index>=S.length)break;var _e=oe.index,J=oe.index+oe[0].length,$e=te;for($e+=W.value.length;_e>=$e;)W=W.next,$e+=W.value.length;if($e-=W.value.length,te=$e,W.value instanceof a)continue;for(var ee=W;ee!==C.tail&&($eO.reach&&(O.reach=Ne);var Pe=W.prev;ie&&(Pe=d(C,Pe,ie),te+=ie.length),h(C,Pe,ve);var ze=new a(D,R?s.tokenize(fe,R):fe,z,fe);if(W=d(C,Pe,ze),ye&&d(C,W,ye),ve>1){var se={cause:D+","+P,reach:Ne};c(S,C,x,W.prev,te,se),O&&se.reach>O.reach&&(O.reach=se.reach)}}}}}}function u(){var S={value:null,prev:null,next:null},C={value:null,prev:S,next:null};S.next=C,this.head=S,this.tail=C,this.length=0}function d(S,C,x){var M=C.next,A={value:x,prev:C,next:M};return C.next=A,M.prev=A,S.length++,A}function h(S,C,x){for(var M=C.next,A=0;A/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},t.languages.markup.tag.inside["attr-value"].inside.entity=t.languages.markup.entity,t.languages.markup.doctype.inside["internal-subset"].inside=t.languages.markup,t.hooks.add("wrap",function(i){i.type==="entity"&&(i.attributes.title=i.content.replace(/&/,"&"))}),Object.defineProperty(t.languages.markup.tag,"addInlined",{value:function(o,r){var l={};l["language-"+r]={pattern:/(^$)/i,lookbehind:!0,inside:t.languages[r]},l.cdata=/^$/i;var s={"included-cdata":{pattern://i,inside:l}};s["language-"+r]={pattern:/[\s\S]+/,inside:t.languages[r]};var a={};a[o]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,function(){return o}),"i"),lookbehind:!0,greedy:!0,inside:s},t.languages.insertBefore("markup","cdata",a)}}),Object.defineProperty(t.languages.markup.tag,"addAttribute",{value:function(i,o){t.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+i+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[o,"language-"+o],inside:t.languages[o]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),t.languages.html=t.languages.markup,t.languages.mathml=t.languages.markup,t.languages.svg=t.languages.markup,t.languages.xml=t.languages.extend("markup",{}),t.languages.ssml=t.languages.xml,t.languages.atom=t.languages.xml,t.languages.rss=t.languages.xml,function(i){var o=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;i.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+o.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+o.source+"$"),alias:"url"}}},selector:{pattern:RegExp(`(^|[{}\\s])[^{}\\s](?:[^{};"'\\s]|\\s+(?![\\s{])|`+o.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:o,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},i.languages.css.atrule.inside.rest=i.languages.css;var r=i.languages.markup;r&&(r.tag.addInlined("style","css"),r.tag.addAttribute("style","css"))}(t),t.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},t.languages.javascript=t.languages.extend("clike",{"class-name":[t.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+(/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source)+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),t.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,t.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:t.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:t.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:t.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:t.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:t.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),t.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:t.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),t.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),t.languages.markup&&(t.languages.markup.tag.addInlined("script","javascript"),t.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),t.languages.js=t.languages.javascript,function(){if(typeof t=="undefined"||typeof document=="undefined")return;Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector);var i="Loading\u2026",o=function(v,_){return"\u2716 Error "+v+" while fetching file: "+_},r="\u2716 Error: File does not exist or is empty",l={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},s="data-src-status",a="loading",f="loaded",c="failed",u="pre[data-src]:not(["+s+'="'+f+'"]):not(['+s+'="'+a+'"])';function d(v,_,y){var S=new XMLHttpRequest;S.open("GET",v,!0),S.onreadystatechange=function(){S.readyState==4&&(S.status<400&&S.responseText?_(S.responseText):S.status>=400?y(o(S.status,S.statusText)):y(r))},S.send(null)}function h(v){var _=/^\s*(\d+)\s*(?:(,)\s*(?:(\d+)\s*)?)?$/.exec(v||"");if(_){var y=Number(_[1]),S=_[2],C=_[3];return S?C?[y,Number(C)]:[y,void 0]:[y,y]}}t.hooks.add("before-highlightall",function(v){v.selector+=", "+u}),t.hooks.add("before-sanity-check",function(v){var _=v.element;if(_.matches(u)){v.code="",_.setAttribute(s,a);var y=_.appendChild(document.createElement("CODE"));y.textContent=i;var S=_.getAttribute("data-src"),C=v.language;if(C==="none"){var x=(/\.(\w+)$/.exec(S)||[,"none"])[1];C=l[x]||x}t.util.setLanguage(y,C),t.util.setLanguage(_,C);var M=t.plugins.autoloader;M&&M.loadLanguages(C),d(S,function(A){_.setAttribute(s,f);var O=h(_.getAttribute("data-range"));if(O){var D=A.split(/\r\n?|\n/g),E=O[0],P=O[1]==null?D.length:O[1];E<0&&(E+=D.length),E=Math.max(0,Math.min(E-1,D.length)),P<0&&(P+=D.length),P=Math.max(0,Math.min(P,D.length)),A=D.slice(E,P).join(` +`),_.hasAttribute("data-start")||_.setAttribute("data-start",String(E+1))}y.textContent=A,t.highlightElement(y)},function(A){_.setAttribute(s,c),y.textContent=A})}}),t.plugins.fileHighlight={highlight:function(_){for(var y=(_||document).querySelectorAll(u),S=0,C;C=y[S++];)t.highlightElement(C)}};var b=!1;t.fileHighlight=function(){b||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),b=!0),t.plugins.fileHighlight.highlight.apply(this,arguments)}}()})(M1);var mr=M1.exports,K4={exports:{}};(function(n){(function(){if(typeof Prism=="undefined")return;var e=Object.assign||function(r,l){for(var s in l)l.hasOwnProperty(s)&&(r[s]=l[s]);return r};function t(r){this.defaults=e({},r)}function i(r){return r.replace(/-(\w)/g,function(l,s){return s.toUpperCase()})}function o(r){for(var l=0,s=0;sl&&(f[u]=` +`+f[u],c=d)}s[a]=f.join("")}return s.join(` +`)}},n.exports&&(n.exports=t),Prism.plugins.NormalizeWhitespace=new t({"remove-trailing":!0,"remove-indent":!0,"left-trim":!0,"right-trim":!0}),Prism.hooks.add("before-sanity-check",function(r){var l=Prism.plugins.NormalizeWhitespace;if(!(r.settings&&r.settings["whitespace-normalization"]===!1)&&!!Prism.util.isActive(r.element,"whitespace-normalization",!0)){if((!r.element||!r.element.parentNode)&&r.code){r.code=l.normalize(r.code,r.settings);return}var s=r.element.parentNode;if(!(!r.code||!s||s.nodeName.toLowerCase()!=="pre")){for(var a=s.childNodes,f="",c="",u=!1,d=0;d{"content"in s&&t(1,i=s.content),"language"in s&&t(2,o=s.language)},n.$$.update=()=>{n.$$.dirty&2&&typeof mr!="undefined"&&i&&t(0,r=l(i))},[r,i,o]}class tn extends Ie{constructor(e){super(),Le(this,e,Z4,J4,Ee,{content:1,language:2})}}const X4=n=>({}),Xd=n=>({}),Q4=n=>({}),Qd=n=>({});function ep(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y,S,C=n[4]&&!n[2]&&tp(n);const x=n[18].header,M=$n(x,n,n[17],Qd);let A=n[4]&&n[2]&&np(n);const O=n[18].default,D=$n(O,n,n[17],null),E=n[18].footer,P=$n(E,n,n[17],Xd);return{c(){e=g("div"),t=g("div"),o=$(),r=g("div"),l=g("div"),C&&C.c(),s=$(),M&&M.c(),a=$(),A&&A.c(),f=$(),c=g("div"),D&&D.c(),u=$(),d=g("div"),P&&P.c(),p(t,"class","overlay"),p(l,"class","overlay-panel-section panel-header"),p(c,"class","overlay-panel-section panel-content"),p(d,"class","overlay-panel-section panel-footer"),p(r,"class",h="overlay-panel "+n[1]+" "+n[8]),ne(r,"popup",n[2]),p(e,"class","overlay-panel-container"),ne(e,"padded",n[2]),ne(e,"active",n[0])},m(I,R){w(I,e,R),m(e,t),m(e,o),m(e,r),m(r,l),C&&C.m(l,null),m(l,s),M&&M.m(l,null),m(l,a),A&&A.m(l,null),m(r,f),m(r,c),D&&D.m(c,null),n[20](c),m(r,u),m(r,d),P&&P.m(d,null),_=!0,y||(S=[X(t,"click",Gt(n[19])),X(c,"scroll",n[21])],y=!0)},p(I,R){n=I,n[4]&&!n[2]?C?C.p(n,R):(C=tp(n),C.c(),C.m(l,s)):C&&(C.d(1),C=null),M&&M.p&&(!_||R&131072)&&Dn(M,x,n,n[17],_?An(x,n[17],R,Q4):On(n[17]),Qd),n[4]&&n[2]?A?A.p(n,R):(A=np(n),A.c(),A.m(l,null)):A&&(A.d(1),A=null),D&&D.p&&(!_||R&131072)&&Dn(D,O,n,n[17],_?An(O,n[17],R,null):On(n[17]),null),P&&P.p&&(!_||R&131072)&&Dn(P,E,n,n[17],_?An(E,n[17],R,X4):On(n[17]),Xd),(!_||R&258&&h!==(h="overlay-panel "+n[1]+" "+n[8]))&&p(r,"class",h),R&262&&ne(r,"popup",n[2]),R&4&&ne(e,"padded",n[2]),R&1&&ne(e,"active",n[0])},i(I){_||(Dt(()=>{i||(i=ct(t,rs,{duration:Eo,opacity:0},!0)),i.run(1)}),T(M,I),T(D,I),T(P,I),Dt(()=>{v&&v.end(1),b=yf(r,ti,n[2]?{duration:Eo,y:-10}:{duration:Eo,x:50}),b.start()}),_=!0)},o(I){i||(i=ct(t,rs,{duration:Eo,opacity:0},!1)),i.run(0),F(M,I),F(D,I),F(P,I),b&&b.invalidate(),v=Kb(r,ti,n[2]?{duration:Eo,y:10}:{duration:Eo,x:50}),_=!1},d(I){I&&k(e),I&&i&&i.end(),C&&C.d(),M&&M.d(I),A&&A.d(),D&&D.d(I),n[20](null),P&&P.d(I),I&&v&&v.end(),y=!1,rt(S)}}}function tp(n){let e,t,i;return{c(){e=g("div"),e.innerHTML='',p(e,"class","overlay-close")},m(o,r){w(o,e,r),t||(i=X(e,"click",Gt(n[5])),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function np(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-sm btn-circle btn-secondary btn-close m-l-auto")},m(o,r){w(o,e,r),t||(i=X(e,"click",Gt(n[5])),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function eC(n){let e,t,i,o,r=n[0]&&ep(n);return{c(){e=g("div"),r&&r.c(),p(e,"class","overlay-panel-wrapper")},m(l,s){w(l,e,s),r&&r.m(e,null),n[22](e),t=!0,i||(o=[X(window,"resize",n[10]),X(window,"keydown",n[9])],i=!0)},p(l,[s]){l[0]?r?(r.p(l,s),s&1&&T(r,1)):(r=ep(l),r.c(),T(r,1),r.m(e,null)):r&&(Ae(),F(r,1,1,()=>{r=null}),De())},i(l){t||(T(r),t=!0)},o(l){F(r),t=!1},d(l){l&&k(e),r&&r.d(),n[22](null),i=!1,rt(o)}}}let no;function $1(){return no=no||document.querySelector(".overlays"),no||(no=document.createElement("div"),no.classList.add("overlays"),document.body.appendChild(no)),no}let Eo=150;function ip(){return 1e3+$1().querySelectorAll(".overlay-panel-container.active").length}function tC(n,e,t){let{$$slots:i={},$$scope:o}=e,{class:r=""}=e,{active:l=!1}=e,{popup:s=!1}=e,{overlayClose:a=!0}=e,{btnClose:f=!0}=e,{escClose:c=!0}=e,{beforeOpen:u=void 0}=e,{beforeHide:d=void 0}=e;const h=yn();let b,v,_,y,S="";function C(){typeof u=="function"&&u()===!1||t(0,l=!0)}function x(){typeof d=="function"&&d()===!1||t(0,l=!1)}function M(){return l}async function A(z){z?(_=document.activeElement,b==null||b.focus(),h("show")):(clearTimeout(y),_==null||_.focus(),h("hide")),await Bi(),O()}function O(){!b||(l?t(6,b.style.zIndex=ip(),b):t(6,b.style="",b))}function D(z){l&&c&&z.code=="Escape"&&!B.isInput(z.target)&&b&&b.style.zIndex==ip()&&(z.preventDefault(),x())}function E(z){l&&P(v)}function P(z,K){K&&t(8,S=""),z&&(y||(y=setTimeout(()=>{if(clearTimeout(y),y=null,!z)return;if(z.scrollHeight-z.offsetHeight>0)t(8,S="scrollable");else{t(8,S="");return}z.scrollTop==0?t(8,S+=" scroll-top-reached"):z.scrollTop+z.offsetHeight==z.scrollHeight&&t(8,S+=" scroll-bottom-reached")},100)))}di(()=>($1().appendChild(b),()=>{var z;clearTimeout(y),(z=b==null?void 0:b.classList)==null||z.add("hidden")}));const I=()=>a?x():!0;function R(z){he[z?"unshift":"push"](()=>{v=z,t(7,v)})}const G=z=>P(z.target);function U(z){he[z?"unshift":"push"](()=>{b=z,t(6,b)})}return n.$$set=z=>{"class"in z&&t(1,r=z.class),"active"in z&&t(0,l=z.active),"popup"in z&&t(2,s=z.popup),"overlayClose"in z&&t(3,a=z.overlayClose),"btnClose"in z&&t(4,f=z.btnClose),"escClose"in z&&t(12,c=z.escClose),"beforeOpen"in z&&t(13,u=z.beforeOpen),"beforeHide"in z&&t(14,d=z.beforeHide),"$$scope"in z&&t(17,o=z.$$scope)},n.$$.update=()=>{n.$$.dirty&1&&A(l),n.$$.dirty&128&&P(v,!0),n.$$.dirty&64&&b&&O()},[l,r,s,a,f,x,b,v,S,D,E,P,c,u,d,C,M,o,i,I,R,G,U]}class Ai extends Ie{constructor(e){super(),Le(this,e,tC,eC,Ee,{class:1,active:0,popup:2,overlayClose:3,btnClose:4,escClose:12,beforeOpen:13,beforeHide:14,show:15,hide:5,isActive:16})}get show(){return this.$$.ctx[15]}get hide(){return this.$$.ctx[5]}get isActive(){return this.$$.ctx[16]}}function nC(n){let e;return{c(){e=g("span"),e.textContent="N/A",p(e,"class","txt-hint")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function iC(n){let e,t=n[2].referer+"",i,o;return{c(){e=g("a"),i=j(t),p(e,"href",o=n[2].referer),p(e,"target","_blank"),p(e,"rel","noopener noreferrer")},m(r,l){w(r,e,l),m(e,i)},p(r,l){l&4&&t!==(t=r[2].referer+"")&&ge(i,t),l&4&&o!==(o=r[2].referer)&&p(e,"href",o)},d(r){r&&k(e)}}}function oC(n){let e;return{c(){e=g("span"),e.textContent="N/A",p(e,"class","txt-hint")},m(t,i){w(t,e,i)},p:le,i:le,o:le,d(t){t&&k(e)}}}function rC(n){let e,t;return e=new tn({props:{content:JSON.stringify(n[2].meta,null,2)}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,o){const r={};o&4&&(r.content=JSON.stringify(i[2].meta,null,2)),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function lC(n){var ni;let e,t,i,o,r,l,s=n[2].id+"",a,f,c,u,d,h,b,v=n[2].status+"",_,y,S,C,x,M,A=((ni=n[2].method)==null?void 0:ni.toUpperCase())+"",O,D,E,P,I,R,G=n[2].auth+"",U,z,K,Y,W,te,ce=n[2].url+"",ve,oe,J,$e,ee,_e,fe,ie,ye,Ne,Pe,ze=n[2].ip+"",se,re,ke,He,qe,Je,be=n[2].userAgent+"",Oe,Z,ae,Ve,yt,it,bt,at,vt,qt,Mt,$t,me,Ce,Ye,ot;function cn(pe,L){return pe[2].referer?iC:nC}let ue=cn(n),we=ue(n);const Ze=[rC,oC],Kt=[];function zt(pe,L){return L&4&&(bt=null),bt==null&&(bt=!B.isEmpty(pe[2].meta)),bt?0:1}return at=zt(n,-1),vt=Kt[at]=Ze[at](n),Ye=new Ci({props:{date:n[2].created}}),{c(){e=g("table"),t=g("tbody"),i=g("tr"),o=g("td"),o.textContent="ID",r=$(),l=g("td"),a=j(s),f=$(),c=g("tr"),u=g("td"),u.textContent="Status",d=$(),h=g("td"),b=g("span"),_=j(v),y=$(),S=g("tr"),C=g("td"),C.textContent="Method",x=$(),M=g("td"),O=j(A),D=$(),E=g("tr"),P=g("td"),P.textContent="Auth",I=$(),R=g("td"),U=j(G),z=$(),K=g("tr"),Y=g("td"),Y.textContent="URL",W=$(),te=g("td"),ve=j(ce),oe=$(),J=g("tr"),$e=g("td"),$e.textContent="Referer",ee=$(),_e=g("td"),we.c(),fe=$(),ie=g("tr"),ye=g("td"),ye.textContent="IP",Ne=$(),Pe=g("td"),se=j(ze),re=$(),ke=g("tr"),He=g("td"),He.textContent="UserAgent",qe=$(),Je=g("td"),Oe=j(be),Z=$(),ae=g("tr"),Ve=g("td"),Ve.textContent="Meta",yt=$(),it=g("td"),vt.c(),qt=$(),Mt=g("tr"),$t=g("td"),$t.textContent="Created",me=$(),Ce=g("td"),V(Ye.$$.fragment),p(o,"class","min-width txt-hint txt-bold"),p(u,"class","min-width txt-hint txt-bold"),p(b,"class","label"),ne(b,"label-danger",n[2].status>=400),p(C,"class","min-width txt-hint txt-bold"),p(P,"class","min-width txt-hint txt-bold"),p(Y,"class","min-width txt-hint txt-bold"),p($e,"class","min-width txt-hint txt-bold"),p(ye,"class","min-width txt-hint txt-bold"),p(He,"class","min-width txt-hint txt-bold"),p(Ve,"class","min-width txt-hint txt-bold"),p($t,"class","min-width txt-hint txt-bold"),p(e,"class","table-compact table-border")},m(pe,L){w(pe,e,L),m(e,t),m(t,i),m(i,o),m(i,r),m(i,l),m(l,a),m(t,f),m(t,c),m(c,u),m(c,d),m(c,h),m(h,b),m(b,_),m(t,y),m(t,S),m(S,C),m(S,x),m(S,M),m(M,O),m(t,D),m(t,E),m(E,P),m(E,I),m(E,R),m(R,U),m(t,z),m(t,K),m(K,Y),m(K,W),m(K,te),m(te,ve),m(t,oe),m(t,J),m(J,$e),m(J,ee),m(J,_e),we.m(_e,null),m(t,fe),m(t,ie),m(ie,ye),m(ie,Ne),m(ie,Pe),m(Pe,se),m(t,re),m(t,ke),m(ke,He),m(ke,qe),m(ke,Je),m(Je,Oe),m(t,Z),m(t,ae),m(ae,Ve),m(ae,yt),m(ae,it),Kt[at].m(it,null),m(t,qt),m(t,Mt),m(Mt,$t),m(Mt,me),m(Mt,Ce),H(Ye,Ce,null),ot=!0},p(pe,L){var de;(!ot||L&4)&&s!==(s=pe[2].id+"")&&ge(a,s),(!ot||L&4)&&v!==(v=pe[2].status+"")&&ge(_,v),L&4&&ne(b,"label-danger",pe[2].status>=400),(!ot||L&4)&&A!==(A=((de=pe[2].method)==null?void 0:de.toUpperCase())+"")&&ge(O,A),(!ot||L&4)&&G!==(G=pe[2].auth+"")&&ge(U,G),(!ot||L&4)&&ce!==(ce=pe[2].url+"")&&ge(ve,ce),ue===(ue=cn(pe))&&we?we.p(pe,L):(we.d(1),we=ue(pe),we&&(we.c(),we.m(_e,null))),(!ot||L&4)&&ze!==(ze=pe[2].ip+"")&&ge(se,ze),(!ot||L&4)&&be!==(be=pe[2].userAgent+"")&&ge(Oe,be);let N=at;at=zt(pe,L),at===N?Kt[at].p(pe,L):(Ae(),F(Kt[N],1,1,()=>{Kt[N]=null}),De(),vt=Kt[at],vt?vt.p(pe,L):(vt=Kt[at]=Ze[at](pe),vt.c()),T(vt,1),vt.m(it,null));const Q={};L&4&&(Q.date=pe[2].created),Ye.$set(Q)},i(pe){ot||(T(vt),T(Ye.$$.fragment,pe),ot=!0)},o(pe){F(vt),F(Ye.$$.fragment,pe),ot=!1},d(pe){pe&&k(e),we.d(),Kt[at].d(),q(Ye)}}}function sC(n){let e;return{c(){e=g("h4"),e.textContent="Request log"},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function aC(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Close',p(e,"type","button"),p(e,"class","btn btn-secondary")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[4]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function fC(n){let e,t,i={class:"overlay-panel-lg log-panel",$$slots:{footer:[aC],header:[sC],default:[lC]},$$scope:{ctx:n}};return e=new Ai({props:i}),n[5](e),e.$on("hide",n[6]),e.$on("show",n[7]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,[r]){const l={};r&260&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[5](null),q(e,o)}}}function cC(n,e,t){let i,o=new Ha;function r(u){return t(2,o=u),i==null?void 0:i.show()}function l(){return i==null?void 0:i.hide()}const s=()=>l();function a(u){he[u?"unshift":"push"](()=>{i=u,t(1,i)})}function f(u){ft.call(this,n,u)}function c(u){ft.call(this,n,u)}return[l,i,o,r,s,a,f,c]}class uC extends Ie{constructor(e){super(),Le(this,e,cC,fC,Ee,{show:3,hide:0})}get show(){return this.$$.ctx[3]}get hide(){return this.$$.ctx[0]}}function dC(n){let e,t,i,o,r,l,s,a;return{c(){e=g("input"),i=$(),o=g("label"),r=j("Include requests by admins"),p(e,"type","checkbox"),p(e,"id",t=n[12]),p(o,"for",l=n[12])},m(f,c){w(f,e,c),e.checked=n[0],w(f,i,c),w(f,o,c),m(o,r),s||(a=X(e,"change",n[6]),s=!0)},p(f,c){c&4096&&t!==(t=f[12])&&p(e,"id",t),c&1&&(e.checked=f[0]),c&4096&&l!==(l=f[12])&&p(o,"for",l)},d(f){f&&k(e),f&&k(i),f&&k(o),s=!1,a()}}}function op(n){let e,t,i;function o(l){n[8](l)}let r={presets:n[4]};return n[2]!==void 0&&(r.filter=n[2]),e=new G4({props:r}),he.push(()=>Fe(e,"filter",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s&16&&(a.presets=l[4]),!t&&s&4&&(t=!0,a.filter=l[2],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function rp(n){let e,t,i;function o(l){n[9](l)}let r={presets:n[4]};return n[2]!==void 0&&(r.filter=n[2]),e=new dk({props:r}),he.push(()=>Fe(e,"filter",o)),e.$on("select",n[10]),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s&16&&(a.presets=l[4]),!t&&s&4&&(t=!0,a.filter=l[2],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function pC(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y=n[3],S,C=n[3],x,M,A,O,D;u=new je({props:{class:"form-field form-field-toggle m-0",$$slots:{default:[dC,({uniqueId:R})=>({12:R}),({uniqueId:R})=>R?4096:0]},$$scope:{ctx:n}}}),h=new Ds({props:{value:n[2],placeholder:"Search logs, ex. status > 200",extraAutocompleteKeys:["method","url","ip","referer","status","auth","userAgent"]}}),h.$on("submit",n[7]);let E=op(n),P=rp(n),I={};return M=new uC({props:I}),n[11](M),{c(){e=g("main"),t=g("div"),i=g("header"),o=g("nav"),o.innerHTML='',r=$(),l=g("button"),l.innerHTML='',s=$(),a=g("div"),f=$(),c=g("div"),V(u.$$.fragment),d=$(),V(h.$$.fragment),b=$(),v=g("div"),_=$(),E.c(),S=$(),P.c(),x=$(),V(M.$$.fragment),p(o,"class","breadcrumbs"),p(l,"type","button"),p(l,"class","btn btn-circle btn-secondary"),p(a,"class","flex-fill"),p(c,"class","inline-flex"),p(i,"class","page-header"),p(v,"class","clearfix m-b-xs"),p(t,"class","page-header-wrapper m-b-0"),p(e,"class","page-wrapper")},m(R,G){w(R,e,G),m(e,t),m(t,i),m(i,o),m(i,r),m(i,l),m(i,s),m(i,a),m(i,f),m(i,c),H(u,c,null),m(t,d),H(h,t,null),m(t,b),m(t,v),m(t,_),E.m(t,null),m(e,S),P.m(e,null),w(R,x,G),H(M,R,G),A=!0,O||(D=[Xe(St.call(null,l,{text:"Refresh",position:"right"})),X(l,"click",n[5])],O=!0)},p(R,[G]){const U={};G&12289&&(U.$$scope={dirty:G,ctx:R}),u.$set(U);const z={};G&4&&(z.value=R[2]),h.$set(z),G&8&&Ee(y,y=R[3])?(Ae(),F(E,1,1,le),De(),E=op(R),E.c(),T(E,1),E.m(t,null)):E.p(R,G),G&8&&Ee(C,C=R[3])?(Ae(),F(P,1,1,le),De(),P=rp(R),P.c(),T(P,1),P.m(e,null)):P.p(R,G);const K={};M.$set(K)},i(R){A||(T(u.$$.fragment,R),T(h.$$.fragment,R),T(E),T(P),T(M.$$.fragment,R),A=!0)},o(R){F(u.$$.fragment,R),F(h.$$.fragment,R),F(E),F(P),F(M.$$.fragment,R),A=!1},d(R){R&&k(e),q(u),q(h),E.d(R),P.d(R),R&&k(x),n[11](null),q(M,R),O=!1,rt(D)}}}const lp="includeAdminLogs";function hC(n,e,t){var v;let i,o,r="",l=((v=window.localStorage)==null?void 0:v.getItem(lp))<<0,s=1;function a(){t(3,s++,s)}B.setDocumentTitle("Request logs");function f(){l=this.checked,t(0,l)}const c=_=>t(2,r=_.detail);function u(_){r=_,t(2,r)}function d(_){r=_,t(2,r)}const h=_=>o==null?void 0:o.show(_==null?void 0:_.detail);function b(_){he[_?"unshift":"push"](()=>{o=_,t(1,o)})}return n.$$.update=()=>{n.$$.dirty&1&&t(4,i=l?"":'auth!="admin"'),n.$$.dirty&1&&typeof l!="undefined"&&window.localStorage&&window.localStorage.setItem(lp,l<<0)},[l,o,r,s,i,a,f,c,u,d,h,b]}class mC extends Ie{constructor(e){super(),Le(this,e,hC,pC,Ee,{})}}const Go=Mi([]),fi=Mi({}),pf=Mi(!1);function bC(n){fi.update(e=>B.isEmpty(e==null?void 0:e.id)||e.id===n.id?n:e),Go.update(e=>(B.pushOrReplaceByKey(e,n,"id"),e))}function gC(n){Go.update(e=>(B.removeByKey(e,"id",n.id),fi.update(t=>t.id===n.id?e.find(i=>i.name!="profiles")||{}:t),e))}async function _C(n=null){return pf.set(!0),fi.set({}),Go.set([]),Se.Collections.getFullList(200,{sort:"+created"}).then(e=>{Go.set(e);const t=n&&B.findByKey(e,"id",n);if(t)fi.set(t);else if(e.length){const i=e.find(o=>o.name!="profiles");i&&fi.set(i)}}).catch(e=>{Se.errorResponseHandler(e)}).finally(()=>{pf.set(!1)})}const uc=Mi({});function xi(n,e,t){uc.set({text:n,yesCallback:e,noCallback:t})}function A1(){uc.set({})}function sp(n){let e,t,i,o;const r=n[13].default,l=$n(r,n,n[12],null);return{c(){e=g("div"),l&&l.c(),p(e,"class",n[1]),ne(e,"active",n[0])},m(s,a){w(s,e,a),l&&l.m(e,null),o=!0},p(s,a){l&&l.p&&(!o||a&4096)&&Dn(l,r,s,s[12],o?An(r,s[12],a,null):On(s[12]),null),(!o||a&2)&&p(e,"class",s[1]),a&3&&ne(e,"active",s[0])},i(s){o||(T(l,s),s&&Dt(()=>{i&&i.end(1),t=yf(e,ti,{duration:150,y:-5}),t.start()}),o=!0)},o(s){F(l,s),t&&t.invalidate(),s&&(i=Kb(e,ti,{duration:150,y:2})),o=!1},d(s){s&&k(e),l&&l.d(s),s&&i&&i.end()}}}function vC(n){let e,t,i,o,r=n[0]&&sp(n);return{c(){e=g("div"),r&&r.c(),p(e,"class","toggler-container")},m(l,s){w(l,e,s),r&&r.m(e,null),n[14](e),t=!0,i||(o=[X(window,"click",n[3]),X(window,"keydown",n[4]),X(window,"focusin",n[5])],i=!0)},p(l,[s]){l[0]?r?(r.p(l,s),s&1&&T(r,1)):(r=sp(l),r.c(),T(r,1),r.m(e,null)):r&&(Ae(),F(r,1,1,()=>{r=null}),De())},i(l){t||(T(r),t=!0)},o(l){F(r),t=!1},d(l){l&&k(e),r&&r.d(),n[14](null),i=!1,rt(o)}}}function yC(n,e,t){let{$$slots:i={},$$scope:o}=e,{trigger:r=void 0}=e,{active:l=!1}=e,{escClose:s=!0}=e,{closableClass:a="closable"}=e,{class:f=""}=e,c;const u=yn();function d(){t(0,l=!1)}function h(){t(0,l=!0)}function b(){l?d():h()}function v(A){return!c||A.classList.contains(a)||(r==null?void 0:r.contains(A))&&!c.contains(A)||c.contains(A)&&A.closest&&A.closest("."+a)}function _(A){(!l||v(A.target))&&(A.preventDefault(),b())}function y(A){(A.code==="Enter"||A.code==="Space")&&(!l||v(A.target))&&(A.preventDefault(),A.stopPropagation(),b())}function S(A){l&&!(c!=null&&c.contains(A.target))&&!(r!=null&&r.contains(A.target))&&d()}function C(A){l&&s&&A.code=="Escape"&&(A.preventDefault(),d())}function x(A){return S(A)}di(()=>(t(6,r=r||c.parentNode),r.addEventListener("click",_),r.addEventListener("keydown",y),()=>{r.removeEventListener("click",_),r.removeEventListener("keydown",y)}));function M(A){he[A?"unshift":"push"](()=>{c=A,t(2,c)})}return n.$$set=A=>{"trigger"in A&&t(6,r=A.trigger),"active"in A&&t(0,l=A.active),"escClose"in A&&t(7,s=A.escClose),"closableClass"in A&&t(8,a=A.closableClass),"class"in A&&t(1,f=A.class),"$$scope"in A&&t(12,o=A.$$scope)},n.$$.update=()=>{var A,O;n.$$.dirty&65&&(l?((A=r==null?void 0:r.classList)==null||A.add("active"),u("show")):((O=r==null?void 0:r.classList)==null||O.remove("active"),u("hide")))},[l,f,c,S,C,x,r,s,a,d,h,b,o,i,M]}class vo extends Ie{constructor(e){super(),Le(this,e,yC,vC,Ee,{trigger:6,active:0,escClose:7,closableClass:8,class:1,hide:9,show:10,toggle:11})}get hide(){return this.$$.ctx[9]}get show(){return this.$$.ctx[10]}get toggle(){return this.$$.ctx[11]}}const kC=n=>({active:n&1}),ap=n=>({active:n[0]});function fp(n){let e,t,i;const o=n[12].default,r=$n(o,n,n[11],null);return{c(){e=g("div"),r&&r.c(),p(e,"class","accordion-content")},m(l,s){w(l,e,s),r&&r.m(e,null),i=!0},p(l,s){r&&r.p&&(!i||s&2048)&&Dn(r,o,l,l[11],i?An(o,l[11],s,null):On(l[11]),null)},i(l){i||(T(r,l),l&&Dt(()=>{t||(t=ct(e,fn,{duration:150},!0)),t.run(1)}),i=!0)},o(l){F(r,l),l&&(t||(t=ct(e,fn,{duration:150},!1)),t.run(0)),i=!1},d(l){l&&k(e),r&&r.d(l),l&&t&&t.end()}}}function wC(n){let e,t,i,o,r,l,s,a;const f=n[12].header,c=$n(f,n,n[11],ap);let u=n[0]&&fp(n);return{c(){e=g("div"),t=g("header"),c&&c.c(),i=$(),u&&u.c(),p(t,"class","accordion-header"),ne(t,"interactive",n[2]),p(e,"tabindex",o=n[2]?0:-1),p(e,"class",r="accordion "+n[1]),ne(e,"active",n[0])},m(d,h){w(d,e,h),m(e,t),c&&c.m(t,null),m(e,i),u&&u.m(e,null),n[14](e),l=!0,s||(a=[X(t,"click",Gt(n[13])),X(e,"keydown",Vb(n[5]))],s=!0)},p(d,[h]){c&&c.p&&(!l||h&2049)&&Dn(c,f,d,d[11],l?An(f,d[11],h,kC):On(d[11]),ap),h&4&&ne(t,"interactive",d[2]),d[0]?u?(u.p(d,h),h&1&&T(u,1)):(u=fp(d),u.c(),T(u,1),u.m(e,null)):u&&(Ae(),F(u,1,1,()=>{u=null}),De()),(!l||h&4&&o!==(o=d[2]?0:-1))&&p(e,"tabindex",o),(!l||h&2&&r!==(r="accordion "+d[1]))&&p(e,"class",r),h&3&&ne(e,"active",d[0])},i(d){l||(T(c,d),T(u),l=!0)},o(d){F(c,d),F(u),l=!1},d(d){d&&k(e),c&&c.d(d),u&&u.d(),n[14](null),s=!1,rt(a)}}}function SC(n,e,t){let{$$slots:i={},$$scope:o}=e;const r=yn();let l,s,{class:a=""}=e,{active:f=!1}=e,{interactive:c=!0}=e,{single:u=!1}=e;function d(){v(),t(0,f=!0),r("expand")}function h(){t(0,f=!1),clearTimeout(s),r("collapse")}function b(){r("toggle"),f?h():d()}function v(){if(u&&l.parentElement){const C=l.parentElement.querySelectorAll(".accordion.active .accordion-header.interactive");for(const x of C)x.click()}}function _(C){!c||(C.code==="Enter"||C.code==="Space")&&(C.preventDefault(),b())}di(()=>()=>clearTimeout(s));const y=()=>c&&b();function S(C){he[C?"unshift":"push"](()=>{l=C,t(4,l)})}return n.$$set=C=>{"class"in C&&t(1,a=C.class),"active"in C&&t(0,f=C.active),"interactive"in C&&t(2,c=C.interactive),"single"in C&&t(6,u=C.single),"$$scope"in C&&t(11,o=C.$$scope)},n.$$.update=()=>{n.$$.dirty&1041&&f&&(clearTimeout(s),t(10,s=setTimeout(()=>{l!=null&&l.scrollIntoView&&l.scrollIntoView({behavior:"smooth",block:"nearest"})},250)))},[f,a,c,b,l,_,u,d,h,v,s,o,i,y,S]}class dc extends Ie{constructor(e){super(),Le(this,e,SC,wC,Ee,{class:1,active:0,interactive:2,single:6,expand:7,collapse:8,toggle:3,collapseSiblings:9})}get expand(){return this.$$.ctx[7]}get collapse(){return this.$$.ctx[8]}get toggle(){return this.$$.ctx[3]}get collapseSiblings(){return this.$$.ctx[9]}}const CC=n=>({}),cp=n=>({});function up(n,e,t){const i=n.slice();return i[46]=e[t],i}function dp(n,e,t){const i=n.slice();return i[49]=e[t],i}const xC=n=>({}),pp=n=>({});function hp(n,e,t){const i=n.slice();return i[49]=e[t],i}function mp(n){let e,t;return{c(){e=g("div"),t=j(n[2]),p(e,"class","txt-placeholder")},m(i,o){w(i,e,o),m(e,t)},p(i,o){o[0]&4&&ge(t,i[2])},d(i){i&&k(e)}}}function MC(n){let e,t=n[49]+"",i;return{c(){e=g("span"),i=j(t),p(e,"class","txt")},m(o,r){w(o,e,r),m(e,i)},p(o,r){r[0]&1&&t!==(t=o[49]+"")&&ge(i,t)},i:le,o:le,d(o){o&&k(e)}}}function $C(n){let e,t,i;const o=[{item:n[49]},n[8]];var r=n[7];function l(s){let a={};for(let f=0;f{q(c,1)}),De()}r?(e=new r(l()),V(e.$$.fragment),T(e.$$.fragment,1),H(e,t.parentNode,t)):e=null}else r&&e.$set(f)},i(s){i||(e&&T(e.$$.fragment,s),i=!0)},o(s){e&&F(e.$$.fragment,s),i=!1},d(s){s&&k(t),e&&q(e,s)}}}function bp(n){let e,t,i;function o(){return n[34](n[49])}return{c(){e=g("span"),e.innerHTML='',p(e,"class","clear")},m(r,l){w(r,e,l),t||(i=[Xe(St.call(null,e,"Clear")),X(e,"click",Vn(Gt(o)))],t=!0)},p(r,l){n=r},d(r){r&&k(e),t=!1,rt(i)}}}function gp(n){let e,t,i,o,r,l;const s=[$C,MC],a=[];function f(u,d){return u[7]?0:1}t=f(n),i=a[t]=s[t](n);let c=(n[4]||n[6])&&bp(n);return{c(){e=g("div"),i.c(),o=$(),c&&c.c(),r=$(),p(e,"class","option")},m(u,d){w(u,e,d),a[t].m(e,null),m(e,o),c&&c.m(e,null),m(e,r),l=!0},p(u,d){let h=t;t=f(u),t===h?a[t].p(u,d):(Ae(),F(a[h],1,1,()=>{a[h]=null}),De(),i=a[t],i?i.p(u,d):(i=a[t]=s[t](u),i.c()),T(i,1),i.m(e,o)),u[4]||u[6]?c?c.p(u,d):(c=bp(u),c.c(),c.m(e,r)):c&&(c.d(1),c=null)},i(u){l||(T(i),l=!0)},o(u){F(i),l=!1},d(u){u&&k(e),a[t].d(),c&&c.d()}}}function _p(n){let e,t,i={class:"dropdown dropdown-block options-dropdown dropdown-left",trigger:n[17],$$slots:{default:[OC]},$$scope:{ctx:n}};return e=new vo({props:i}),n[39](e),e.$on("show",n[23]),e.$on("hide",n[40]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,r){const l={};r[0]&131072&&(l.trigger=o[17]),r[0]&806410|r[1]&2048&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[39](null),q(e,o)}}}function vp(n){let e,t,i,o,r,l,s,a,f=n[14].length&&yp(n);return{c(){e=g("div"),t=g("label"),i=g("div"),i.innerHTML='',o=$(),r=g("input"),l=$(),f&&f.c(),p(i,"class","addon p-r-0"),r.autofocus=!0,p(r,"type","text"),p(r,"placeholder",n[3]),p(t,"class","input-group"),p(e,"class","form-field form-field-sm options-search")},m(c,u){w(c,e,u),m(e,t),m(t,i),m(t,o),m(t,r),Me(r,n[14]),m(t,l),f&&f.m(t,null),r.focus(),s||(a=X(r,"input",n[36]),s=!0)},p(c,u){u[0]&8&&p(r,"placeholder",c[3]),u[0]&16384&&r.value!==c[14]&&Me(r,c[14]),c[14].length?f?f.p(c,u):(f=yp(c),f.c(),f.m(t,null)):f&&(f.d(1),f=null)},d(c){c&&k(e),f&&f.d(),s=!1,a()}}}function yp(n){let e,t,i,o;return{c(){e=g("div"),t=g("button"),t.innerHTML='',p(t,"type","button"),p(t,"class","btn btn-sm btn-circle btn-secondary clear"),p(e,"class","addon suffix p-r-5")},m(r,l){w(r,e,l),m(e,t),i||(o=X(t,"click",Vn(Gt(n[20]))),i=!0)},p:le,d(r){r&&k(e),i=!1,o()}}}function kp(n){let e,t=n[1]&&wp(n);return{c(){t&&t.c(),e=lt()},m(i,o){t&&t.m(i,o),w(i,e,o)},p(i,o){i[1]?t?t.p(i,o):(t=wp(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){t&&t.d(i),i&&k(e)}}}function wp(n){let e,t;return{c(){e=g("div"),t=j(n[1]),p(e,"class","txt-missing")},m(i,o){w(i,e,o),m(e,t)},p(i,o){o[0]&2&&ge(t,i[1])},d(i){i&&k(e)}}}function Sp(n){let e,t=n[46].group+"",i;return{c(){e=g("div"),i=j(t),p(e,"class","dropdown-item separator")},m(o,r){w(o,e,r),m(e,i)},p(o,r){r[0]&524288&&t!==(t=o[46].group+"")&&ge(i,t)},d(o){o&&k(e)}}}function AC(n){let e=n[49]+"",t;return{c(){t=j(e)},m(i,o){w(i,t,o)},p(i,o){o[0]&524288&&e!==(e=i[49]+"")&&ge(t,e)},i:le,o:le,d(i){i&&k(t)}}}function DC(n){let e,t,i;const o=[{item:n[49]},n[10]];var r=n[9];function l(s){let a={};for(let f=0;f{q(c,1)}),De()}r?(e=new r(l()),V(e.$$.fragment),T(e.$$.fragment,1),H(e,t.parentNode,t)):e=null}else r&&e.$set(f)},i(s){i||(e&&T(e.$$.fragment,s),i=!0)},o(s){e&&F(e.$$.fragment,s),i=!1},d(s){s&&k(t),e&&q(e,s)}}}function Cp(n){let e,t,i,o,r,l,s;const a=[DC,AC],f=[];function c(h,b){return h[9]?0:1}t=c(n),i=f[t]=a[t](n);function u(...h){return n[37](n[49],...h)}function d(...h){return n[38](n[49],...h)}return{c(){e=g("div"),i.c(),o=$(),p(e,"tabindex","0"),p(e,"class","dropdown-item option closable"),ne(e,"selected",n[18](n[49]))},m(h,b){w(h,e,b),f[t].m(e,null),m(e,o),r=!0,l||(s=[X(e,"click",u),X(e,"keydown",d)],l=!0)},p(h,b){n=h;let v=t;t=c(n),t===v?f[t].p(n,b):(Ae(),F(f[v],1,1,()=>{f[v]=null}),De(),i=f[t],i?i.p(n,b):(i=f[t]=a[t](n),i.c()),T(i,1),i.m(e,o)),b[0]&786432&&ne(e,"selected",n[18](n[49]))},i(h){r||(T(i),r=!0)},o(h){F(i),r=!1},d(h){h&&k(e),f[t].d(),l=!1,rt(s)}}}function xp(n){let e,t,i,o=n[46].group!=hf&&Sp(n),r=n[46].items,l=[];for(let a=0;aF(l[a],1,1,()=>{l[a]=null});return{c(){o&&o.c(),e=$();for(let a=0;aF(c[v],1,1,()=>{c[v]=null});let d=null;f.length||(d=kp(n));const h=n[33].afterOptions,b=$n(h,n,n[42],cp);return{c(){l&&l.c(),e=$(),a&&a.c(),t=$(),i=g("div");for(let v=0;vF(a[d],1,1,()=>{a[d]=null});let c=null;s.length||(c=mp(n));let u=!n[5]&&_p(n);return{c(){e=g("div"),t=g("div");for(let d=0;d{u=null}),De()):u?(u.p(d,h),h[0]&32&&T(u,1)):(u=_p(d),u.c(),T(u,1),u.m(e,null)),(!l||h[0]&4096&&r!==(r="select "+d[12]))&&p(e,"class",r),h[0]&4112&&ne(e,"multiple",d[4]),h[0]&4128&&ne(e,"disabled",d[5])},i(d){if(!l){for(let h=0;hHe(Oe,re)))||[]:be=Je.items||[],be.length&&ke.push({group:Je.group,items:be})}return ke}function ve(se,re){se.preventDefault(),_&&h?U(re):G(re)}function oe(se,re){(se.code==="Enter"||se.code==="Space")&&ve(se,re)}function J(){te(),setTimeout(()=>{const se=P==null?void 0:P.querySelector(".dropdown-item.option.selected");se&&(se.focus(),se.scrollIntoView({block:"nearest"}))},0)}function $e(se){se.stopPropagation(),!b&&(D==null||D.toggle())}di(()=>{const se=document.querySelectorAll(`label[for="${a}"]`);for(const re of se)re.addEventListener("click",$e);return()=>{for(const re of se)re.removeEventListener("click",$e)}});const ee=se=>R(se);function _e(se){he[se?"unshift":"push"](()=>{I=se,t(17,I)})}function fe(){E=this.value,t(14,E)}const ie=(se,re)=>ve(re,se),ye=(se,re)=>oe(re,se);function Ne(se){he[se?"unshift":"push"](()=>{D=se,t(15,D)})}function Pe(se){ft.call(this,n,se)}function ze(se){he[se?"unshift":"push"](()=>{P=se,t(16,P)})}return n.$$set=se=>{"id"in se&&t(24,a=se.id),"noOptionsText"in se&&t(1,f=se.noOptionsText),"selectPlaceholder"in se&&t(2,c=se.selectPlaceholder),"searchPlaceholder"in se&&t(3,u=se.searchPlaceholder),"items"in se&&t(25,d=se.items),"multiple"in se&&t(4,h=se.multiple),"disabled"in se&&t(5,b=se.disabled),"selected"in se&&t(0,v=se.selected),"toggle"in se&&t(6,_=se.toggle),"labelComponent"in se&&t(7,y=se.labelComponent),"labelComponentProps"in se&&t(8,S=se.labelComponentProps),"optionComponent"in se&&t(9,C=se.optionComponent),"optionComponentProps"in se&&t(10,x=se.optionComponentProps),"searchable"in se&&t(11,M=se.searchable),"searchFunc"in se&&t(26,A=se.searchFunc),"class"in se&&t(12,O=se.class),"$$scope"in se&&t(42,s=se.$$scope)},n.$$.update=()=>{n.$$.dirty[0]&33554432&&t(32,i=B.isObjectArrayWithKeys(d,["group"])?d:[{group:hf,items:d}]),n.$$.dirty[0]&33554432&&d&&(W(),te()),n.$$.dirty[0]&16384|n.$$.dirty[1]&2&&t(19,o=ce(i,E)),n.$$.dirty[0]&1&&t(18,r=function(se){let re=B.toArray(v);return B.inArray(re,se)})},[v,f,c,u,h,b,_,y,S,C,x,M,O,R,E,D,P,I,r,o,te,ve,oe,J,a,d,A,G,U,z,K,Y,i,l,ee,_e,fe,ie,ye,Ne,Pe,ze,s]}class D1 extends Ie{constructor(e){super(),Le(this,e,PC,TC,Ee,{id:24,noOptionsText:1,selectPlaceholder:2,searchPlaceholder:3,items:25,multiple:4,disabled:5,selected:0,toggle:6,labelComponent:7,labelComponentProps:8,optionComponent:9,optionComponentProps:10,searchable:11,searchFunc:26,class:12,deselectItem:13,selectItem:27,toggleItem:28,reset:29,showDropdown:30,hideDropdown:31},null,[-1,-1])}get deselectItem(){return this.$$.ctx[13]}get selectItem(){return this.$$.ctx[27]}get toggleItem(){return this.$$.ctx[28]}get reset(){return this.$$.ctx[29]}get showDropdown(){return this.$$.ctx[30]}get hideDropdown(){return this.$$.ctx[31]}}function Mp(n){let e,t;return{c(){e=g("i"),p(e,"class",t="icon "+n[0].icon)},m(i,o){w(i,e,o)},p(i,o){o&1&&t!==(t="icon "+i[0].icon)&&p(e,"class",t)},d(i){i&&k(e)}}}function FC(n){let e,t,i=(n[0].label||n[0].name||n[0].title||n[0].id||n[0].value)+"",o,r=n[0].icon&&Mp(n);return{c(){r&&r.c(),e=$(),t=g("span"),o=j(i),p(t,"class","txt")},m(l,s){r&&r.m(l,s),w(l,e,s),w(l,t,s),m(t,o)},p(l,[s]){l[0].icon?r?r.p(l,s):(r=Mp(l),r.c(),r.m(e.parentNode,e)):r&&(r.d(1),r=null),s&1&&i!==(i=(l[0].label||l[0].name||l[0].title||l[0].id||l[0].value)+"")&&ge(o,i)},i:le,o:le,d(l){r&&r.d(l),l&&k(e),l&&k(t)}}}function LC(n,e,t){let{item:i={}}=e;return n.$$set=o=>{"item"in o&&t(0,i=o.item)},[i]}class $p extends Ie{constructor(e){super(),Le(this,e,LC,FC,Ee,{item:0})}}const IC=n=>({}),Ap=n=>({});function RC(n){let e;const t=n[8].afterOptions,i=$n(t,n,n[12],Ap);return{c(){i&&i.c()},m(o,r){i&&i.m(o,r),e=!0},p(o,r){i&&i.p&&(!e||r&4096)&&Dn(i,t,o,o[12],e?An(t,o[12],r,IC):On(o[12]),Ap)},i(o){e||(T(i,o),e=!0)},o(o){F(i,o),e=!1},d(o){i&&i.d(o)}}}function NC(n){let e,t,i;const o=[{items:n[1]},{multiple:n[2]},{labelComponent:n[3]},{optionComponent:n[4]},n[5]];function r(s){n[9](s)}let l={$$slots:{afterOptions:[RC]},$$scope:{ctx:n}};for(let s=0;sFe(e,"selected",r)),e.$on("show",n[10]),e.$on("hide",n[11]),{c(){V(e.$$.fragment)},m(s,a){H(e,s,a),i=!0},p(s,[a]){const f=a&62?bn(o,[a&2&&{items:s[1]},a&4&&{multiple:s[2]},a&8&&{labelComponent:s[3]},a&16&&{optionComponent:s[4]},a&32&&pi(s[5])]):{};a&4096&&(f.$$scope={dirty:a,ctx:s}),!t&&a&1&&(t=!0,f.selected=s[0],Re(()=>t=!1)),e.$set(f)},i(s){i||(T(e.$$.fragment,s),i=!0)},o(s){F(e.$$.fragment,s),i=!1},d(s){q(e,s)}}}function jC(n,e,t){const i=["items","multiple","selected","labelComponent","optionComponent","selectionKey","keyOfSelected"];let o=Wt(e,i),{$$slots:r={},$$scope:l}=e,{items:s=[]}=e,{multiple:a=!1}=e,{selected:f=a?[]:void 0}=e,{labelComponent:c=$p}=e,{optionComponent:u=$p}=e,{selectionKey:d="value"}=e,{keyOfSelected:h=a?[]:void 0}=e;function b(x){x=B.toArray(x,!0);let M=[],A=_();for(let O of A)B.inArray(x,O[d])&&M.push(O);x.length&&!M.length||t(0,f=a?M:M[0])}async function v(x){let M=B.toArray(x,!0).map(A=>A[d]);!s.length||t(6,h=a?M:M[0])}function _(){if(!B.isObjectArrayWithKeys(s,["group","items"]))return s;let x=[];for(const M of s)x=x.concat(M.items);return x}function y(x){f=x,t(0,f)}function S(x){ft.call(this,n,x)}function C(x){ft.call(this,n,x)}return n.$$set=x=>{e=ut(ut({},e),ui(x)),t(5,o=Wt(e,i)),"items"in x&&t(1,s=x.items),"multiple"in x&&t(2,a=x.multiple),"selected"in x&&t(0,f=x.selected),"labelComponent"in x&&t(3,c=x.labelComponent),"optionComponent"in x&&t(4,u=x.optionComponent),"selectionKey"in x&&t(7,d=x.selectionKey),"keyOfSelected"in x&&t(6,h=x.keyOfSelected),"$$scope"in x&&t(12,l=x.$$scope)},n.$$.update=()=>{n.$$.dirty&66&&s&&b(h),n.$$.dirty&1&&v(f)},[f,s,a,c,u,o,h,d,r,y,S,C,l]}class yo extends Ie{constructor(e){super(),Le(this,e,jC,NC,Ee,{items:1,multiple:2,selected:0,labelComponent:3,optionComponent:4,selectionKey:7,keyOfSelected:6})}}function zC(n){let e,t,i;const o=[{class:"field-type-select "+n[1]},{searchable:!0},{items:n[2]},n[3]];function r(s){n[4](s)}let l={};for(let s=0;sFe(e,"keyOfSelected",r)),{c(){V(e.$$.fragment)},m(s,a){H(e,s,a),i=!0},p(s,[a]){const f=a&14?bn(o,[a&2&&{class:"field-type-select "+s[1]},o[1],a&4&&{items:s[2]},a&8&&pi(s[3])]):{};!t&&a&1&&(t=!0,f.keyOfSelected=s[0],Re(()=>t=!1)),e.$set(f)},i(s){i||(T(e.$$.fragment,s),i=!0)},o(s){F(e.$$.fragment,s),i=!1},d(s){q(e,s)}}}function HC(n,e,t){const i=["value","class"];let o=Wt(e,i),{value:r="text"}=e,{class:l=""}=e;const s=[{label:"Text",value:"text",icon:B.getFieldTypeIcon("text")},{label:"Number",value:"number",icon:B.getFieldTypeIcon("number")},{label:"Bool",value:"bool",icon:B.getFieldTypeIcon("bool")},{label:"Email",value:"email",icon:B.getFieldTypeIcon("email")},{label:"Url",value:"url",icon:B.getFieldTypeIcon("url")},{label:"DateTime",value:"date",icon:B.getFieldTypeIcon("date")},{label:"Multiple choices",value:"select",icon:B.getFieldTypeIcon("select")},{label:"JSON",value:"json",icon:B.getFieldTypeIcon("json")},{label:"File",value:"file",icon:B.getFieldTypeIcon("file")},{label:"Relation",value:"relation",icon:B.getFieldTypeIcon("relation")},{label:"User",value:"user",icon:B.getFieldTypeIcon("user")}];function a(f){r=f,t(0,r)}return n.$$set=f=>{e=ut(ut({},e),ui(f)),t(3,o=Wt(e,i)),"value"in f&&t(0,r=f.value),"class"in f&&t(1,l=f.class)},[r,l,s,o,a]}class qC extends Ie{constructor(e){super(),Le(this,e,HC,zC,Ee,{value:0,class:1})}}function VC(n){let e,t,i,o,r,l,s,a;return{c(){e=g("label"),t=j("Min length"),o=$(),r=g("input"),p(e,"for",i=n[5]),p(r,"type","number"),p(r,"id",l=n[5]),p(r,"step","1"),p(r,"min","0")},m(f,c){w(f,e,c),m(e,t),w(f,o,c),w(f,r,c),Me(r,n[0].min),s||(a=X(r,"input",n[2]),s=!0)},p(f,c){c&32&&i!==(i=f[5])&&p(e,"for",i),c&32&&l!==(l=f[5])&&p(r,"id",l),c&1&&At(r.value)!==f[0].min&&Me(r,f[0].min)},d(f){f&&k(e),f&&k(o),f&&k(r),s=!1,a()}}}function BC(n){let e,t,i,o,r,l,s,a,f;return{c(){e=g("label"),t=j("Max length"),o=$(),r=g("input"),p(e,"for",i=n[5]),p(r,"type","number"),p(r,"id",l=n[5]),p(r,"step","1"),p(r,"min",s=n[0].min||0)},m(c,u){w(c,e,u),m(e,t),w(c,o,u),w(c,r,u),Me(r,n[0].max),a||(f=X(r,"input",n[3]),a=!0)},p(c,u){u&32&&i!==(i=c[5])&&p(e,"for",i),u&32&&l!==(l=c[5])&&p(r,"id",l),u&1&&s!==(s=c[0].min||0)&&p(r,"min",s),u&1&&At(r.value)!==c[0].max&&Me(r,c[0].max)},d(c){c&&k(e),c&&k(o),c&&k(r),a=!1,f()}}}function UC(n){let e,t,i,o,r,l,s,a,f,c;return{c(){e=g("label"),t=j("Regex pattern"),o=$(),r=g("input"),s=$(),a=g("div"),a.innerHTML="Valid Go regular expression, eg. ^\\w+$.",p(e,"for",i=n[5]),p(r,"type","text"),p(r,"id",l=n[5]),p(a,"class","help-block")},m(u,d){w(u,e,d),m(e,t),w(u,o,d),w(u,r,d),Me(r,n[0].pattern),w(u,s,d),w(u,a,d),f||(c=X(r,"input",n[4]),f=!0)},p(u,d){d&32&&i!==(i=u[5])&&p(e,"for",i),d&32&&l!==(l=u[5])&&p(r,"id",l),d&1&&r.value!==u[0].pattern&&Me(r,u[0].pattern)},d(u){u&&k(e),u&&k(o),u&&k(r),u&&k(s),u&&k(a),f=!1,c()}}}function WC(n){let e,t,i,o,r,l,s,a,f,c;return i=new je({props:{class:"form-field",name:"schema."+n[1]+".options.min",$$slots:{default:[VC,({uniqueId:u})=>({5:u}),({uniqueId:u})=>u?32:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field",name:"schema."+n[1]+".options.max",$$slots:{default:[BC,({uniqueId:u})=>({5:u}),({uniqueId:u})=>u?32:0]},$$scope:{ctx:n}}}),f=new je({props:{class:"form-field",name:"schema."+n[1]+".options.pattern",$$slots:{default:[UC,({uniqueId:u})=>({5:u}),({uniqueId:u})=>u?32:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),s=$(),a=g("div"),V(f.$$.fragment),p(t,"class","col-sm-6"),p(r,"class","col-sm-6"),p(a,"class","col-sm-12"),p(e,"class","grid")},m(u,d){w(u,e,d),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),m(e,s),m(e,a),H(f,a,null),c=!0},p(u,[d]){const h={};d&2&&(h.name="schema."+u[1]+".options.min"),d&97&&(h.$$scope={dirty:d,ctx:u}),i.$set(h);const b={};d&2&&(b.name="schema."+u[1]+".options.max"),d&97&&(b.$$scope={dirty:d,ctx:u}),l.$set(b);const v={};d&2&&(v.name="schema."+u[1]+".options.pattern"),d&97&&(v.$$scope={dirty:d,ctx:u}),f.$set(v)},i(u){c||(T(i.$$.fragment,u),T(l.$$.fragment,u),T(f.$$.fragment,u),c=!0)},o(u){F(i.$$.fragment,u),F(l.$$.fragment,u),F(f.$$.fragment,u),c=!1},d(u){u&&k(e),q(i),q(l),q(f)}}}function YC(n,e,t){let{key:i=""}=e,{options:o={}}=e;function r(){o.min=At(this.value),t(0,o)}function l(){o.max=At(this.value),t(0,o)}function s(){o.pattern=this.value,t(0,o)}return n.$$set=a=>{"key"in a&&t(1,i=a.key),"options"in a&&t(0,o=a.options)},[o,i,r,l,s]}class GC extends Ie{constructor(e){super(),Le(this,e,YC,WC,Ee,{key:1,options:0})}}function KC(n){let e,t,i,o,r,l,s,a;return{c(){e=g("label"),t=j("Min"),o=$(),r=g("input"),p(e,"for",i=n[4]),p(r,"type","number"),p(r,"id",l=n[4])},m(f,c){w(f,e,c),m(e,t),w(f,o,c),w(f,r,c),Me(r,n[0].min),s||(a=X(r,"input",n[2]),s=!0)},p(f,c){c&16&&i!==(i=f[4])&&p(e,"for",i),c&16&&l!==(l=f[4])&&p(r,"id",l),c&1&&At(r.value)!==f[0].min&&Me(r,f[0].min)},d(f){f&&k(e),f&&k(o),f&&k(r),s=!1,a()}}}function JC(n){let e,t,i,o,r,l,s,a,f;return{c(){e=g("label"),t=j("Max"),o=$(),r=g("input"),p(e,"for",i=n[4]),p(r,"type","number"),p(r,"id",l=n[4]),p(r,"min",s=n[0].min)},m(c,u){w(c,e,u),m(e,t),w(c,o,u),w(c,r,u),Me(r,n[0].max),a||(f=X(r,"input",n[3]),a=!0)},p(c,u){u&16&&i!==(i=c[4])&&p(e,"for",i),u&16&&l!==(l=c[4])&&p(r,"id",l),u&1&&s!==(s=c[0].min)&&p(r,"min",s),u&1&&At(r.value)!==c[0].max&&Me(r,c[0].max)},d(c){c&&k(e),c&&k(o),c&&k(r),a=!1,f()}}}function ZC(n){let e,t,i,o,r,l,s;return i=new je({props:{class:"form-field",name:"schema."+n[1]+".options.min",$$slots:{default:[KC,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field",name:"schema."+n[1]+".options.max",$$slots:{default:[JC,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),p(t,"class","col-sm-6"),p(r,"class","col-sm-6"),p(e,"class","grid")},m(a,f){w(a,e,f),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),s=!0},p(a,[f]){const c={};f&2&&(c.name="schema."+a[1]+".options.min"),f&49&&(c.$$scope={dirty:f,ctx:a}),i.$set(c);const u={};f&2&&(u.name="schema."+a[1]+".options.max"),f&49&&(u.$$scope={dirty:f,ctx:a}),l.$set(u)},i(a){s||(T(i.$$.fragment,a),T(l.$$.fragment,a),s=!0)},o(a){F(i.$$.fragment,a),F(l.$$.fragment,a),s=!1},d(a){a&&k(e),q(i),q(l)}}}function XC(n,e,t){let{key:i=""}=e,{options:o={}}=e;function r(){o.min=At(this.value),t(0,o)}function l(){o.max=At(this.value),t(0,o)}return n.$$set=s=>{"key"in s&&t(1,i=s.key),"options"in s&&t(0,o=s.options)},[o,i,r,l]}class QC extends Ie{constructor(e){super(),Le(this,e,XC,ZC,Ee,{key:1,options:0})}}function e5(n,e,t){let{key:i=""}=e,{options:o={}}=e;return n.$$set=r=>{"key"in r&&t(0,i=r.key),"options"in r&&t(1,o=r.options)},[i,o]}class t5 extends Ie{constructor(e){super(),Le(this,e,e5,null,Ee,{key:0,options:1})}}function n5(n){let e,t,i,o,r=[{type:t=n[3].type||"text"},{value:n[2]},n[3]],l={};for(let s=0;s{t(0,l=B.splitNonEmpty(f.target.value,s))};return n.$$set=f=>{e=ut(ut({},e),ui(f)),t(3,r=Wt(e,o)),"value"in f&&t(0,l=f.value),"separator"in f&&t(1,s=f.separator)},n.$$.update=()=>{n.$$.dirty&1&&t(2,i=(l||[]).join(","))},[l,s,i,r,a]}class ko extends Ie{constructor(e){super(),Le(this,e,i5,n5,Ee,{value:0,separator:1})}}function o5(n){let e,t,i,o,r,l,s,a,f,c,u,d,h;function b(_){n[2](_)}let v={id:n[4],disabled:!B.isEmpty(n[0].onlyDomains)};return n[0].exceptDomains!==void 0&&(v.value=n[0].exceptDomains),s=new ko({props:v}),he.push(()=>Fe(s,"value",b)),{c(){e=g("label"),t=g("span"),t.textContent="Except domains",i=$(),o=g("i"),l=$(),V(s.$$.fragment),f=$(),c=g("div"),c.textContent="Use comma as separator.",p(t,"class","txt"),p(o,"class","ri-information-line link-hint"),p(e,"for",r=n[4]),p(c,"class","help-block")},m(_,y){w(_,e,y),m(e,t),m(e,i),m(e,o),w(_,l,y),H(s,_,y),w(_,f,y),w(_,c,y),u=!0,d||(h=Xe(St.call(null,o,{text:`Domains that are NOT allowed as value. + This field is disabled if "Only domains" is set.`,position:"top"})),d=!0)},p(_,y){(!u||y&16&&r!==(r=_[4]))&&p(e,"for",r);const S={};y&16&&(S.id=_[4]),y&1&&(S.disabled=!B.isEmpty(_[0].onlyDomains)),!a&&y&1&&(a=!0,S.value=_[0].exceptDomains,Re(()=>a=!1)),s.$set(S)},i(_){u||(T(s.$$.fragment,_),u=!0)},o(_){F(s.$$.fragment,_),u=!1},d(_){_&&k(e),_&&k(l),q(s,_),_&&k(f),_&&k(c),d=!1,h()}}}function r5(n){let e,t,i,o,r,l,s,a,f,c,u,d,h;function b(_){n[3](_)}let v={id:n[4]+".options.onlyDomains",disabled:!B.isEmpty(n[0].exceptDomains)};return n[0].onlyDomains!==void 0&&(v.value=n[0].onlyDomains),s=new ko({props:v}),he.push(()=>Fe(s,"value",b)),{c(){e=g("label"),t=g("span"),t.textContent="Only domains",i=$(),o=g("i"),l=$(),V(s.$$.fragment),f=$(),c=g("div"),c.textContent="Use comma as separator.",p(t,"class","txt"),p(o,"class","ri-information-line link-hint"),p(e,"for",r=n[4]+".options.onlyDomains"),p(c,"class","help-block")},m(_,y){w(_,e,y),m(e,t),m(e,i),m(e,o),w(_,l,y),H(s,_,y),w(_,f,y),w(_,c,y),u=!0,d||(h=Xe(St.call(null,o,{text:`Domains that are ONLY allowed as value. + This field is disabled if "Except domains" is set.`,position:"top"})),d=!0)},p(_,y){(!u||y&16&&r!==(r=_[4]+".options.onlyDomains"))&&p(e,"for",r);const S={};y&16&&(S.id=_[4]+".options.onlyDomains"),y&1&&(S.disabled=!B.isEmpty(_[0].exceptDomains)),!a&&y&1&&(a=!0,S.value=_[0].onlyDomains,Re(()=>a=!1)),s.$set(S)},i(_){u||(T(s.$$.fragment,_),u=!0)},o(_){F(s.$$.fragment,_),u=!1},d(_){_&&k(e),_&&k(l),q(s,_),_&&k(f),_&&k(c),d=!1,h()}}}function l5(n){let e,t,i,o,r,l,s;return i=new je({props:{class:"form-field",name:"schema."+n[1]+".options.exceptDomains",$$slots:{default:[o5,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field",name:"schema."+n[1]+".options.onlyDomains",$$slots:{default:[r5,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),p(t,"class","col-sm-6"),p(r,"class","col-sm-6"),p(e,"class","grid")},m(a,f){w(a,e,f),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),s=!0},p(a,[f]){const c={};f&2&&(c.name="schema."+a[1]+".options.exceptDomains"),f&49&&(c.$$scope={dirty:f,ctx:a}),i.$set(c);const u={};f&2&&(u.name="schema."+a[1]+".options.onlyDomains"),f&49&&(u.$$scope={dirty:f,ctx:a}),l.$set(u)},i(a){s||(T(i.$$.fragment,a),T(l.$$.fragment,a),s=!0)},o(a){F(i.$$.fragment,a),F(l.$$.fragment,a),s=!1},d(a){a&&k(e),q(i),q(l)}}}function s5(n,e,t){let{key:i=""}=e,{options:o={}}=e;function r(s){n.$$.not_equal(o.exceptDomains,s)&&(o.exceptDomains=s,t(0,o))}function l(s){n.$$.not_equal(o.onlyDomains,s)&&(o.onlyDomains=s,t(0,o))}return n.$$set=s=>{"key"in s&&t(1,i=s.key),"options"in s&&t(0,o=s.options)},[o,i,r,l]}class O1 extends Ie{constructor(e){super(),Le(this,e,s5,l5,Ee,{key:1,options:0})}}function a5(n){let e,t,i,o;function r(a){n[2](a)}function l(a){n[3](a)}let s={};return n[0]!==void 0&&(s.key=n[0]),n[1]!==void 0&&(s.options=n[1]),e=new O1({props:s}),he.push(()=>Fe(e,"key",r)),he.push(()=>Fe(e,"options",l)),{c(){V(e.$$.fragment)},m(a,f){H(e,a,f),o=!0},p(a,[f]){const c={};!t&&f&1&&(t=!0,c.key=a[0],Re(()=>t=!1)),!i&&f&2&&(i=!0,c.options=a[1],Re(()=>i=!1)),e.$set(c)},i(a){o||(T(e.$$.fragment,a),o=!0)},o(a){F(e.$$.fragment,a),o=!1},d(a){q(e,a)}}}function f5(n,e,t){let{key:i=""}=e,{options:o={}}=e;function r(s){i=s,t(0,i)}function l(s){o=s,t(1,o)}return n.$$set=s=>{"key"in s&&t(0,i=s.key),"options"in s&&t(1,o=s.options)},[i,o,r,l]}class c5 extends Ie{constructor(e){super(),Le(this,e,f5,a5,Ee,{key:0,options:1})}}var Sa=["onChange","onClose","onDayCreate","onDestroy","onKeyDown","onMonthChange","onOpen","onParseConfig","onReady","onValueUpdate","onYearChange","onPreCalendarPosition"],zo={_disable:[],allowInput:!1,allowInvalidPreload:!1,altFormat:"F j, Y",altInput:!1,altInputClass:"form-control input",animate:typeof window=="object"&&window.navigator.userAgent.indexOf("MSIE")===-1,ariaDateFormat:"F j, Y",autoFillDefaultTime:!0,clickOpens:!0,closeOnSelect:!0,conjunction:", ",dateFormat:"Y-m-d",defaultHour:12,defaultMinute:0,defaultSeconds:0,disable:[],disableMobile:!1,enableSeconds:!1,enableTime:!1,errorHandler:function(n){return typeof console!="undefined"&&console.warn(n)},getWeek:function(n){var e=new Date(n.getTime());e.setHours(0,0,0,0),e.setDate(e.getDate()+3-(e.getDay()+6)%7);var t=new Date(e.getFullYear(),0,4);return 1+Math.round(((e.getTime()-t.getTime())/864e5-3+(t.getDay()+6)%7)/7)},hourIncrement:1,ignoredFocusElements:[],inline:!1,locale:"default",minuteIncrement:5,mode:"single",monthSelectorType:"dropdown",nextArrow:"",noCalendar:!1,now:new Date,onChange:[],onClose:[],onDayCreate:[],onDestroy:[],onKeyDown:[],onMonthChange:[],onOpen:[],onParseConfig:[],onReady:[],onValueUpdate:[],onYearChange:[],onPreCalendarPosition:[],plugins:[],position:"auto",positionElement:void 0,prevArrow:"",shorthandCurrentMonth:!1,showMonths:1,static:!1,time_24hr:!1,weekNumbers:!1,wrap:!1},Hr={weekdays:{shorthand:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],longhand:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},months:{shorthand:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],longhand:["January","February","March","April","May","June","July","August","September","October","November","December"]},daysInMonth:[31,28,31,30,31,30,31,31,30,31,30,31],firstDayOfWeek:0,ordinal:function(n){var e=n%100;if(e>3&&e<21)return"th";switch(e%10){case 1:return"st";case 2:return"nd";case 3:return"rd";default:return"th"}},rangeSeparator:" to ",weekAbbreviation:"Wk",scrollTitle:"Scroll to increment",toggleTitle:"Click to toggle",amPM:["AM","PM"],yearAriaLabel:"Year",monthAriaLabel:"Month",hourAriaLabel:"Hour",minuteAriaLabel:"Minute",time_24hr:!1},gn=function(n,e){return e===void 0&&(e=2),("000"+n).slice(e*-1)},Nn=function(n){return n===!0?1:0};function Dp(n,e){var t;return function(){var i=this,o=arguments;clearTimeout(t),t=setTimeout(function(){return n.apply(i,o)},e)}}var Ca=function(n){return n instanceof Array?n:[n]};function un(n,e,t){if(t===!0)return n.classList.add(e);n.classList.remove(e)}function wt(n,e,t){var i=window.document.createElement(n);return e=e||"",t=t||"",i.className=e,t!==void 0&&(i.textContent=t),i}function Fl(n){for(;n.firstChild;)n.removeChild(n.firstChild)}function T1(n,e){if(e(n))return n;if(n.parentNode)return T1(n.parentNode,e)}function Ll(n,e){var t=wt("div","numInputWrapper"),i=wt("input","numInput "+n),o=wt("span","arrowUp"),r=wt("span","arrowDown");if(navigator.userAgent.indexOf("MSIE 9.0")===-1?i.type="number":(i.type="text",i.pattern="\\d*"),e!==void 0)for(var l in e)i.setAttribute(l,e[l]);return t.appendChild(i),t.appendChild(o),t.appendChild(r),t}function wn(n){try{if(typeof n.composedPath=="function"){var e=n.composedPath();return e[0]}return n.target}catch{return n.target}}var xa=function(){},_s=function(n,e,t){return t.months[e?"shorthand":"longhand"][n]},u5={D:xa,F:function(n,e,t){n.setMonth(t.months.longhand.indexOf(e))},G:function(n,e){n.setHours((n.getHours()>=12?12:0)+parseFloat(e))},H:function(n,e){n.setHours(parseFloat(e))},J:function(n,e){n.setDate(parseFloat(e))},K:function(n,e,t){n.setHours(n.getHours()%12+12*Nn(new RegExp(t.amPM[1],"i").test(e)))},M:function(n,e,t){n.setMonth(t.months.shorthand.indexOf(e))},S:function(n,e){n.setSeconds(parseFloat(e))},U:function(n,e){return new Date(parseFloat(e)*1e3)},W:function(n,e,t){var i=parseInt(e),o=new Date(n.getFullYear(),0,2+(i-1)*7,0,0,0,0);return o.setDate(o.getDate()-o.getDay()+t.firstDayOfWeek),o},Y:function(n,e){n.setFullYear(parseFloat(e))},Z:function(n,e){return new Date(e)},d:function(n,e){n.setDate(parseFloat(e))},h:function(n,e){n.setHours((n.getHours()>=12?12:0)+parseFloat(e))},i:function(n,e){n.setMinutes(parseFloat(e))},j:function(n,e){n.setDate(parseFloat(e))},l:xa,m:function(n,e){n.setMonth(parseFloat(e)-1)},n:function(n,e){n.setMonth(parseFloat(e)-1)},s:function(n,e){n.setSeconds(parseFloat(e))},u:function(n,e){return new Date(parseFloat(e))},w:xa,y:function(n,e){n.setFullYear(2e3+parseFloat(e))}},lo={D:"",F:"",G:"(\\d\\d|\\d)",H:"(\\d\\d|\\d)",J:"(\\d\\d|\\d)\\w+",K:"",M:"",S:"(\\d\\d|\\d)",U:"(.+)",W:"(\\d\\d|\\d)",Y:"(\\d{4})",Z:"(.+)",d:"(\\d\\d|\\d)",h:"(\\d\\d|\\d)",i:"(\\d\\d|\\d)",j:"(\\d\\d|\\d)",l:"",m:"(\\d\\d|\\d)",n:"(\\d\\d|\\d)",s:"(\\d\\d|\\d)",u:"(.+)",w:"(\\d\\d|\\d)",y:"(\\d{2})"},Tr={Z:function(n){return n.toISOString()},D:function(n,e,t){return e.weekdays.shorthand[Tr.w(n,e,t)]},F:function(n,e,t){return _s(Tr.n(n,e,t)-1,!1,e)},G:function(n,e,t){return gn(Tr.h(n,e,t))},H:function(n){return gn(n.getHours())},J:function(n,e){return e.ordinal!==void 0?n.getDate()+e.ordinal(n.getDate()):n.getDate()},K:function(n,e){return e.amPM[Nn(n.getHours()>11)]},M:function(n,e){return _s(n.getMonth(),!0,e)},S:function(n){return gn(n.getSeconds())},U:function(n){return n.getTime()/1e3},W:function(n,e,t){return t.getWeek(n)},Y:function(n){return gn(n.getFullYear(),4)},d:function(n){return gn(n.getDate())},h:function(n){return n.getHours()%12?n.getHours()%12:12},i:function(n){return gn(n.getMinutes())},j:function(n){return n.getDate()},l:function(n,e){return e.weekdays.longhand[n.getDay()]},m:function(n){return gn(n.getMonth()+1)},n:function(n){return n.getMonth()+1},s:function(n){return n.getSeconds()},u:function(n){return n.getTime()},w:function(n){return n.getDay()},y:function(n){return String(n.getFullYear()).substring(2)}},E1=function(n){var e=n.config,t=e===void 0?zo:e,i=n.l10n,o=i===void 0?Hr:i,r=n.isMobile,l=r===void 0?!1:r;return function(s,a,f){var c=f||o;return t.formatDate!==void 0&&!l?t.formatDate(s,a,c):a.split("").map(function(u,d,h){return Tr[u]&&h[d-1]!=="\\"?Tr[u](s,c,t):u!=="\\"?u:""}).join("")}},mf=function(n){var e=n.config,t=e===void 0?zo:e,i=n.l10n,o=i===void 0?Hr:i;return function(r,l,s,a){if(!(r!==0&&!r)){var f=a||o,c,u=r;if(r instanceof Date)c=new Date(r.getTime());else if(typeof r!="string"&&r.toFixed!==void 0)c=new Date(r);else if(typeof r=="string"){var d=l||(t||zo).dateFormat,h=String(r).trim();if(h==="today")c=new Date,s=!0;else if(t&&t.parseDate)c=t.parseDate(r,d);else if(/Z$/.test(h)||/GMT$/.test(h))c=new Date(r);else{for(var b=void 0,v=[],_=0,y=0,S="";_Math.min(e,t)&&n=0?new Date:new Date(t.config.minDate.getTime()),Q=$a(t.config);N.setHours(Q.hours,Q.minutes,Q.seconds,N.getMilliseconds()),t.selectedDates=[N],t.latestSelectedDateObj=N}L!==void 0&&L.type!=="blur"&&pe(L);var de=t._input.value;u(),zt(),t._input.value!==de&&t._debouncedChange()}function f(L,N){return L%12+12*Nn(N===t.l10n.amPM[1])}function c(L){switch(L%24){case 0:case 12:return 12;default:return L%12}}function u(){if(!(t.hourElement===void 0||t.minuteElement===void 0)){var L=(parseInt(t.hourElement.value.slice(-2),10)||0)%24,N=(parseInt(t.minuteElement.value,10)||0)%60,Q=t.secondElement!==void 0?(parseInt(t.secondElement.value,10)||0)%60:0;t.amPM!==void 0&&(L=f(L,t.amPM.textContent));var de=t.config.minTime!==void 0||t.config.minDate&&t.minDateHasTime&&t.latestSelectedDateObj&&Sn(t.latestSelectedDateObj,t.config.minDate,!0)===0,Te=t.config.maxTime!==void 0||t.config.maxDate&&t.maxDateHasTime&&t.latestSelectedDateObj&&Sn(t.latestSelectedDateObj,t.config.maxDate,!0)===0;if(t.config.maxTime!==void 0&&t.config.minTime!==void 0&&t.config.minTime>t.config.maxTime){var Ue=Ma(t.config.minTime.getHours(),t.config.minTime.getMinutes(),t.config.minTime.getSeconds()),tt=Ma(t.config.maxTime.getHours(),t.config.maxTime.getMinutes(),t.config.maxTime.getSeconds()),Ge=Ma(L,N,Q);if(Ge>tt&&Ge=12)]),t.secondElement!==void 0&&(t.secondElement.value=gn(Q)))}function b(L){var N=wn(L),Q=parseInt(N.value)+(L.delta||0);(Q/1e3>1||L.key==="Enter"&&!/[^\d]/.test(Q.toString()))&&fe(Q)}function v(L,N,Q,de){if(N instanceof Array)return N.forEach(function(Te){return v(L,Te,Q,de)});if(L instanceof Array)return L.forEach(function(Te){return v(Te,N,Q,de)});L.addEventListener(N,Q,de),t._handlers.push({remove:function(){return L.removeEventListener(N,Q,de)}})}function _(){ot("onChange")}function y(){if(t.config.wrap&&["open","close","toggle","clear"].forEach(function(Q){Array.prototype.forEach.call(t.element.querySelectorAll("[data-"+Q+"]"),function(de){return v(de,"click",t[Q])})}),t.isMobile){Ce();return}var L=Dp(se,50);if(t._debouncedChange=Dp(_,m5),t.daysContainer&&!/iPhone|iPad|iPod/i.test(navigator.userAgent)&&v(t.daysContainer,"mouseover",function(Q){t.config.mode==="range"&&ze(wn(Q))}),v(t._input,"keydown",Pe),t.calendarContainer!==void 0&&v(t.calendarContainer,"keydown",Pe),!t.config.inline&&!t.config.static&&v(window,"resize",L),window.ontouchstart!==void 0?v(window.document,"touchstart",_e):v(window.document,"mousedown",_e),v(window.document,"focus",_e,{capture:!0}),t.config.clickOpens===!0&&(v(t._input,"focus",t.open),v(t._input,"click",t.open)),t.daysContainer!==void 0&&(v(t.monthNav,"click",ni),v(t.monthNav,["keyup","increment"],b),v(t.daysContainer,"click",yt)),t.timeContainer!==void 0&&t.minuteElement!==void 0&&t.hourElement!==void 0){var N=function(Q){return wn(Q).select()};v(t.timeContainer,["increment"],a),v(t.timeContainer,"blur",a,{capture:!0}),v(t.timeContainer,"click",C),v([t.hourElement,t.minuteElement],["focus","click"],N),t.secondElement!==void 0&&v(t.secondElement,"focus",function(){return t.secondElement&&t.secondElement.select()}),t.amPM!==void 0&&v(t.amPM,"click",function(Q){a(Q)})}t.config.allowInput&&v(t._input,"blur",Ne)}function S(L,N){var Q=L!==void 0?t.parseDate(L):t.latestSelectedDateObj||(t.config.minDate&&t.config.minDate>t.now?t.config.minDate:t.config.maxDate&&t.config.maxDate1),t.calendarContainer.appendChild(L);var Te=t.config.appendTo!==void 0&&t.config.appendTo.nodeType!==void 0;if((t.config.inline||t.config.static)&&(t.calendarContainer.classList.add(t.config.inline?"inline":"static"),t.config.inline&&(!Te&&t.element.parentNode?t.element.parentNode.insertBefore(t.calendarContainer,t._input.nextSibling):t.config.appendTo!==void 0&&t.config.appendTo.appendChild(t.calendarContainer)),t.config.static)){var Ue=wt("div","flatpickr-wrapper");t.element.parentNode&&t.element.parentNode.insertBefore(Ue,t.element),Ue.appendChild(t.element),t.altInput&&Ue.appendChild(t.altInput),Ue.appendChild(t.calendarContainer)}!t.config.static&&!t.config.inline&&(t.config.appendTo!==void 0?t.config.appendTo:window.document.body).appendChild(t.calendarContainer)}function A(L,N,Q,de){var Te=ie(N,!0),Ue=wt("span",L,N.getDate().toString());return Ue.dateObj=N,Ue.$i=de,Ue.setAttribute("aria-label",t.formatDate(N,t.config.ariaDateFormat)),L.indexOf("hidden")===-1&&Sn(N,t.now)===0&&(t.todayDateElem=Ue,Ue.classList.add("today"),Ue.setAttribute("aria-current","date")),Te?(Ue.tabIndex=-1,ue(N)&&(Ue.classList.add("selected"),t.selectedDateElem=Ue,t.config.mode==="range"&&(un(Ue,"startRange",t.selectedDates[0]&&Sn(N,t.selectedDates[0],!0)===0),un(Ue,"endRange",t.selectedDates[1]&&Sn(N,t.selectedDates[1],!0)===0),L==="nextMonthDay"&&Ue.classList.add("inRange")))):Ue.classList.add("flatpickr-disabled"),t.config.mode==="range"&&we(N)&&!ue(N)&&Ue.classList.add("inRange"),t.weekNumbers&&t.config.showMonths===1&&L!=="prevMonthDay"&&de%7===6&&t.weekNumbers.insertAdjacentHTML("beforeend",""+t.config.getWeek(N)+""),ot("onDayCreate",Ue),Ue}function O(L){L.focus(),t.config.mode==="range"&&ze(L)}function D(L){for(var N=L>0?0:t.config.showMonths-1,Q=L>0?t.config.showMonths:-1,de=N;de!=Q;de+=L)for(var Te=t.daysContainer.children[de],Ue=L>0?0:Te.children.length-1,tt=L>0?Te.children.length:-1,Ge=Ue;Ge!=tt;Ge+=L){var nt=Te.children[Ge];if(nt.className.indexOf("hidden")===-1&&ie(nt.dateObj))return nt}}function E(L,N){for(var Q=L.className.indexOf("Month")===-1?L.dateObj.getMonth():t.currentMonth,de=N>0?t.config.showMonths:-1,Te=N>0?1:-1,Ue=Q-t.currentMonth;Ue!=de;Ue+=Te)for(var tt=t.daysContainer.children[Ue],Ge=Q-t.currentMonth===Ue?L.$i+N:N<0?tt.children.length-1:0,nt=tt.children.length,Ke=Ge;Ke>=0&&Ke0?nt:-1);Ke+=Te){var et=tt.children[Ke];if(et.className.indexOf("hidden")===-1&&ie(et.dateObj)&&Math.abs(L.$i-Ke)>=Math.abs(N))return O(et)}t.changeMonth(Te),P(D(Te),0)}function P(L,N){var Q=r(),de=ye(Q||document.body),Te=L!==void 0?L:de?Q:t.selectedDateElem!==void 0&&ye(t.selectedDateElem)?t.selectedDateElem:t.todayDateElem!==void 0&&ye(t.todayDateElem)?t.todayDateElem:D(N>0?1:-1);Te===void 0?t._input.focus():de?E(Te,N):O(Te)}function I(L,N){for(var Q=(new Date(L,N,1).getDay()-t.l10n.firstDayOfWeek+7)%7,de=t.utils.getDaysInMonth((N-1+12)%12,L),Te=t.utils.getDaysInMonth(N,L),Ue=window.document.createDocumentFragment(),tt=t.config.showMonths>1,Ge=tt?"prevMonthDay hidden":"prevMonthDay",nt=tt?"nextMonthDay hidden":"nextMonthDay",Ke=de+1-Q,et=0;Ke<=de;Ke++,et++)Ue.appendChild(A("flatpickr-day "+Ge,new Date(L,N-1,Ke),Ke,et));for(Ke=1;Ke<=Te;Ke++,et++)Ue.appendChild(A("flatpickr-day",new Date(L,N,Ke),Ke,et));for(var gt=Te+1;gt<=42-Q&&(t.config.showMonths===1||et%7!==0);gt++,et++)Ue.appendChild(A("flatpickr-day "+nt,new Date(L,N+1,gt%Te),gt,et));var Ft=wt("div","dayContainer");return Ft.appendChild(Ue),Ft}function R(){if(t.daysContainer!==void 0){Fl(t.daysContainer),t.weekNumbers&&Fl(t.weekNumbers);for(var L=document.createDocumentFragment(),N=0;N1||t.config.monthSelectorType!=="dropdown")){var L=function(de){return t.config.minDate!==void 0&&t.currentYear===t.config.minDate.getFullYear()&&det.config.maxDate.getMonth())};t.monthsDropdownContainer.tabIndex=-1,t.monthsDropdownContainer.innerHTML="";for(var N=0;N<12;N++)if(!!L(N)){var Q=wt("option","flatpickr-monthDropdown-month");Q.value=new Date(t.currentYear,N).getMonth().toString(),Q.textContent=_s(N,t.config.shorthandCurrentMonth,t.l10n),Q.tabIndex=-1,t.currentMonth===N&&(Q.selected=!0),t.monthsDropdownContainer.appendChild(Q)}}}function U(){var L=wt("div","flatpickr-month"),N=window.document.createDocumentFragment(),Q;t.config.showMonths>1||t.config.monthSelectorType==="static"?Q=wt("span","cur-month"):(t.monthsDropdownContainer=wt("select","flatpickr-monthDropdown-months"),t.monthsDropdownContainer.setAttribute("aria-label",t.l10n.monthAriaLabel),v(t.monthsDropdownContainer,"change",function(tt){var Ge=wn(tt),nt=parseInt(Ge.value,10);t.changeMonth(nt-t.currentMonth),ot("onMonthChange")}),G(),Q=t.monthsDropdownContainer);var de=Ll("cur-year",{tabindex:"-1"}),Te=de.getElementsByTagName("input")[0];Te.setAttribute("aria-label",t.l10n.yearAriaLabel),t.config.minDate&&Te.setAttribute("min",t.config.minDate.getFullYear().toString()),t.config.maxDate&&(Te.setAttribute("max",t.config.maxDate.getFullYear().toString()),Te.disabled=!!t.config.minDate&&t.config.minDate.getFullYear()===t.config.maxDate.getFullYear());var Ue=wt("div","flatpickr-current-month");return Ue.appendChild(Q),Ue.appendChild(de),N.appendChild(Ue),L.appendChild(N),{container:L,yearElement:Te,monthElement:Q}}function z(){Fl(t.monthNav),t.monthNav.appendChild(t.prevMonthNav),t.config.showMonths&&(t.yearElements=[],t.monthElements=[]);for(var L=t.config.showMonths;L--;){var N=U();t.yearElements.push(N.yearElement),t.monthElements.push(N.monthElement),t.monthNav.appendChild(N.container)}t.monthNav.appendChild(t.nextMonthNav)}function K(){return t.monthNav=wt("div","flatpickr-months"),t.yearElements=[],t.monthElements=[],t.prevMonthNav=wt("span","flatpickr-prev-month"),t.prevMonthNav.innerHTML=t.config.prevArrow,t.nextMonthNav=wt("span","flatpickr-next-month"),t.nextMonthNav.innerHTML=t.config.nextArrow,z(),Object.defineProperty(t,"_hidePrevMonthArrow",{get:function(){return t.__hidePrevMonthArrow},set:function(L){t.__hidePrevMonthArrow!==L&&(un(t.prevMonthNav,"flatpickr-disabled",L),t.__hidePrevMonthArrow=L)}}),Object.defineProperty(t,"_hideNextMonthArrow",{get:function(){return t.__hideNextMonthArrow},set:function(L){t.__hideNextMonthArrow!==L&&(un(t.nextMonthNav,"flatpickr-disabled",L),t.__hideNextMonthArrow=L)}}),t.currentYearElement=t.yearElements[0],Ze(),t.monthNav}function Y(){t.calendarContainer.classList.add("hasTime"),t.config.noCalendar&&t.calendarContainer.classList.add("noCalendar");var L=$a(t.config);t.timeContainer=wt("div","flatpickr-time"),t.timeContainer.tabIndex=-1;var N=wt("span","flatpickr-time-separator",":"),Q=Ll("flatpickr-hour",{"aria-label":t.l10n.hourAriaLabel});t.hourElement=Q.getElementsByTagName("input")[0];var de=Ll("flatpickr-minute",{"aria-label":t.l10n.minuteAriaLabel});if(t.minuteElement=de.getElementsByTagName("input")[0],t.hourElement.tabIndex=t.minuteElement.tabIndex=-1,t.hourElement.value=gn(t.latestSelectedDateObj?t.latestSelectedDateObj.getHours():t.config.time_24hr?L.hours:c(L.hours)),t.minuteElement.value=gn(t.latestSelectedDateObj?t.latestSelectedDateObj.getMinutes():L.minutes),t.hourElement.setAttribute("step",t.config.hourIncrement.toString()),t.minuteElement.setAttribute("step",t.config.minuteIncrement.toString()),t.hourElement.setAttribute("min",t.config.time_24hr?"0":"1"),t.hourElement.setAttribute("max",t.config.time_24hr?"23":"12"),t.hourElement.setAttribute("maxlength","2"),t.minuteElement.setAttribute("min","0"),t.minuteElement.setAttribute("max","59"),t.minuteElement.setAttribute("maxlength","2"),t.timeContainer.appendChild(Q),t.timeContainer.appendChild(N),t.timeContainer.appendChild(de),t.config.time_24hr&&t.timeContainer.classList.add("time24hr"),t.config.enableSeconds){t.timeContainer.classList.add("hasSeconds");var Te=Ll("flatpickr-second");t.secondElement=Te.getElementsByTagName("input")[0],t.secondElement.value=gn(t.latestSelectedDateObj?t.latestSelectedDateObj.getSeconds():L.seconds),t.secondElement.setAttribute("step",t.minuteElement.getAttribute("step")),t.secondElement.setAttribute("min","0"),t.secondElement.setAttribute("max","59"),t.secondElement.setAttribute("maxlength","2"),t.timeContainer.appendChild(wt("span","flatpickr-time-separator",":")),t.timeContainer.appendChild(Te)}return t.config.time_24hr||(t.amPM=wt("span","flatpickr-am-pm",t.l10n.amPM[Nn((t.latestSelectedDateObj?t.hourElement.value:t.config.defaultHour)>11)]),t.amPM.title=t.l10n.toggleTitle,t.amPM.tabIndex=-1,t.timeContainer.appendChild(t.amPM)),t.timeContainer}function W(){t.weekdayContainer?Fl(t.weekdayContainer):t.weekdayContainer=wt("div","flatpickr-weekdays");for(var L=t.config.showMonths;L--;){var N=wt("div","flatpickr-weekdaycontainer");t.weekdayContainer.appendChild(N)}return te(),t.weekdayContainer}function te(){if(!!t.weekdayContainer){var L=t.l10n.firstDayOfWeek,N=Op(t.l10n.weekdays.shorthand);L>0&&L + `+N.join("")+` + + `}}function ce(){t.calendarContainer.classList.add("hasWeeks");var L=wt("div","flatpickr-weekwrapper");L.appendChild(wt("span","flatpickr-weekday",t.l10n.weekAbbreviation));var N=wt("div","flatpickr-weeks");return L.appendChild(N),{weekWrapper:L,weekNumbers:N}}function ve(L,N){N===void 0&&(N=!0);var Q=N?L:L-t.currentMonth;Q<0&&t._hidePrevMonthArrow===!0||Q>0&&t._hideNextMonthArrow===!0||(t.currentMonth+=Q,(t.currentMonth<0||t.currentMonth>11)&&(t.currentYear+=t.currentMonth>11?1:-1,t.currentMonth=(t.currentMonth+12)%12,ot("onYearChange"),G()),R(),ot("onMonthChange"),Ze())}function oe(L,N){if(L===void 0&&(L=!0),N===void 0&&(N=!0),t.input.value="",t.altInput!==void 0&&(t.altInput.value=""),t.mobileInput!==void 0&&(t.mobileInput.value=""),t.selectedDates=[],t.latestSelectedDateObj=void 0,N===!0&&(t.currentYear=t._initialDate.getFullYear(),t.currentMonth=t._initialDate.getMonth()),t.config.enableTime===!0){var Q=$a(t.config),de=Q.hours,Te=Q.minutes,Ue=Q.seconds;h(de,Te,Ue)}t.redraw(),L&&ot("onChange")}function J(){t.isOpen=!1,t.isMobile||(t.calendarContainer!==void 0&&t.calendarContainer.classList.remove("open"),t._input!==void 0&&t._input.classList.remove("active")),ot("onClose")}function $e(){t.config!==void 0&&ot("onDestroy");for(var L=t._handlers.length;L--;)t._handlers[L].remove();if(t._handlers=[],t.mobileInput)t.mobileInput.parentNode&&t.mobileInput.parentNode.removeChild(t.mobileInput),t.mobileInput=void 0;else if(t.calendarContainer&&t.calendarContainer.parentNode)if(t.config.static&&t.calendarContainer.parentNode){var N=t.calendarContainer.parentNode;if(N.lastChild&&N.removeChild(N.lastChild),N.parentNode){for(;N.firstChild;)N.parentNode.insertBefore(N.firstChild,N);N.parentNode.removeChild(N)}}else t.calendarContainer.parentNode.removeChild(t.calendarContainer);t.altInput&&(t.input.type="text",t.altInput.parentNode&&t.altInput.parentNode.removeChild(t.altInput),delete t.altInput),t.input&&(t.input.type=t.input._type,t.input.classList.remove("flatpickr-input"),t.input.removeAttribute("readonly")),["_showTimeInput","latestSelectedDateObj","_hideNextMonthArrow","_hidePrevMonthArrow","__hideNextMonthArrow","__hidePrevMonthArrow","isMobile","isOpen","selectedDateElem","minDateHasTime","maxDateHasTime","days","daysContainer","_input","_positionElement","innerContainer","rContainer","monthNav","todayDateElem","calendarContainer","weekdayContainer","prevMonthNav","nextMonthNav","monthsDropdownContainer","currentMonthElement","currentYearElement","navigationCurrentMonth","selectedDateElem","config"].forEach(function(Q){try{delete t[Q]}catch{}})}function ee(L){return t.calendarContainer.contains(L)}function _e(L){if(t.isOpen&&!t.config.inline){var N=wn(L),Q=ee(N),de=N===t.input||N===t.altInput||t.element.contains(N)||L.path&&L.path.indexOf&&(~L.path.indexOf(t.input)||~L.path.indexOf(t.altInput)),Te=!de&&!Q&&!ee(L.relatedTarget),Ue=!t.config.ignoredFocusElements.some(function(tt){return tt.contains(N)});Te&&Ue&&(t.config.allowInput&&t.setDate(t._input.value,!1,t.config.altInput?t.config.altFormat:t.config.dateFormat),t.timeContainer!==void 0&&t.minuteElement!==void 0&&t.hourElement!==void 0&&t.input.value!==""&&t.input.value!==void 0&&a(),t.close(),t.config&&t.config.mode==="range"&&t.selectedDates.length===1&&t.clear(!1))}}function fe(L){if(!(!L||t.config.minDate&&Lt.config.maxDate.getFullYear())){var N=L,Q=t.currentYear!==N;t.currentYear=N||t.currentYear,t.config.maxDate&&t.currentYear===t.config.maxDate.getFullYear()?t.currentMonth=Math.min(t.config.maxDate.getMonth(),t.currentMonth):t.config.minDate&&t.currentYear===t.config.minDate.getFullYear()&&(t.currentMonth=Math.max(t.config.minDate.getMonth(),t.currentMonth)),Q&&(t.redraw(),ot("onYearChange"),G())}}function ie(L,N){var Q;N===void 0&&(N=!0);var de=t.parseDate(L,void 0,N);if(t.config.minDate&&de&&Sn(de,t.config.minDate,N!==void 0?N:!t.minDateHasTime)<0||t.config.maxDate&&de&&Sn(de,t.config.maxDate,N!==void 0?N:!t.maxDateHasTime)>0)return!1;if(!t.config.enable&&t.config.disable.length===0)return!0;if(de===void 0)return!1;for(var Te=!!t.config.enable,Ue=(Q=t.config.enable)!==null&&Q!==void 0?Q:t.config.disable,tt=0,Ge=void 0;tt=Ge.from.getTime()&&de.getTime()<=Ge.to.getTime())return Te}return!Te}function ye(L){return t.daysContainer!==void 0?L.className.indexOf("hidden")===-1&&L.className.indexOf("flatpickr-disabled")===-1&&t.daysContainer.contains(L):!1}function Ne(L){var N=L.target===t._input,Q=t._input.value.trimEnd()!==Kt();N&&Q&&!(L.relatedTarget&&ee(L.relatedTarget))&&t.setDate(t._input.value,!0,L.target===t.altInput?t.config.altFormat:t.config.dateFormat)}function Pe(L){var N=wn(L),Q=t.config.wrap?n.contains(N):N===t._input,de=t.config.allowInput,Te=t.isOpen&&(!de||!Q),Ue=t.config.inline&&Q&&!de;if(L.keyCode===13&&Q){if(de)return t.setDate(t._input.value,!0,N===t.altInput?t.config.altFormat:t.config.dateFormat),t.close(),N.blur();t.open()}else if(ee(N)||Te||Ue){var tt=!!t.timeContainer&&t.timeContainer.contains(N);switch(L.keyCode){case 13:tt?(L.preventDefault(),a(),Ve()):yt(L);break;case 27:L.preventDefault(),Ve();break;case 8:case 46:Q&&!t.config.allowInput&&(L.preventDefault(),t.clear());break;case 37:case 39:if(!tt&&!Q){L.preventDefault();var Ge=r();if(t.daysContainer!==void 0&&(de===!1||Ge&&ye(Ge))){var nt=L.keyCode===39?1:-1;L.ctrlKey?(L.stopPropagation(),ve(nt),P(D(1),0)):P(void 0,nt)}}else t.hourElement&&t.hourElement.focus();break;case 38:case 40:L.preventDefault();var Ke=L.keyCode===40?1:-1;t.daysContainer&&N.$i!==void 0||N===t.input||N===t.altInput?L.ctrlKey?(L.stopPropagation(),fe(t.currentYear-Ke),P(D(1),0)):tt||P(void 0,Ke*7):N===t.currentYearElement?fe(t.currentYear-Ke):t.config.enableTime&&(!tt&&t.hourElement&&t.hourElement.focus(),a(L),t._debouncedChange());break;case 9:if(tt){var et=[t.hourElement,t.minuteElement,t.secondElement,t.amPM].concat(t.pluginElements).filter(function(nn){return nn}),gt=et.indexOf(N);if(gt!==-1){var Ft=et[gt+(L.shiftKey?-1:1)];L.preventDefault(),(Ft||t._input).focus()}}else!t.config.noCalendar&&t.daysContainer&&t.daysContainer.contains(N)&&L.shiftKey&&(L.preventDefault(),t._input.focus());break}}if(t.amPM!==void 0&&N===t.amPM)switch(L.key){case t.l10n.amPM[0].charAt(0):case t.l10n.amPM[0].charAt(0).toLowerCase():t.amPM.textContent=t.l10n.amPM[0],u(),zt();break;case t.l10n.amPM[1].charAt(0):case t.l10n.amPM[1].charAt(0).toLowerCase():t.amPM.textContent=t.l10n.amPM[1],u(),zt();break}(Q||ee(N))&&ot("onKeyDown",L)}function ze(L,N){if(N===void 0&&(N="flatpickr-day"),!(t.selectedDates.length!==1||L&&(!L.classList.contains(N)||L.classList.contains("flatpickr-disabled")))){for(var Q=L?L.dateObj.getTime():t.days.firstElementChild.dateObj.getTime(),de=t.parseDate(t.selectedDates[0],void 0,!0).getTime(),Te=Math.min(Q,t.selectedDates[0].getTime()),Ue=Math.max(Q,t.selectedDates[0].getTime()),tt=!1,Ge=0,nt=0,Ke=Te;KeTe&&KeGe)?Ge=Ke:Ke>de&&(!nt||Ke ."+N));et.forEach(function(gt){var Ft=gt.dateObj,nn=Ft.getTime(),Fn=Ge>0&&nn0&&nn>nt;if(Fn){gt.classList.add("notAllowed"),["inRange","startRange","endRange"].forEach(function(Vt){gt.classList.remove(Vt)});return}else if(tt&&!Fn)return;["startRange","inRange","endRange","notAllowed"].forEach(function(Vt){gt.classList.remove(Vt)}),L!==void 0&&(L.classList.add(Q<=t.selectedDates[0].getTime()?"startRange":"endRange"),deQ&&nn===de&>.classList.add("endRange"),nn>=Ge&&(nt===0||nn<=nt)&&d5(nn,de,Q)&>.classList.add("inRange"))})}}function se(){t.isOpen&&!t.config.static&&!t.config.inline&&be()}function re(L,N){if(N===void 0&&(N=t._positionElement),t.isMobile===!0){if(L){L.preventDefault();var Q=wn(L);Q&&Q.blur()}t.mobileInput!==void 0&&(t.mobileInput.focus(),t.mobileInput.click()),ot("onOpen");return}else if(t._input.disabled||t.config.inline)return;var de=t.isOpen;t.isOpen=!0,de||(t.calendarContainer.classList.add("open"),t._input.classList.add("active"),ot("onOpen"),be(N)),t.config.enableTime===!0&&t.config.noCalendar===!0&&t.config.allowInput===!1&&(L===void 0||!t.timeContainer.contains(L.relatedTarget))&&setTimeout(function(){return t.hourElement.select()},50)}function ke(L){return function(N){var Q=t.config["_"+L+"Date"]=t.parseDate(N,t.config.dateFormat),de=t.config["_"+(L==="min"?"max":"min")+"Date"];Q!==void 0&&(t[L==="min"?"minDateHasTime":"maxDateHasTime"]=Q.getHours()>0||Q.getMinutes()>0||Q.getSeconds()>0),t.selectedDates&&(t.selectedDates=t.selectedDates.filter(function(Te){return ie(Te)}),!t.selectedDates.length&&L==="min"&&d(Q),zt()),t.daysContainer&&(ae(),Q!==void 0?t.currentYearElement[L]=Q.getFullYear().toString():t.currentYearElement.removeAttribute(L),t.currentYearElement.disabled=!!de&&Q!==void 0&&de.getFullYear()===Q.getFullYear())}}function He(){var L=["wrap","weekNumbers","allowInput","allowInvalidPreload","clickOpens","time_24hr","enableTime","noCalendar","altInput","shorthandCurrentMonth","inline","static","enableSeconds","disableMobile"],N=ln(ln({},JSON.parse(JSON.stringify(n.dataset||{}))),e),Q={};t.config.parseDate=N.parseDate,t.config.formatDate=N.formatDate,Object.defineProperty(t.config,"enable",{get:function(){return t.config._enable},set:function(et){t.config._enable=qt(et)}}),Object.defineProperty(t.config,"disable",{get:function(){return t.config._disable},set:function(et){t.config._disable=qt(et)}});var de=N.mode==="time";if(!N.dateFormat&&(N.enableTime||de)){var Te=Ut.defaultConfig.dateFormat||zo.dateFormat;Q.dateFormat=N.noCalendar||de?"H:i"+(N.enableSeconds?":S":""):Te+" H:i"+(N.enableSeconds?":S":"")}if(N.altInput&&(N.enableTime||de)&&!N.altFormat){var Ue=Ut.defaultConfig.altFormat||zo.altFormat;Q.altFormat=N.noCalendar||de?"h:i"+(N.enableSeconds?":S K":" K"):Ue+(" h:i"+(N.enableSeconds?":S":"")+" K")}Object.defineProperty(t.config,"minDate",{get:function(){return t.config._minDate},set:ke("min")}),Object.defineProperty(t.config,"maxDate",{get:function(){return t.config._maxDate},set:ke("max")});var tt=function(et){return function(gt){t.config[et==="min"?"_minTime":"_maxTime"]=t.parseDate(gt,"H:i:S")}};Object.defineProperty(t.config,"minTime",{get:function(){return t.config._minTime},set:tt("min")}),Object.defineProperty(t.config,"maxTime",{get:function(){return t.config._maxTime},set:tt("max")}),N.mode==="time"&&(t.config.noCalendar=!0,t.config.enableTime=!0),Object.assign(t.config,Q,N);for(var Ge=0;Ge-1?t.config[Ke]=Ca(nt[Ke]).map(l).concat(t.config[Ke]):typeof N[Ke]=="undefined"&&(t.config[Ke]=nt[Ke])}N.altInputClass||(t.config.altInputClass=qe().className+" "+t.config.altInputClass),ot("onParseConfig")}function qe(){return t.config.wrap?n.querySelector("[data-input]"):n}function Je(){typeof t.config.locale!="object"&&typeof Ut.l10ns[t.config.locale]=="undefined"&&t.config.errorHandler(new Error("flatpickr: invalid locale "+t.config.locale)),t.l10n=ln(ln({},Ut.l10ns.default),typeof t.config.locale=="object"?t.config.locale:t.config.locale!=="default"?Ut.l10ns[t.config.locale]:void 0),lo.D="("+t.l10n.weekdays.shorthand.join("|")+")",lo.l="("+t.l10n.weekdays.longhand.join("|")+")",lo.M="("+t.l10n.months.shorthand.join("|")+")",lo.F="("+t.l10n.months.longhand.join("|")+")",lo.K="("+t.l10n.amPM[0]+"|"+t.l10n.amPM[1]+"|"+t.l10n.amPM[0].toLowerCase()+"|"+t.l10n.amPM[1].toLowerCase()+")";var L=ln(ln({},e),JSON.parse(JSON.stringify(n.dataset||{})));L.time_24hr===void 0&&Ut.defaultConfig.time_24hr===void 0&&(t.config.time_24hr=t.l10n.time_24hr),t.formatDate=E1(t),t.parseDate=mf({config:t.config,l10n:t.l10n})}function be(L){if(typeof t.config.position=="function")return void t.config.position(t,L);if(t.calendarContainer!==void 0){ot("onPreCalendarPosition");var N=L||t._positionElement,Q=Array.prototype.reduce.call(t.calendarContainer.children,function(xe,We){return xe+We.offsetHeight},0),de=t.calendarContainer.offsetWidth,Te=t.config.position.split(" "),Ue=Te[0],tt=Te.length>1?Te[1]:null,Ge=N.getBoundingClientRect(),nt=window.innerHeight-Ge.bottom,Ke=Ue==="above"||Ue!=="below"&&ntQ,et=window.pageYOffset+Ge.top+(Ke?-Q-2:N.offsetHeight+2);if(un(t.calendarContainer,"arrowTop",!Ke),un(t.calendarContainer,"arrowBottom",Ke),!t.config.inline){var gt=window.pageXOffset+Ge.left,Ft=!1,nn=!1;tt==="center"?(gt-=(de-Ge.width)/2,Ft=!0):tt==="right"&&(gt-=de-Ge.width,nn=!0),un(t.calendarContainer,"arrowLeft",!Ft&&!nn),un(t.calendarContainer,"arrowCenter",Ft),un(t.calendarContainer,"arrowRight",nn);var Fn=window.document.body.offsetWidth-(window.pageXOffset+Ge.right),Vt=gt+de>window.document.body.offsetWidth,wo=Fn+de>window.document.body.offsetWidth;if(un(t.calendarContainer,"rightMost",Vt),!t.config.static)if(t.calendarContainer.style.top=et+"px",!Vt)t.calendarContainer.style.left=gt+"px",t.calendarContainer.style.right="auto";else if(!wo)t.calendarContainer.style.left="auto",t.calendarContainer.style.right=Fn+"px";else{var So=Oe();if(So===void 0)return;var Gi=window.document.body.offsetWidth,nl=Math.max(0,Gi/2-de/2),Co=".flatpickr-calendar.centerMost:before",il=".flatpickr-calendar.centerMost:after",Ki=So.cssRules.length,ol="{left:"+Ge.left+"px;right:auto;}";un(t.calendarContainer,"rightMost",!1),un(t.calendarContainer,"centerMost",!0),So.insertRule(Co+","+il+ol,Ki),t.calendarContainer.style.left=nl+"px",t.calendarContainer.style.right="auto"}}}}function Oe(){for(var L=null,N=0;Nt.currentMonth+t.config.showMonths-1)&&t.config.mode!=="range";if(t.selectedDateElem=de,t.config.mode==="single")t.selectedDates=[Te];else if(t.config.mode==="multiple"){var tt=ue(Te);tt?t.selectedDates.splice(parseInt(tt),1):t.selectedDates.push(Te)}else t.config.mode==="range"&&(t.selectedDates.length===2&&t.clear(!1,!1),t.latestSelectedDateObj=Te,t.selectedDates.push(Te),Sn(Te,t.selectedDates[0],!0)!==0&&t.selectedDates.sort(function(et,gt){return et.getTime()-gt.getTime()}));if(u(),Ue){var Ge=t.currentYear!==Te.getFullYear();t.currentYear=Te.getFullYear(),t.currentMonth=Te.getMonth(),Ge&&(ot("onYearChange"),G()),ot("onMonthChange")}if(Ze(),R(),zt(),!Ue&&t.config.mode!=="range"&&t.config.showMonths===1?O(de):t.selectedDateElem!==void 0&&t.hourElement===void 0&&t.selectedDateElem&&t.selectedDateElem.focus(),t.hourElement!==void 0&&t.hourElement!==void 0&&t.hourElement.focus(),t.config.closeOnSelect){var nt=t.config.mode==="single"&&!t.config.enableTime,Ke=t.config.mode==="range"&&t.selectedDates.length===2&&!t.config.enableTime;(nt||Ke)&&Ve()}_()}}var it={locale:[Je,te],showMonths:[z,s,W],minDate:[S],maxDate:[S],positionElement:[me],clickOpens:[function(){t.config.clickOpens===!0?(v(t._input,"focus",t.open),v(t._input,"click",t.open)):(t._input.removeEventListener("focus",t.open),t._input.removeEventListener("click",t.open))}]};function bt(L,N){if(L!==null&&typeof L=="object"){Object.assign(t.config,L);for(var Q in L)it[Q]!==void 0&&it[Q].forEach(function(de){return de()})}else t.config[L]=N,it[L]!==void 0?it[L].forEach(function(de){return de()}):Sa.indexOf(L)>-1&&(t.config[L]=Ca(N));t.redraw(),zt(!0)}function at(L,N){var Q=[];if(L instanceof Array)Q=L.map(function(de){return t.parseDate(de,N)});else if(L instanceof Date||typeof L=="number")Q=[t.parseDate(L,N)];else if(typeof L=="string")switch(t.config.mode){case"single":case"time":Q=[t.parseDate(L,N)];break;case"multiple":Q=L.split(t.config.conjunction).map(function(de){return t.parseDate(de,N)});break;case"range":Q=L.split(t.l10n.rangeSeparator).map(function(de){return t.parseDate(de,N)});break}else t.config.errorHandler(new Error("Invalid date supplied: "+JSON.stringify(L)));t.selectedDates=t.config.allowInvalidPreload?Q:Q.filter(function(de){return de instanceof Date&&ie(de,!1)}),t.config.mode==="range"&&t.selectedDates.sort(function(de,Te){return de.getTime()-Te.getTime()})}function vt(L,N,Q){if(N===void 0&&(N=!1),Q===void 0&&(Q=t.config.dateFormat),L!==0&&!L||L instanceof Array&&L.length===0)return t.clear(N);at(L,Q),t.latestSelectedDateObj=t.selectedDates[t.selectedDates.length-1],t.redraw(),S(void 0,N),d(),t.selectedDates.length===0&&t.clear(!1),zt(N),N&&ot("onChange")}function qt(L){return L.slice().map(function(N){return typeof N=="string"||typeof N=="number"||N instanceof Date?t.parseDate(N,void 0,!0):N&&typeof N=="object"&&N.from&&N.to?{from:t.parseDate(N.from,void 0),to:t.parseDate(N.to,void 0)}:N}).filter(function(N){return N})}function Mt(){t.selectedDates=[],t.now=t.parseDate(t.config.now)||new Date;var L=t.config.defaultDate||((t.input.nodeName==="INPUT"||t.input.nodeName==="TEXTAREA")&&t.input.placeholder&&t.input.value===t.input.placeholder?null:t.input.value);L&&at(L,t.config.dateFormat),t._initialDate=t.selectedDates.length>0?t.selectedDates[0]:t.config.minDate&&t.config.minDate.getTime()>t.now.getTime()?t.config.minDate:t.config.maxDate&&t.config.maxDate.getTime()0&&(t.latestSelectedDateObj=t.selectedDates[0]),t.config.minTime!==void 0&&(t.config.minTime=t.parseDate(t.config.minTime,"H:i")),t.config.maxTime!==void 0&&(t.config.maxTime=t.parseDate(t.config.maxTime,"H:i")),t.minDateHasTime=!!t.config.minDate&&(t.config.minDate.getHours()>0||t.config.minDate.getMinutes()>0||t.config.minDate.getSeconds()>0),t.maxDateHasTime=!!t.config.maxDate&&(t.config.maxDate.getHours()>0||t.config.maxDate.getMinutes()>0||t.config.maxDate.getSeconds()>0)}function $t(){if(t.input=qe(),!t.input){t.config.errorHandler(new Error("Invalid input element specified"));return}t.input._type=t.input.type,t.input.type="text",t.input.classList.add("flatpickr-input"),t._input=t.input,t.config.altInput&&(t.altInput=wt(t.input.nodeName,t.config.altInputClass),t._input=t.altInput,t.altInput.placeholder=t.input.placeholder,t.altInput.disabled=t.input.disabled,t.altInput.required=t.input.required,t.altInput.tabIndex=t.input.tabIndex,t.altInput.type="text",t.input.setAttribute("type","hidden"),!t.config.static&&t.input.parentNode&&t.input.parentNode.insertBefore(t.altInput,t.input.nextSibling)),t.config.allowInput||t._input.setAttribute("readonly","readonly"),me()}function me(){t._positionElement=t.config.positionElement||t._input}function Ce(){var L=t.config.enableTime?t.config.noCalendar?"time":"datetime-local":"date";t.mobileInput=wt("input",t.input.className+" flatpickr-mobile"),t.mobileInput.tabIndex=1,t.mobileInput.type=L,t.mobileInput.disabled=t.input.disabled,t.mobileInput.required=t.input.required,t.mobileInput.placeholder=t.input.placeholder,t.mobileFormatStr=L==="datetime-local"?"Y-m-d\\TH:i:S":L==="date"?"Y-m-d":"H:i:S",t.selectedDates.length>0&&(t.mobileInput.defaultValue=t.mobileInput.value=t.formatDate(t.selectedDates[0],t.mobileFormatStr)),t.config.minDate&&(t.mobileInput.min=t.formatDate(t.config.minDate,"Y-m-d")),t.config.maxDate&&(t.mobileInput.max=t.formatDate(t.config.maxDate,"Y-m-d")),t.input.getAttribute("step")&&(t.mobileInput.step=String(t.input.getAttribute("step"))),t.input.type="hidden",t.altInput!==void 0&&(t.altInput.type="hidden");try{t.input.parentNode&&t.input.parentNode.insertBefore(t.mobileInput,t.input.nextSibling)}catch{}v(t.mobileInput,"change",function(N){t.setDate(wn(N).value,!1,t.mobileFormatStr),ot("onChange"),ot("onClose")})}function Ye(L){if(t.isOpen===!0)return t.close();t.open(L)}function ot(L,N){if(t.config!==void 0){var Q=t.config[L];if(Q!==void 0&&Q.length>0)for(var de=0;Q[de]&&de=0&&Sn(L,t.selectedDates[1])<=0}function Ze(){t.config.noCalendar||t.isMobile||!t.monthNav||(t.yearElements.forEach(function(L,N){var Q=new Date(t.currentYear,t.currentMonth,1);Q.setMonth(t.currentMonth+N),t.config.showMonths>1||t.config.monthSelectorType==="static"?t.monthElements[N].textContent=_s(Q.getMonth(),t.config.shorthandCurrentMonth,t.l10n)+" ":t.monthsDropdownContainer.value=Q.getMonth().toString(),L.value=Q.getFullYear().toString()}),t._hidePrevMonthArrow=t.config.minDate!==void 0&&(t.currentYear===t.config.minDate.getFullYear()?t.currentMonth<=t.config.minDate.getMonth():t.currentYeart.config.maxDate.getMonth():t.currentYear>t.config.maxDate.getFullYear()))}function Kt(L){var N=L||(t.config.altInput?t.config.altFormat:t.config.dateFormat);return t.selectedDates.map(function(Q){return t.formatDate(Q,N)}).filter(function(Q,de,Te){return t.config.mode!=="range"||t.config.enableTime||Te.indexOf(Q)===de}).join(t.config.mode!=="range"?t.config.conjunction:t.l10n.rangeSeparator)}function zt(L){L===void 0&&(L=!0),t.mobileInput!==void 0&&t.mobileFormatStr&&(t.mobileInput.value=t.latestSelectedDateObj!==void 0?t.formatDate(t.latestSelectedDateObj,t.mobileFormatStr):""),t.input.value=Kt(t.config.dateFormat),t.altInput!==void 0&&(t.altInput.value=Kt(t.config.altFormat)),L!==!1&&ot("onValueUpdate")}function ni(L){var N=wn(L),Q=t.prevMonthNav.contains(N),de=t.nextMonthNav.contains(N);Q||de?ve(Q?-1:1):t.yearElements.indexOf(N)>=0?N.select():N.classList.contains("arrowUp")?t.changeYear(t.currentYear+1):N.classList.contains("arrowDown")&&t.changeYear(t.currentYear-1)}function pe(L){L.preventDefault();var N=L.type==="keydown",Q=wn(L),de=Q;t.amPM!==void 0&&Q===t.amPM&&(t.amPM.textContent=t.l10n.amPM[Nn(t.amPM.textContent===t.l10n.amPM[0])]);var Te=parseFloat(de.getAttribute("min")),Ue=parseFloat(de.getAttribute("max")),tt=parseFloat(de.getAttribute("step")),Ge=parseInt(de.value,10),nt=L.delta||(N?L.which===38?1:-1:0),Ke=Ge+tt*nt;if(typeof de.value!="undefined"&&de.value.length===2){var et=de===t.hourElement,gt=de===t.minuteElement;KeUe&&(Ke=de===t.hourElement?Ke-Ue-Nn(!t.amPM):Te,gt&&x(void 0,1,t.hourElement)),t.amPM&&et&&(tt===1?Ke+Ge===23:Math.abs(Ke-Ge)>tt)&&(t.amPM.textContent=t.l10n.amPM[Nn(t.amPM.textContent===t.l10n.amPM[0])]),de.value=gn(Ke)}}return o(),t}function Ho(n,e){for(var t=Array.prototype.slice.call(n).filter(function(l){return l instanceof HTMLElement}),i=[],o=0;o{const x=c||b,M=y(d);return M.onReady.push(()=>{t(8,h=!0)}),t(3,v=Ut(x,Object.assign(M,c?{wrap:!0}:{}))),()=>{v.destroy()}});const _=yn();function y(x={}){x=Object.assign({},x);for(const M of s){const A=(O,D,E)=>{_(v5(M),[O,D,E])};M in x?(Array.isArray(x[M])||(x[M]=[x[M]]),x[M].push(A)):x[M]=[A]}return x.onChange&&!x.onChange.includes(S)&&x.onChange.push(S),x}function S(x,M,A){var D,E;const O=(E=(D=A==null?void 0:A.config)==null?void 0:D.mode)!=null?E:"single";t(2,a=O==="single"?x[0]:x),t(4,f=M)}function C(x){he[x?"unshift":"push"](()=>{b=x,t(0,b)})}return n.$$set=x=>{e=ut(ut({},e),ui(x)),t(1,o=Wt(e,i)),"value"in x&&t(2,a=x.value),"formattedValue"in x&&t(4,f=x.formattedValue),"element"in x&&t(5,c=x.element),"dateFormat"in x&&t(6,u=x.dateFormat),"options"in x&&t(7,d=x.options),"input"in x&&t(0,b=x.input),"flatpickr"in x&&t(3,v=x.flatpickr),"$$scope"in x&&t(9,l=x.$$scope)},n.$$.update=()=>{if(n.$$.dirty&332&&v&&h&&v.setDate(a,!1,u),n.$$.dirty&392&&v&&h)for(const[x,M]of Object.entries(y(d)))v.set(x,M)},[b,o,a,v,f,c,u,d,h,l,r,C]}class pc extends Ie{constructor(e){super(),Le(this,e,y5,_5,Ee,{value:2,formattedValue:4,element:5,dateFormat:6,options:7,input:0,flatpickr:3})}}function k5(n){let e,t,i,o,r,l,s;function a(c){n[2](c)}let f={id:n[4],options:B.defaultFlatpickrOptions(),value:n[0].min};return n[0].min!==void 0&&(f.formattedValue=n[0].min),r=new pc({props:f}),he.push(()=>Fe(r,"formattedValue",a)),{c(){e=g("label"),t=j("Min date (UTC)"),o=$(),V(r.$$.fragment),p(e,"for",i=n[4])},m(c,u){w(c,e,u),m(e,t),w(c,o,u),H(r,c,u),s=!0},p(c,u){(!s||u&16&&i!==(i=c[4]))&&p(e,"for",i);const d={};u&16&&(d.id=c[4]),u&1&&(d.value=c[0].min),!l&&u&1&&(l=!0,d.formattedValue=c[0].min,Re(()=>l=!1)),r.$set(d)},i(c){s||(T(r.$$.fragment,c),s=!0)},o(c){F(r.$$.fragment,c),s=!1},d(c){c&&k(e),c&&k(o),q(r,c)}}}function w5(n){let e,t,i,o,r,l,s;function a(c){n[3](c)}let f={id:n[4],options:B.defaultFlatpickrOptions(),value:n[0].max};return n[0].max!==void 0&&(f.formattedValue=n[0].max),r=new pc({props:f}),he.push(()=>Fe(r,"formattedValue",a)),{c(){e=g("label"),t=j("Max date (UTC)"),o=$(),V(r.$$.fragment),p(e,"for",i=n[4])},m(c,u){w(c,e,u),m(e,t),w(c,o,u),H(r,c,u),s=!0},p(c,u){(!s||u&16&&i!==(i=c[4]))&&p(e,"for",i);const d={};u&16&&(d.id=c[4]),u&1&&(d.value=c[0].max),!l&&u&1&&(l=!0,d.formattedValue=c[0].max,Re(()=>l=!1)),r.$set(d)},i(c){s||(T(r.$$.fragment,c),s=!0)},o(c){F(r.$$.fragment,c),s=!1},d(c){c&&k(e),c&&k(o),q(r,c)}}}function S5(n){let e,t,i,o,r,l,s;return i=new je({props:{class:"form-field",name:"schema."+n[1]+".options.min",$$slots:{default:[k5,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field",name:"schema."+n[1]+".options.max",$$slots:{default:[w5,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),p(t,"class","col-sm-6"),p(r,"class","col-sm-6"),p(e,"class","grid")},m(a,f){w(a,e,f),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),s=!0},p(a,[f]){const c={};f&2&&(c.name="schema."+a[1]+".options.min"),f&49&&(c.$$scope={dirty:f,ctx:a}),i.$set(c);const u={};f&2&&(u.name="schema."+a[1]+".options.max"),f&49&&(u.$$scope={dirty:f,ctx:a}),l.$set(u)},i(a){s||(T(i.$$.fragment,a),T(l.$$.fragment,a),s=!0)},o(a){F(i.$$.fragment,a),F(l.$$.fragment,a),s=!1},d(a){a&&k(e),q(i),q(l)}}}function C5(n,e,t){let{key:i=""}=e,{options:o={}}=e;function r(s){n.$$.not_equal(o.min,s)&&(o.min=s,t(0,o))}function l(s){n.$$.not_equal(o.max,s)&&(o.max=s,t(0,o))}return n.$$set=s=>{"key"in s&&t(1,i=s.key),"options"in s&&t(0,o=s.options)},[o,i,r,l]}class x5 extends Ie{constructor(e){super(),Le(this,e,C5,S5,Ee,{key:1,options:0})}}function M5(n){let e,t,i,o,r,l,s,a,f;function c(d){n[2](d)}let u={id:n[4],placeholder:"eg. optionA, optionB",required:!0};return n[0].values!==void 0&&(u.value=n[0].values),r=new ko({props:u}),he.push(()=>Fe(r,"value",c)),{c(){e=g("label"),t=j("Choices"),o=$(),V(r.$$.fragment),s=$(),a=g("div"),a.textContent="Use comma as separator.",p(e,"for",i=n[4]),p(a,"class","help-block")},m(d,h){w(d,e,h),m(e,t),w(d,o,h),H(r,d,h),w(d,s,h),w(d,a,h),f=!0},p(d,h){(!f||h&16&&i!==(i=d[4]))&&p(e,"for",i);const b={};h&16&&(b.id=d[4]),!l&&h&1&&(l=!0,b.value=d[0].values,Re(()=>l=!1)),r.$set(b)},i(d){f||(T(r.$$.fragment,d),f=!0)},o(d){F(r.$$.fragment,d),f=!1},d(d){d&&k(e),d&&k(o),q(r,d),d&&k(s),d&&k(a)}}}function $5(n){let e,t,i,o,r,l,s,a;return{c(){e=g("label"),t=j("Max select"),o=$(),r=g("input"),p(e,"for",i=n[4]),p(r,"type","number"),p(r,"id",l=n[4]),p(r,"step","1"),p(r,"min","1"),r.required=!0},m(f,c){w(f,e,c),m(e,t),w(f,o,c),w(f,r,c),Me(r,n[0].maxSelect),s||(a=X(r,"input",n[3]),s=!0)},p(f,c){c&16&&i!==(i=f[4])&&p(e,"for",i),c&16&&l!==(l=f[4])&&p(r,"id",l),c&1&&At(r.value)!==f[0].maxSelect&&Me(r,f[0].maxSelect)},d(f){f&&k(e),f&&k(o),f&&k(r),s=!1,a()}}}function A5(n){let e,t,i,o,r,l,s;return i=new je({props:{class:"form-field required",name:"schema."+n[1]+".options.values",$$slots:{default:[M5,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSelect",$$slots:{default:[$5,({uniqueId:a})=>({4:a}),({uniqueId:a})=>a?16:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),p(t,"class","col-sm-9"),p(r,"class","col-sm-3"),p(e,"class","grid")},m(a,f){w(a,e,f),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),s=!0},p(a,[f]){const c={};f&2&&(c.name="schema."+a[1]+".options.values"),f&49&&(c.$$scope={dirty:f,ctx:a}),i.$set(c);const u={};f&2&&(u.name="schema."+a[1]+".options.maxSelect"),f&49&&(u.$$scope={dirty:f,ctx:a}),l.$set(u)},i(a){s||(T(i.$$.fragment,a),T(l.$$.fragment,a),s=!0)},o(a){F(i.$$.fragment,a),F(l.$$.fragment,a),s=!1},d(a){a&&k(e),q(i),q(l)}}}function D5(n,e,t){let{key:i=""}=e,{options:o={}}=e;function r(s){n.$$.not_equal(o.values,s)&&(o.values=s,t(0,o))}function l(){o.maxSelect=At(this.value),t(0,o)}return n.$$set=s=>{"key"in s&&t(1,i=s.key),"options"in s&&t(0,o=s.options)},n.$$.update=()=>{n.$$.dirty&1&&B.isEmpty(o)&&t(0,o={maxSelect:1,values:[]})},[o,i,r,l]}class O5 extends Ie{constructor(e){super(),Le(this,e,D5,A5,Ee,{key:1,options:0})}}function T5(n,e,t){return["",{}]}class E5 extends Ie{constructor(e){super(),Le(this,e,T5,null,Ee,{key:0,options:1})}get key(){return this.$$.ctx[0]}get options(){return this.$$.ctx[1]}}function P5(n){let e,t,i,o,r,l,s,a;return{c(){e=g("label"),t=j("Max file size (bytes)"),o=$(),r=g("input"),p(e,"for",i=n[9]),p(r,"type","number"),p(r,"id",l=n[9]),p(r,"step","1"),p(r,"min","0")},m(f,c){w(f,e,c),m(e,t),w(f,o,c),w(f,r,c),Me(r,n[0].maxSize),s||(a=X(r,"input",n[2]),s=!0)},p(f,c){c&512&&i!==(i=f[9])&&p(e,"for",i),c&512&&l!==(l=f[9])&&p(r,"id",l),c&1&&At(r.value)!==f[0].maxSize&&Me(r,f[0].maxSize)},d(f){f&&k(e),f&&k(o),f&&k(r),s=!1,a()}}}function F5(n){let e,t,i,o,r,l,s,a;return{c(){e=g("label"),t=j("Max files"),o=$(),r=g("input"),p(e,"for",i=n[9]),p(r,"type","number"),p(r,"id",l=n[9]),p(r,"step","1"),p(r,"min",""),r.required=!0},m(f,c){w(f,e,c),m(e,t),w(f,o,c),w(f,r,c),Me(r,n[0].maxSelect),s||(a=X(r,"input",n[3]),s=!0)},p(f,c){c&512&&i!==(i=f[9])&&p(e,"for",i),c&512&&l!==(l=f[9])&&p(r,"id",l),c&1&&At(r.value)!==f[0].maxSelect&&Me(r,f[0].maxSelect)},d(f){f&&k(e),f&&k(o),f&&k(r),s=!1,a()}}}function L5(n){let e,t,i,o,r,l,s;return{c(){e=g("div"),e.innerHTML='Images (jpg, png, svg, gif)',t=$(),i=g("div"),i.innerHTML='Documents (pdf, doc/docx, xls/xlsx)',o=$(),r=g("div"),r.innerHTML='Archives (zip, 7zip, rar)',p(e,"tabindex","0"),p(e,"class","dropdown-item closable"),p(i,"tabindex","0"),p(i,"class","dropdown-item closable"),p(r,"tabindex","0"),p(r,"class","dropdown-item closable")},m(a,f){w(a,e,f),w(a,t,f),w(a,i,f),w(a,o,f),w(a,r,f),l||(s=[X(e,"click",n[5]),X(i,"click",n[6]),X(r,"click",n[7])],l=!0)},p:le,d(a){a&&k(e),a&&k(t),a&&k(i),a&&k(o),a&&k(r),l=!1,rt(s)}}}function I5(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y,S;function C(M){n[4](M)}let x={id:n[9],placeholder:"eg. image/png, application/pdf..."};return n[0].mimeTypes!==void 0&&(x.value=n[0].mimeTypes),s=new ko({props:x}),he.push(()=>Fe(s,"value",C)),v=new vo({props:{class:"dropdown dropdown-sm dropdown-nowrap",$$slots:{default:[L5]},$$scope:{ctx:n}}}),{c(){e=g("label"),t=g("span"),t.textContent="Mime types",i=$(),o=g("i"),l=$(),V(s.$$.fragment),f=$(),c=g("div"),u=j(`Use comma as separator. + `),d=g("span"),h=g("span"),h.textContent="Choose presets",b=$(),V(v.$$.fragment),p(t,"class","txt"),p(o,"class","ri-information-line link-hint"),p(e,"for",r=n[9]),p(h,"class","txt link-primary"),p(d,"class","inline-flex"),p(c,"class","help-block")},m(M,A){w(M,e,A),m(e,t),m(e,i),m(e,o),w(M,l,A),H(s,M,A),w(M,f,A),w(M,c,A),m(c,u),m(c,d),m(d,h),m(d,b),H(v,d,null),_=!0,y||(S=Xe(St.call(null,o,{text:`Allow uploading files ONLY with the listed mime types. + Leave empty for no restriction.`,position:"top"})),y=!0)},p(M,A){(!_||A&512&&r!==(r=M[9]))&&p(e,"for",r);const O={};A&512&&(O.id=M[9]),!a&&A&1&&(a=!0,O.value=M[0].mimeTypes,Re(()=>a=!1)),s.$set(O);const D={};A&1025&&(D.$$scope={dirty:A,ctx:M}),v.$set(D)},i(M){_||(T(s.$$.fragment,M),T(v.$$.fragment,M),_=!0)},o(M){F(s.$$.fragment,M),F(v.$$.fragment,M),_=!1},d(M){M&&k(e),M&&k(l),q(s,M),M&&k(f),M&&k(c),q(v),y=!1,S()}}}function R5(n){let e,t,i,o,r,l,s,a,f,c,u,d,h;function b(_){n[8](_)}let v={id:n[9],placeholder:"eg. 50x50, 480x720"};return n[0].thumbs!==void 0&&(v.value=n[0].thumbs),s=new ko({props:v}),he.push(()=>Fe(s,"value",b)),{c(){e=g("label"),t=g("span"),t.textContent="Thumb sizes",i=$(),o=g("i"),l=$(),V(s.$$.fragment),f=$(),c=g("div"),c.textContent="Use comma as separator.",p(t,"class","txt"),p(o,"class","ri-information-line link-hint"),p(e,"for",r=n[9]),p(c,"class","help-block")},m(_,y){w(_,e,y),m(e,t),m(e,i),m(e,o),w(_,l,y),H(s,_,y),w(_,f,y),w(_,c,y),u=!0,d||(h=Xe(St.call(null,o,{text:"List of thumb sizes for image files. The thumbs will be generated lazily on first access.",position:"top"})),d=!0)},p(_,y){(!u||y&512&&r!==(r=_[9]))&&p(e,"for",r);const S={};y&512&&(S.id=_[9]),!a&&y&1&&(a=!0,S.value=_[0].thumbs,Re(()=>a=!1)),s.$set(S)},i(_){u||(T(s.$$.fragment,_),u=!0)},o(_){F(s.$$.fragment,_),u=!1},d(_){_&&k(e),_&&k(l),q(s,_),_&&k(f),_&&k(c),d=!1,h()}}}function N5(n){let e,t,i,o,r,l,s,a,f,c,u,d,h;return i=new je({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSize",$$slots:{default:[P5,({uniqueId:b})=>({9:b}),({uniqueId:b})=>b?512:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSelect",$$slots:{default:[F5,({uniqueId:b})=>({9:b}),({uniqueId:b})=>b?512:0]},$$scope:{ctx:n}}}),f=new je({props:{class:"form-field",name:"schema."+n[1]+".options.mimeTypes",$$slots:{default:[I5,({uniqueId:b})=>({9:b}),({uniqueId:b})=>b?512:0]},$$scope:{ctx:n}}}),d=new je({props:{class:"form-field",name:"schema."+n[1]+".options.thumbs",$$slots:{default:[R5,({uniqueId:b})=>({9:b}),({uniqueId:b})=>b?512:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),s=$(),a=g("div"),V(f.$$.fragment),c=$(),u=g("div"),V(d.$$.fragment),p(t,"class","col-sm-6"),p(r,"class","col-sm-6"),p(a,"class","col-sm-12"),p(u,"class","col-sm-12"),p(e,"class","grid")},m(b,v){w(b,e,v),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),m(e,s),m(e,a),H(f,a,null),m(e,c),m(e,u),H(d,u,null),h=!0},p(b,[v]){const _={};v&2&&(_.name="schema."+b[1]+".options.maxSize"),v&1537&&(_.$$scope={dirty:v,ctx:b}),i.$set(_);const y={};v&2&&(y.name="schema."+b[1]+".options.maxSelect"),v&1537&&(y.$$scope={dirty:v,ctx:b}),l.$set(y);const S={};v&2&&(S.name="schema."+b[1]+".options.mimeTypes"),v&1537&&(S.$$scope={dirty:v,ctx:b}),f.$set(S);const C={};v&2&&(C.name="schema."+b[1]+".options.thumbs"),v&1537&&(C.$$scope={dirty:v,ctx:b}),d.$set(C)},i(b){h||(T(i.$$.fragment,b),T(l.$$.fragment,b),T(f.$$.fragment,b),T(d.$$.fragment,b),h=!0)},o(b){F(i.$$.fragment,b),F(l.$$.fragment,b),F(f.$$.fragment,b),F(d.$$.fragment,b),h=!1},d(b){b&&k(e),q(i),q(l),q(f),q(d)}}}function j5(n,e,t){let{key:i=""}=e,{options:o={}}=e;function r(){o.maxSize=At(this.value),t(0,o)}function l(){o.maxSelect=At(this.value),t(0,o)}function s(d){n.$$.not_equal(o.mimeTypes,d)&&(o.mimeTypes=d,t(0,o))}const a=()=>{t(0,o.mimeTypes=["image/jpg","image/jpeg","image/png","image/svg+xml","image/gif"],o)},f=()=>{t(0,o.mimeTypes=["application/pdf","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],o)},c=()=>{t(0,o.mimeTypes=["application/zip","application/x-7z-compressed","application/x-rar-compressed"],o)};function u(d){n.$$.not_equal(o.thumbs,d)&&(o.thumbs=d,t(0,o))}return n.$$set=d=>{"key"in d&&t(1,i=d.key),"options"in d&&t(0,o=d.options)},n.$$.update=()=>{n.$$.dirty&1&&B.isEmpty(o)&&t(0,o={maxSelect:1,maxSize:5242880,thumbs:[],mimeTypes:[]})},[o,i,r,l,s,a,f,c,u]}class z5 extends Ie{constructor(e){super(),Le(this,e,j5,N5,Ee,{key:1,options:0})}}function H5(n){let e,t,i,o,r,l,s;function a(c){n[5](c)}let f={searchable:n[3].length>5,selectPlaceholder:n[2]?"Loading...":"Select collection",noOptionsText:"No collections found",selectionKey:"id",items:n[3]};return n[0].collectionId!==void 0&&(f.keyOfSelected=n[0].collectionId),r=new yo({props:f}),he.push(()=>Fe(r,"keyOfSelected",a)),{c(){e=g("label"),t=j("Collection"),o=$(),V(r.$$.fragment),p(e,"for",i=n[9])},m(c,u){w(c,e,u),m(e,t),w(c,o,u),H(r,c,u),s=!0},p(c,u){(!s||u&512&&i!==(i=c[9]))&&p(e,"for",i);const d={};u&8&&(d.searchable=c[3].length>5),u&4&&(d.selectPlaceholder=c[2]?"Loading...":"Select collection"),u&8&&(d.items=c[3]),!l&&u&1&&(l=!0,d.keyOfSelected=c[0].collectionId,Re(()=>l=!1)),r.$set(d)},i(c){s||(T(r.$$.fragment,c),s=!0)},o(c){F(r.$$.fragment,c),s=!1},d(c){c&&k(e),c&&k(o),q(r,c)}}}function q5(n){let e,t,i,o,r,l,s,a;return{c(){e=g("label"),t=j("Max select"),o=$(),r=g("input"),p(e,"for",i=n[9]),p(r,"type","number"),p(r,"id",l=n[9]),p(r,"step","1"),p(r,"min","1"),r.required=!0},m(f,c){w(f,e,c),m(e,t),w(f,o,c),w(f,r,c),Me(r,n[0].maxSelect),s||(a=X(r,"input",n[6]),s=!0)},p(f,c){c&512&&i!==(i=f[9])&&p(e,"for",i),c&512&&l!==(l=f[9])&&p(r,"id",l),c&1&&At(r.value)!==f[0].maxSelect&&Me(r,f[0].maxSelect)},d(f){f&&k(e),f&&k(o),f&&k(r),s=!1,a()}}}function V5(n){let e,t,i,o,r,l,s;function a(c){n[7](c)}let f={id:n[9],items:n[4]};return n[0].cascadeDelete!==void 0&&(f.keyOfSelected=n[0].cascadeDelete),r=new yo({props:f}),he.push(()=>Fe(r,"keyOfSelected",a)),{c(){e=g("label"),t=j("Delete record on relation delete"),o=$(),V(r.$$.fragment),p(e,"for",i=n[9])},m(c,u){w(c,e,u),m(e,t),w(c,o,u),H(r,c,u),s=!0},p(c,u){(!s||u&512&&i!==(i=c[9]))&&p(e,"for",i);const d={};u&512&&(d.id=c[9]),!l&&u&1&&(l=!0,d.keyOfSelected=c[0].cascadeDelete,Re(()=>l=!1)),r.$set(d)},i(c){s||(T(r.$$.fragment,c),s=!0)},o(c){F(r.$$.fragment,c),s=!1},d(c){c&&k(e),c&&k(o),q(r,c)}}}function B5(n){let e,t,i,o,r,l,s,a,f,c;return i=new je({props:{class:"form-field required",name:"schema."+n[1]+".options.collectionId",$$slots:{default:[H5,({uniqueId:u})=>({9:u}),({uniqueId:u})=>u?512:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSelect",$$slots:{default:[q5,({uniqueId:u})=>({9:u}),({uniqueId:u})=>u?512:0]},$$scope:{ctx:n}}}),f=new je({props:{class:"form-field",name:"schema."+n[1]+".options.cascadeDelete",$$slots:{default:[V5,({uniqueId:u})=>({9:u}),({uniqueId:u})=>u?512:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),s=$(),a=g("div"),V(f.$$.fragment),p(t,"class","col-sm-9"),p(r,"class","col-sm-3"),p(a,"class","col-sm-12"),p(e,"class","grid")},m(u,d){w(u,e,d),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),m(e,s),m(e,a),H(f,a,null),c=!0},p(u,[d]){const h={};d&2&&(h.name="schema."+u[1]+".options.collectionId"),d&1549&&(h.$$scope={dirty:d,ctx:u}),i.$set(h);const b={};d&2&&(b.name="schema."+u[1]+".options.maxSelect"),d&1537&&(b.$$scope={dirty:d,ctx:u}),l.$set(b);const v={};d&2&&(v.name="schema."+u[1]+".options.cascadeDelete"),d&1537&&(v.$$scope={dirty:d,ctx:u}),f.$set(v)},i(u){c||(T(i.$$.fragment,u),T(l.$$.fragment,u),T(f.$$.fragment,u),c=!0)},o(u){F(i.$$.fragment,u),F(l.$$.fragment,u),F(f.$$.fragment,u),c=!1},d(u){u&&k(e),q(i),q(l),q(f)}}}function U5(n,e,t){let{key:i=""}=e,{options:o={}}=e;const r=[{label:"False",value:!1},{label:"True",value:!0}];let l=!1,s=[];a();function a(){t(2,l=!0),Se.Collections.getFullList(200,{sort:"-created"}).then(d=>{t(3,s=d)}).catch(d=>{Se.errorResponseHandler(d)}).finally(()=>{t(2,l=!1)})}function f(d){n.$$.not_equal(o.collectionId,d)&&(o.collectionId=d,t(0,o))}function c(){o.maxSelect=At(this.value),t(0,o)}function u(d){n.$$.not_equal(o.cascadeDelete,d)&&(o.cascadeDelete=d,t(0,o))}return n.$$set=d=>{"key"in d&&t(1,i=d.key),"options"in d&&t(0,o=d.options)},n.$$.update=()=>{n.$$.dirty&1&&B.isEmpty(o)&&t(0,o={maxSelect:1,collectionId:null,cascadeDelete:!1})},[o,i,l,s,r,f,c,u]}class W5 extends Ie{constructor(e){super(),Le(this,e,U5,B5,Ee,{key:1,options:0})}}function Y5(n){let e,t,i,o,r,l,s,a;return{c(){e=g("label"),t=j("Max select"),o=$(),r=g("input"),p(e,"for",i=n[5]),p(r,"type","number"),p(r,"id",l=n[5]),p(r,"step","1"),p(r,"min","1"),r.required=!0},m(f,c){w(f,e,c),m(e,t),w(f,o,c),w(f,r,c),Me(r,n[0].maxSelect),s||(a=X(r,"input",n[3]),s=!0)},p(f,c){c&32&&i!==(i=f[5])&&p(e,"for",i),c&32&&l!==(l=f[5])&&p(r,"id",l),c&1&&At(r.value)!==f[0].maxSelect&&Me(r,f[0].maxSelect)},d(f){f&&k(e),f&&k(o),f&&k(r),s=!1,a()}}}function G5(n){let e,t,i,o,r,l,s;function a(c){n[4](c)}let f={id:n[5],items:n[2]};return n[0].cascadeDelete!==void 0&&(f.keyOfSelected=n[0].cascadeDelete),r=new yo({props:f}),he.push(()=>Fe(r,"keyOfSelected",a)),{c(){e=g("label"),t=j("Delete record on user delete"),o=$(),V(r.$$.fragment),p(e,"for",i=n[5])},m(c,u){w(c,e,u),m(e,t),w(c,o,u),H(r,c,u),s=!0},p(c,u){(!s||u&32&&i!==(i=c[5]))&&p(e,"for",i);const d={};u&32&&(d.id=c[5]),!l&&u&1&&(l=!0,d.keyOfSelected=c[0].cascadeDelete,Re(()=>l=!1)),r.$set(d)},i(c){s||(T(r.$$.fragment,c),s=!0)},o(c){F(r.$$.fragment,c),s=!1},d(c){c&&k(e),c&&k(o),q(r,c)}}}function K5(n){let e,t,i,o,r,l,s;return i=new je({props:{class:"form-field required",name:"schema."+n[1]+".options.maxSelect",$$slots:{default:[Y5,({uniqueId:a})=>({5:a}),({uniqueId:a})=>a?32:0]},$$scope:{ctx:n}}}),l=new je({props:{class:"form-field",name:"schema."+n[1]+".options.cascadeDelete",$$slots:{default:[G5,({uniqueId:a})=>({5:a}),({uniqueId:a})=>a?32:0]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),V(i.$$.fragment),o=$(),r=g("div"),V(l.$$.fragment),p(t,"class","col-sm-6"),p(r,"class","col-sm-6"),p(e,"class","grid")},m(a,f){w(a,e,f),m(e,t),H(i,t,null),m(e,o),m(e,r),H(l,r,null),s=!0},p(a,[f]){const c={};f&2&&(c.name="schema."+a[1]+".options.maxSelect"),f&97&&(c.$$scope={dirty:f,ctx:a}),i.$set(c);const u={};f&2&&(u.name="schema."+a[1]+".options.cascadeDelete"),f&97&&(u.$$scope={dirty:f,ctx:a}),l.$set(u)},i(a){s||(T(i.$$.fragment,a),T(l.$$.fragment,a),s=!0)},o(a){F(i.$$.fragment,a),F(l.$$.fragment,a),s=!1},d(a){a&&k(e),q(i),q(l)}}}function J5(n,e,t){const i=[{label:"False",value:!1},{label:"True",value:!0}];let{key:o=""}=e,{options:r={}}=e;function l(){r.maxSelect=At(this.value),t(0,r)}function s(a){n.$$.not_equal(r.cascadeDelete,a)&&(r.cascadeDelete=a,t(0,r))}return n.$$set=a=>{"key"in a&&t(1,o=a.key),"options"in a&&t(0,r=a.options)},n.$$.update=()=>{n.$$.dirty&1&&B.isEmpty(r)&&t(0,r={maxSelect:1,cascadeDelete:!1})},[r,o,i,l,s]}class Z5 extends Ie{constructor(e){super(),Le(this,e,J5,K5,Ee,{key:1,options:0})}}function X5(n){let e,t,i,o,r,l,s;function a(c){n[15](c)}let f={id:n[37],disabled:n[0].id};return n[0].type!==void 0&&(f.value=n[0].type),r=new qC({props:f}),he.push(()=>Fe(r,"value",a)),{c(){e=g("label"),t=j("Type"),o=$(),V(r.$$.fragment),p(e,"for",i=n[37])},m(c,u){w(c,e,u),m(e,t),w(c,o,u),H(r,c,u),s=!0},p(c,u){(!s||u[1]&64&&i!==(i=c[37]))&&p(e,"for",i);const d={};u[1]&64&&(d.id=c[37]),u[0]&1&&(d.disabled=c[0].id),!l&&u[0]&1&&(l=!0,d.value=c[0].type,Re(()=>l=!1)),r.$set(d)},i(c){s||(T(r.$$.fragment,c),s=!0)},o(c){F(r.$$.fragment,c),s=!1},d(c){c&&k(e),c&&k(o),q(r,c)}}}function Q5(n){let e,t,i,o,r,l,s,a,f,c,u;return{c(){e=g("label"),t=j("Name"),o=$(),r=g("input"),p(e,"for",i=n[37]),p(r,"type","text"),p(r,"id",l=n[37]),r.required=!0,r.disabled=s=n[0].id&&n[0].system,p(r,"spellcheck","false"),r.autofocus=a=!n[0].id,r.value=f=n[0].name},m(d,h){w(d,e,h),m(e,t),w(d,o,h),w(d,r,h),n[0].id||r.focus(),c||(u=X(r,"input",n[16]),c=!0)},p(d,h){h[1]&64&&i!==(i=d[37])&&p(e,"for",i),h[1]&64&&l!==(l=d[37])&&p(r,"id",l),h[0]&1&&s!==(s=d[0].id&&d[0].system)&&(r.disabled=s),h[0]&1&&a!==(a=!d[0].id)&&(r.autofocus=a),h[0]&1&&f!==(f=d[0].name)&&r.value!==f&&(r.value=f)},d(d){d&&k(e),d&&k(o),d&&k(r),c=!1,u()}}}function ex(n){let e,t,i;function o(l){n[27](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new Z5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function tx(n){let e,t,i;function o(l){n[26](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new W5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function nx(n){let e,t,i;function o(l){n[25](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new z5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function ix(n){let e,t,i;function o(l){n[24](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new E5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function ox(n){let e,t,i;function o(l){n[23](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new O5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function rx(n){let e,t,i;function o(l){n[22](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new x5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function lx(n){let e,t,i;function o(l){n[21](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new c5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function sx(n){let e,t,i;function o(l){n[20](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new O1({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function ax(n){let e,t,i;function o(l){n[19](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new t5({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function fx(n){let e,t,i;function o(l){n[18](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new QC({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function cx(n){let e,t,i;function o(l){n[17](l)}let r={key:n[1]};return n[0].options!==void 0&&(r.options=n[0].options),e=new GC({props:r}),he.push(()=>Fe(e,"options",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){const a={};s[0]&2&&(a.key=l[1]),!t&&s[0]&1&&(t=!0,a.options=l[0].options,Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function ux(n){let e,t,i,o,r,l,s,a;return{c(){e=g("input"),i=$(),o=g("label"),r=j("Required"),p(e,"type","checkbox"),p(e,"id",t=n[37]),p(o,"for",l=n[37])},m(f,c){w(f,e,c),e.checked=n[0].required,w(f,i,c),w(f,o,c),m(o,r),s||(a=X(e,"change",n[28]),s=!0)},p(f,c){c[1]&64&&t!==(t=f[37])&&p(e,"id",t),c[0]&1&&(e.checked=f[0].required),c[1]&64&&l!==(l=f[37])&&p(o,"for",l)},d(f){f&&k(e),f&&k(i),f&&k(o),s=!1,a()}}}function Tp(n){let e,t;return e=new je({props:{class:"form-field form-field-toggle m-0",name:"unique",$$slots:{default:[dx,({uniqueId:i})=>({37:i}),({uniqueId:i})=>[0,i?64:0]]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,o){const r={};o[0]&1|o[1]&192&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function dx(n){let e,t,i,o,r,l,s,a;return{c(){e=g("input"),i=$(),o=g("label"),r=j("Unique"),p(e,"type","checkbox"),p(e,"id",t=n[37]),p(o,"for",l=n[37])},m(f,c){w(f,e,c),e.checked=n[0].unique,w(f,i,c),w(f,o,c),m(o,r),s||(a=X(e,"change",n[29]),s=!0)},p(f,c){c[1]&64&&t!==(t=f[37])&&p(e,"id",t),c[0]&1&&(e.checked=f[0].unique),c[1]&64&&l!==(l=f[37])&&p(o,"for",l)},d(f){f&&k(e),f&&k(i),f&&k(o),s=!1,a()}}}function px(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y,S,C,x,M;o=new je({props:{class:"form-field required "+(n[0].id?"disabled":""),name:"schema."+n[1]+".type",$$slots:{default:[X5,({uniqueId:P})=>({37:P}),({uniqueId:P})=>[0,P?64:0]]},$$scope:{ctx:n}}}),s=new je({props:{class:"form-field required "+(n[0].id&&n[0].system?"disabled":""),name:"schema."+n[1]+".name",$$slots:{default:[Q5,({uniqueId:P})=>({37:P}),({uniqueId:P})=>[0,P?64:0]]},$$scope:{ctx:n}}});const A=[cx,fx,ax,sx,lx,rx,ox,ix,nx,tx,ex],O=[];function D(P,I){return P[0].type==="text"?0:P[0].type==="number"?1:P[0].type==="bool"?2:P[0].type==="email"?3:P[0].type==="url"?4:P[0].type==="date"?5:P[0].type==="select"?6:P[0].type==="json"?7:P[0].type==="file"?8:P[0].type==="relation"?9:P[0].type==="user"?10:-1}~(c=D(n))&&(u=O[c]=A[c](n)),b=new je({props:{class:"form-field form-field-toggle m-0",name:"requried",$$slots:{default:[ux,({uniqueId:P})=>({37:P}),({uniqueId:P})=>[0,P?64:0]]},$$scope:{ctx:n}}});let E=n[0].type!=="file"&&Tp(n);return{c(){e=g("form"),t=g("div"),i=g("div"),V(o.$$.fragment),r=$(),l=g("div"),V(s.$$.fragment),a=$(),f=g("div"),u&&u.c(),d=$(),h=g("div"),V(b.$$.fragment),v=$(),_=g("div"),E&&E.c(),y=$(),S=g("input"),p(i,"class","col-sm-6"),p(l,"class","col-sm-6"),p(f,"class","col-sm-12 hidden-empty"),p(h,"class","col-4"),p(_,"class","col-4"),p(t,"class","grid"),p(S,"type","submit"),p(S,"class","hidden"),p(S,"tabindex","-1"),p(e,"class","field-form")},m(P,I){w(P,e,I),m(e,t),m(t,i),H(o,i,null),m(t,r),m(t,l),H(s,l,null),m(t,a),m(t,f),~c&&O[c].m(f,null),m(t,d),m(t,h),H(b,h,null),m(t,v),m(t,_),E&&E.m(_,null),m(e,y),m(e,S),C=!0,x||(M=X(e,"submit",Gt(n[30])),x=!0)},p(P,I){const R={};I[0]&1&&(R.class="form-field required "+(P[0].id?"disabled":"")),I[0]&2&&(R.name="schema."+P[1]+".type"),I[0]&1|I[1]&192&&(R.$$scope={dirty:I,ctx:P}),o.$set(R);const G={};I[0]&1&&(G.class="form-field required "+(P[0].id&&P[0].system?"disabled":"")),I[0]&2&&(G.name="schema."+P[1]+".name"),I[0]&1|I[1]&192&&(G.$$scope={dirty:I,ctx:P}),s.$set(G);let U=c;c=D(P),c===U?~c&&O[c].p(P,I):(u&&(Ae(),F(O[U],1,1,()=>{O[U]=null}),De()),~c?(u=O[c],u?u.p(P,I):(u=O[c]=A[c](P),u.c()),T(u,1),u.m(f,null)):u=null);const z={};I[0]&1|I[1]&192&&(z.$$scope={dirty:I,ctx:P}),b.$set(z),P[0].type!=="file"?E?(E.p(P,I),I[0]&1&&T(E,1)):(E=Tp(P),E.c(),T(E,1),E.m(_,null)):E&&(Ae(),F(E,1,1,()=>{E=null}),De())},i(P){C||(T(o.$$.fragment,P),T(s.$$.fragment,P),T(u),T(b.$$.fragment,P),T(E),C=!0)},o(P){F(o.$$.fragment,P),F(s.$$.fragment,P),F(u),F(b.$$.fragment,P),F(E),C=!1},d(P){P&&k(e),q(o),q(s),~c&&O[c].d(),q(b),E&&E.d(),x=!1,M()}}}function Ep(n){let e,t,i,o,r=n[0].system&&Pp(),l=!n[0].id&&Fp(n),s=n[0].required&&Lp(),a=n[0].unique&&Ip();return{c(){e=g("div"),r&&r.c(),t=$(),l&&l.c(),i=$(),s&&s.c(),o=$(),a&&a.c(),p(e,"class","inline-flex")},m(f,c){w(f,e,c),r&&r.m(e,null),m(e,t),l&&l.m(e,null),m(e,i),s&&s.m(e,null),m(e,o),a&&a.m(e,null)},p(f,c){f[0].system?r||(r=Pp(),r.c(),r.m(e,t)):r&&(r.d(1),r=null),f[0].id?l&&(l.d(1),l=null):l?l.p(f,c):(l=Fp(f),l.c(),l.m(e,i)),f[0].required?s||(s=Lp(),s.c(),s.m(e,o)):s&&(s.d(1),s=null),f[0].unique?a||(a=Ip(),a.c(),a.m(e,null)):a&&(a.d(1),a=null)},d(f){f&&k(e),r&&r.d(),l&&l.d(),s&&s.d(),a&&a.d()}}}function Pp(n){let e;return{c(){e=g("span"),e.textContent="System",p(e,"class","label label-danger")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Fp(n){let e;return{c(){e=g("span"),e.textContent="New",p(e,"class","label"),ne(e,"label-warning",n[7]&&!n[0].toDelete)},m(t,i){w(t,e,i)},p(t,i){i[0]&129&&ne(e,"label-warning",t[7]&&!t[0].toDelete)},d(t){t&&k(e)}}}function Lp(n){let e;return{c(){e=g("span"),e.textContent="Required",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Ip(n){let e;return{c(){e=g("span"),e.textContent="Unique",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Rp(n){let e,t,i,o,r,l;return{c(){e=g("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(s,a){w(s,e,a),o=!0,r||(l=Xe(t=St.call(null,e,{text:"Has errors",position:"left"})),r=!0)},i(s){o||(Dt(()=>{i||(i=ct(e,Bn,{duration:150,start:.7},!0)),i.run(1)}),o=!0)},o(s){i||(i=ct(e,Bn,{duration:150,start:.7},!1)),i.run(0),o=!1},d(s){s&&k(e),s&&i&&i.end(),r=!1,l()}}}function Np(n){let e,t,i,o,r,l,s=n[7]&&jp(n);return{c(){e=g("div"),t=g("button"),t.innerHTML='Remove',i=$(),s&&s.c(),p(t,"type","button"),p(t,"class","btn btn-sm fade p-l-0 p-r-0"),p(e,"class","inline-flex flex-gap-sm flex-nowrap")},m(a,f){w(a,e,f),m(e,t),m(e,i),s&&s.m(e,null),r||(l=X(t,"click",Vn(n[8])),r=!0)},p(a,f){a[7]?s?s.p(a,f):(s=jp(a),s.c(),s.m(e,null)):s&&(s.d(1),s=null)},i(a){o||Dt(()=>{o=yf(e,ti,{duration:200,x:20,opacity:0}),o.start()})},o:le,d(a){a&&k(e),s&&s.d(),r=!1,l()}}}function jp(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Done',p(e,"type","button"),p(e,"class","btn btn-sm btn-outline btn-expanded-sm")},m(o,r){w(o,e,r),t||(i=X(e,"click",Vn(n[3])),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function zp(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Restore',p(e,"type","button"),p(e,"class","btn btn-sm btn-danger btn-secondary")},m(o,r){w(o,e,r),t||(i=X(e,"click",Vn(n[14])),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function hx(n){let e,t,i,o,r,l,s=(n[0].name||"-")+"",a,f,c,u,d,h,b,v,_,y,S=!n[0].toDelete&&Ep(n),C=n[6]&&Rp(),x=n[36]&&!n[0].toDelete&&Np(n),M=n[0].toDelete&&zp(n);return{c(){e=g("div"),t=g("span"),i=g("i"),r=$(),l=g("strong"),a=j(s),c=$(),S&&S.c(),u=$(),d=g("div"),h=$(),C&&C.c(),b=$(),x&&x.c(),v=$(),M&&M.c(),_=lt(),p(i,"class",o=gc(B.getFieldTypeIcon(n[0].type))+" svelte-162uq6"),p(t,"class","icon field-type"),p(l,"class","title field-name svelte-162uq6"),p(l,"title",f=n[0].name),ne(l,"txt-strikethrough",n[0].toDelete),p(e,"class","inline-flex"),p(d,"class","flex-fill")},m(A,O){w(A,e,O),m(e,t),m(t,i),m(e,r),m(e,l),m(l,a),w(A,c,O),S&&S.m(A,O),w(A,u,O),w(A,d,O),w(A,h,O),C&&C.m(A,O),w(A,b,O),x&&x.m(A,O),w(A,v,O),M&&M.m(A,O),w(A,_,O),y=!0},p(A,O){(!y||O[0]&1&&o!==(o=gc(B.getFieldTypeIcon(A[0].type))+" svelte-162uq6"))&&p(i,"class",o),(!y||O[0]&1)&&s!==(s=(A[0].name||"-")+"")&&ge(a,s),(!y||O[0]&1&&f!==(f=A[0].name))&&p(l,"title",f),O[0]&1&&ne(l,"txt-strikethrough",A[0].toDelete),A[0].toDelete?S&&(S.d(1),S=null):S?S.p(A,O):(S=Ep(A),S.c(),S.m(u.parentNode,u)),A[6]?C?O[0]&64&&T(C,1):(C=Rp(),C.c(),T(C,1),C.m(b.parentNode,b)):C&&(Ae(),F(C,1,1,()=>{C=null}),De()),A[36]&&!A[0].toDelete?x?(x.p(A,O),O[0]&1|O[1]&32&&T(x,1)):(x=Np(A),x.c(),T(x,1),x.m(v.parentNode,v)):x&&(x.d(1),x=null),A[0].toDelete?M?M.p(A,O):(M=zp(A),M.c(),M.m(_.parentNode,_)):M&&(M.d(1),M=null)},i(A){y||(T(C),T(x),y=!0)},o(A){F(C),y=!1},d(A){A&&k(e),A&&k(c),S&&S.d(A),A&&k(u),A&&k(d),A&&k(h),C&&C.d(A),A&&k(b),x&&x.d(A),A&&k(v),M&&M.d(A),A&&k(_)}}}function mx(n){let e,t,i={single:!0,interactive:n[7],class:n[2]||n[0].toDelete||n[0].system?"field-accordion disabled":"field-accordion",$$slots:{header:[hx,({active:o})=>({36:o}),({active:o})=>[0,o?32:0]],default:[px]},$$scope:{ctx:n}};return e=new dc({props:i}),n[31](e),e.$on("expand",n[32]),e.$on("collapse",n[33]),e.$on("toggle",n[34]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,r){const l={};r[0]&128&&(l.interactive=o[7]),r[0]&5&&(l.class=o[2]||o[0].toDelete||o[0].system?"field-accordion disabled":"field-accordion"),r[0]&227|r[1]&160&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[31](null),q(e,o)}}}function bx(n,e,t){let i,o,r,l;pn(n,go,J=>t(13,l=J));const s=yn();let{key:a="0"}=e,{field:f=new kn}=e,{disabled:c=!1}=e,{excludeNames:u=[]}=e,d,h=f.type;function b(){d==null||d.expand()}function v(){d==null||d.collapse()}function _(){f.id?t(0,f.toDelete=!0,f):(v(),s("remove"))}function y(J){J=B.slugify(J);let $e="";for(;u.includes(J+$e);)++$e;return J+$e}di(()=>{f.id||b()});const S=()=>{t(0,f.toDelete=!1,f)};function C(J){n.$$.not_equal(f.type,J)&&(f.type=J,t(0,f),t(12,h),t(10,u),t(4,d))}const x=J=>{t(0,f.name=y(J.target.value),f),J.target.value=f.name};function M(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function A(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function O(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function D(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function E(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function P(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function I(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function R(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function G(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function U(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function z(J){n.$$.not_equal(f.options,J)&&(f.options=J,t(0,f),t(12,h),t(10,u),t(4,d))}function K(){f.required=this.checked,t(0,f),t(12,h),t(10,u),t(4,d)}function Y(){f.unique=this.checked,t(0,f),t(12,h),t(10,u),t(4,d)}const W=()=>{i&&v()};function te(J){he[J?"unshift":"push"](()=>{d=J,t(4,d)})}function ce(J){ft.call(this,n,J)}function ve(J){ft.call(this,n,J)}function oe(J){ft.call(this,n,J)}return n.$$set=J=>{"key"in J&&t(1,a=J.key),"field"in J&&t(0,f=J.field),"disabled"in J&&t(2,c=J.disabled),"excludeNames"in J&&t(10,u=J.excludeNames)},n.$$.update=()=>{if(n.$$.dirty[0]&4097&&h!=f.type&&(t(12,h=f.type),t(0,f.options={},f),t(0,f.unique=!1,f)),n.$$.dirty[0]&1025&&u.length){const J=y(f.name);f.name!==J&&t(0,f.name=J,f)}n.$$.dirty[0]&17&&f.toDelete&&(d&&v(),!f.name&&f.originalName&&t(0,f.name=f.originalName,f)),n.$$.dirty[0]&1&&!f.originalName&&f.name&&t(0,f.originalName=f.name,f),n.$$.dirty[0]&1&&typeof f.toDelete=="undefined"&&t(0,f.toDelete=!1,f),n.$$.dirty[0]&1&&t(5,i=!B.isEmpty(f.name)&&f.type),n.$$.dirty[0]&48&&(i||d&&b()),n.$$.dirty[0]&37&&t(7,o=!c&&!f.system&&!f.toDelete&&i),n.$$.dirty[0]&8194&&t(6,r=!B.isEmpty(B.getNestedVal(l,`schema.${a}`)))},[f,a,c,v,d,i,r,o,_,y,u,b,h,l,S,C,x,M,A,O,D,E,P,I,R,G,U,z,K,Y,W,te,ce,ve,oe]}class gx extends Ie{constructor(e){super(),Le(this,e,bx,mx,Ee,{key:1,field:0,disabled:2,excludeNames:10,expand:11,collapse:3},null,[-1,-1])}get expand(){return this.$$.ctx[11]}get collapse(){return this.$$.ctx[3]}}function Hp(n,e,t){const i=n.slice();return i[9]=e[t],i[10]=e,i[11]=t,i}function qp(n,e){let t,i,o,r;function l(f){e[5](f,e[9],e[10],e[11])}function s(){return e[6](e[11])}let a={key:e[11],excludeNames:e[1].concat(e[4](e[9]))};return e[9]!==void 0&&(a.field=e[9]),i=new gx({props:a}),he.push(()=>Fe(i,"field",l)),i.$on("remove",s),{key:n,first:null,c(){t=lt(),V(i.$$.fragment),this.first=t},m(f,c){w(f,t,c),H(i,f,c),r=!0},p(f,c){e=f;const u={};c&1&&(u.key=e[11]),c&1&&(u.excludeNames=e[1].concat(e[4](e[9]))),!o&&c&1&&(o=!0,u.field=e[9],Re(()=>o=!1)),i.$set(u)},i(f){r||(T(i.$$.fragment,f),r=!0)},o(f){F(i.$$.fragment,f),r=!1},d(f){f&&k(t),q(i,f)}}}function _x(n){let e,t=[],i=new Map,o,r,l,s,a,f,c,u,d,h,b,v=n[0].schema;const _=y=>y[11];for(let y=0;yh.name===d)}function f(d){let h=[];for(let b of o.schema)b!==d&&(h.push(b.name),b.id&&b.originalName!==""&&b.originalName!==b.name&&h.push(b.originalName));return h}function c(d,h,b,v){b[v]=d,t(0,o)}const u=d=>r(d);return n.$$set=d=>{"collection"in d&&t(0,o=d.collection)},n.$$.update=()=>{n.$$.dirty&1&&typeof(o==null?void 0:o.schema)=="undefined"&&(t(0,o=o||{}),t(0,o.schema=[],o))},[o,i,r,l,f,c,u]}class yx extends Ie{constructor(e){super(),Le(this,e,vx,_x,Ee,{collection:0})}}function Vp(n,e,t){const i=n.slice();return i[14]=e[t][0],i[15]=e[t][1],i[16]=e,i[17]=t,i}function Bp(n,e,t){const i=n.slice();return i[19]=e[t],i}function Up(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y,S,C,x,M,A,O,D,E,P,I,R,G,U,z=n[0].schema,K=[];for(let Y=0;Y@request filter:",y=$(),S=g("div"),S.innerHTML=`@request.method + @request.query.* + @request.data.* + @request.user.*`,C=$(),x=g("hr"),M=$(),A=g("p"),A.innerHTML="You could also add constraints and query other collections using the @collection filter:",O=$(),D=g("div"),D.innerHTML="@collection.ANY_COLLECTION_NAME.*",E=$(),P=g("hr"),I=$(),R=g("p"),R.innerHTML=`Example rule: +
    + @request.user.id!=null && created>"2022-01-01 00:00:00"`,p(o,"class","m-b-0"),p(l,"class","inline-flex flex-gap-5"),p(b,"class","m-t-10 m-b-5"),p(_,"class","m-b-0"),p(S,"class","inline-flex flex-gap-5"),p(x,"class","m-t-10 m-b-5"),p(A,"class","m-b-0"),p(D,"class","inline-flex flex-gap-5"),p(P,"class","m-t-10 m-b-5"),p(i,"class","content"),p(t,"class","alert alert-warning m-0")},m(Y,W){w(Y,e,W),m(e,t),m(t,i),m(i,o),m(i,r),m(i,l),m(l,s),m(l,a),m(l,f),m(l,c),m(l,u),m(l,d);for(let te=0;te{G||(G=ct(e,fn,{duration:150},!0)),G.run(1)}),U=!0)},o(Y){Y&&(G||(G=ct(e,fn,{duration:150},!1)),G.run(0)),U=!1},d(Y){Y&&k(e),qn(K,Y),Y&&G&&G.end()}}}function kx(n){let e,t=n[19].name+"",i;return{c(){e=g("code"),i=j(t)},m(o,r){w(o,e,r),m(e,i)},p(o,r){r&1&&t!==(t=o[19].name+"")&&ge(i,t)},d(o){o&&k(e)}}}function wx(n){let e,t=n[19].name+"",i,o;return{c(){e=g("code"),i=j(t),o=j(".*")},m(r,l){w(r,e,l),m(e,i),m(e,o)},p(r,l){l&1&&t!==(t=r[19].name+"")&&ge(i,t)},d(r){r&&k(e)}}}function Wp(n){let e;function t(r,l){return r[19].type==="relation"||r[19].type==="user"?wx:kx}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,l){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},d(r){o.d(r),r&&k(e)}}}function Sx(n){let e=[],t=new Map,i,o,r=Object.entries(n[6]);const l=s=>s[14];for(let s=0;s',p(e,"class","txt-center")},m(t,i){w(t,e,i)},p:le,i:le,o:le,d(t){t&&k(e)}}}function xx(n){let e,t,i;function o(){return n[9](n[14])}return{c(){e=g("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","rule-toggle-btn btn btn-circle btn-outline svelte-fjxz7k")},m(r,l){w(r,e,l),t||(i=[Xe(St.call(null,e,"Lock and set to Admins only")),X(e,"click",o)],t=!0)},p(r,l){n=r},d(r){r&&k(e),t=!1,rt(i)}}}function Mx(n){let e,t,i;function o(){return n[8](n[14])}return{c(){e=g("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","rule-toggle-btn btn btn-circle btn-outline btn-success svelte-fjxz7k")},m(r,l){w(r,e,l),t||(i=[Xe(St.call(null,e,"Unlock and set custom rule")),X(e,"click",o)],t=!0)},p(r,l){n=r},d(r){r&&k(e),t=!1,rt(i)}}}function $x(n){let e;return{c(){e=j("Leave empty to grant everyone access")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Ax(n){let e;return{c(){e=j("Only admins will be able to access (unlock to change)")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Dx(n){let e,t=n[15]+"",i,o,r=Ii(n[0][n[14]])?"Admins only":"Custom rule",l,s,a,f,c=n[14],u,d,h,b,v,_,y;function S(){return n[10](n[14])}const C=()=>n[11](f,c),x=()=>n[11](null,c);function M(I){n[12](I,n[14])}var A=n[4];function O(I){let R={baseCollection:I[0],disabled:Ii(I[0][I[14]])};return I[0][I[14]]!==void 0&&(R.value=I[0][I[14]]),{props:R}}A&&(f=new A(O(n)),C(),he.push(()=>Fe(f,"value",M)));function D(I,R){return R&1&&(b=null),b==null&&(b=!!Ii(I[0][I[14]])),b?Ax:$x}let E=D(n,-1),P=E(n);return{c(){e=g("label"),i=j(t),o=j(" - "),l=j(r),a=$(),f&&V(f.$$.fragment),d=$(),h=g("div"),P.c(),p(e,"for",s=n[18]),p(h,"class","help-block")},m(I,R){w(I,e,R),m(e,i),m(e,o),m(e,l),w(I,a,R),f&&H(f,I,R),w(I,d,R),w(I,h,R),P.m(h,null),v=!0,_||(y=X(e,"click",S),_=!0)},p(I,R){n=I,(!v||R&1)&&r!==(r=Ii(n[0][n[14]])?"Admins only":"Custom rule")&&ge(l,r),(!v||R&262144&&s!==(s=n[18]))&&p(e,"for",s),c!==n[14]&&(x(),c=n[14],C());const G={};if(R&1&&(G.baseCollection=n[0]),R&1&&(G.disabled=Ii(n[0][n[14]])),!u&&R&65&&(u=!0,G.value=n[0][n[14]],Re(()=>u=!1)),A!==(A=n[4])){if(f){Ae();const U=f;F(U.$$.fragment,1,0,()=>{q(U,1)}),De()}A?(f=new A(O(n)),C(),he.push(()=>Fe(f,"value",M)),V(f.$$.fragment),T(f.$$.fragment,1),H(f,d.parentNode,d)):f=null}else A&&f.$set(G);E!==(E=D(n,R))&&(P.d(1),P=E(n),P&&(P.c(),P.m(h,null)))},i(I){v||(f&&T(f.$$.fragment,I),v=!0)},o(I){f&&F(f.$$.fragment,I),v=!1},d(I){I&&k(e),I&&k(a),x(),f&&q(f,I),I&&k(d),I&&k(h),P.d(),_=!1,y()}}}function Yp(n,e){let t,i,o,r,l,s,a,f;function c(h,b){return b&1&&(r=null),r==null&&(r=!!Ii(h[0][h[14]])),r?Mx:xx}let u=c(e,-1),d=u(e);return s=new je({props:{class:"form-field rule-field m-0 "+(Ii(e[0][e[14]])?"disabled":""),name:e[14],$$slots:{default:[Dx,({uniqueId:h})=>({18:h}),({uniqueId:h})=>h?262144:0]},$$scope:{ctx:e}}}),{key:n,first:null,c(){t=g("hr"),i=$(),o=g("div"),d.c(),l=$(),V(s.$$.fragment),a=$(),p(t,"class","m-t-sm m-b-sm"),p(o,"class","rule-block svelte-fjxz7k"),this.first=t},m(h,b){w(h,t,b),w(h,i,b),w(h,o,b),d.m(o,null),m(o,l),H(s,o,null),m(o,a),f=!0},p(h,b){e=h,u===(u=c(e,b))&&d?d.p(e,b):(d.d(1),d=u(e),d&&(d.c(),d.m(o,l)));const v={};b&1&&(v.class="form-field rule-field m-0 "+(Ii(e[0][e[14]])?"disabled":"")),b&4456473&&(v.$$scope={dirty:b,ctx:e}),s.$set(v)},i(h){f||(T(s.$$.fragment,h),f=!0)},o(h){F(s.$$.fragment,h),f=!1},d(h){h&&k(t),h&&k(i),h&&k(o),d.d(),q(s)}}}function Ox(n){let e,t,i,o,r,l=n[2]?"Hide available fields":"Show available fields",s,a,f,c,u,d,h,b,v,_=n[2]&&Up(n);const y=[Cx,Sx],S=[];function C(x,M){return x[5]?0:1}return c=C(n),u=S[c]=y[c](n),{c(){e=g("div"),t=g("div"),i=g("p"),i.innerHTML=`All rules follow the +
    PocketBase filter syntax and operators + .`,o=$(),r=g("span"),s=j(l),a=$(),_&&_.c(),f=$(),u.c(),d=lt(),p(r,"class","expand-handle txt-sm txt-bold txt-nowrap link-hint"),p(t,"class","flex"),p(e,"class","block m-b-base")},m(x,M){w(x,e,M),m(e,t),m(t,i),m(t,o),m(t,r),m(r,s),m(e,a),_&&_.m(e,null),w(x,f,M),S[c].m(x,M),w(x,d,M),h=!0,b||(v=X(r,"click",n[7]),b=!0)},p(x,[M]){(!h||M&4)&&l!==(l=x[2]?"Hide available fields":"Show available fields")&&ge(s,l),x[2]?_?(_.p(x,M),M&4&&T(_,1)):(_=Up(x),_.c(),T(_,1),_.m(e,null)):_&&(Ae(),F(_,1,1,()=>{_=null}),De());let A=c;c=C(x),c===A?S[c].p(x,M):(Ae(),F(S[A],1,1,()=>{S[A]=null}),De(),u=S[c],u?u.p(x,M):(u=S[c]=y[c](x),u.c()),T(u,1),u.m(d.parentNode,d))},i(x){h||(T(_),T(u),h=!0)},o(x){F(_),F(u),h=!1},d(x){x&&k(e),_&&_.d(),x&&k(f),S[c].d(x),x&&k(d),b=!1,v()}}}function Ii(n){return n===null}function Tx(n,e,t){let{collection:i=new En}=e,o={},r=!1,l={},s,a=!1;const f={listRule:"List Action",viewRule:"View Action",createRule:"Create Action",updateRule:"Update Action",deleteRule:"Delete Action"};async function c(){t(5,a=!0);try{t(4,s=(await _i(()=>import("./FilterAutocompleteInput.15d21df7.js"),[])).default)}catch(y){console.warn(y),t(4,s=null)}t(5,a=!1)}di(()=>{c()});const u=()=>t(2,r=!r),d=async y=>{var S;t(0,i[y]=o[y]||"",i),await Bi(),(S=l[y])==null||S.focus()},h=y=>{t(1,o[y]=i[y],o),t(0,i[y]=null,i)},b=y=>{var S;return(S=l[y])==null?void 0:S.focus()};function v(y,S){he[y?"unshift":"push"](()=>{l[S]=y,t(3,l)})}function _(y,S){n.$$.not_equal(i[S],y)&&(i[S]=y,t(0,i))}return n.$$set=y=>{"collection"in y&&t(0,i=y.collection)},[i,o,r,l,s,a,f,u,d,h,b,v,_]}class Ex extends Ie{constructor(e){super(),Le(this,e,Tx,Ox,Ee,{collection:0})}}function Gp(n,e,t){const i=n.slice();return i[14]=e[t],i}function Kp(n,e,t){const i=n.slice();return i[14]=e[t],i}function Jp(n){let e;return{c(){e=g("p"),e.textContent="All data associated with the removed fields will be permanently deleted!"},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Zp(n){let e,t,i,o,r=n[1].originalName+"",l,s,a,f,c,u=n[1].name+"",d;return{c(){e=g("li"),t=g("div"),i=j(`Renamed collection + `),o=g("strong"),l=j(r),s=$(),a=g("i"),f=$(),c=g("strong"),d=j(u),p(o,"class","txt-strikethrough txt-hint"),p(a,"class","ri-arrow-right-line txt-sm"),p(c,"class","txt"),p(t,"class","inline-flex")},m(h,b){w(h,e,b),m(e,t),m(t,i),m(t,o),m(o,l),m(t,s),m(t,a),m(t,f),m(t,c),m(c,d)},p(h,b){b&2&&r!==(r=h[1].originalName+"")&&ge(l,r),b&2&&u!==(u=h[1].name+"")&&ge(d,u)},d(h){h&&k(e)}}}function Xp(n){let e,t,i,o,r=n[14].originalName+"",l,s,a,f,c,u=n[14].name+"",d;return{c(){e=g("li"),t=g("div"),i=j(`Renamed field + `),o=g("strong"),l=j(r),s=$(),a=g("i"),f=$(),c=g("strong"),d=j(u),p(o,"class","txt-strikethrough txt-hint"),p(a,"class","ri-arrow-right-line txt-sm"),p(c,"class","txt"),p(t,"class","inline-flex")},m(h,b){w(h,e,b),m(e,t),m(t,i),m(t,o),m(o,l),m(t,s),m(t,a),m(t,f),m(t,c),m(c,d)},p(h,b){b&16&&r!==(r=h[14].originalName+"")&&ge(l,r),b&16&&u!==(u=h[14].name+"")&&ge(d,u)},d(h){h&&k(e)}}}function Qp(n){let e,t,i,o=n[14].name+"",r,l;return{c(){e=g("li"),t=j("Removed field "),i=g("span"),r=j(o),l=$(),p(i,"class","txt-bold"),p(e,"class","txt-danger")},m(s,a){w(s,e,a),m(e,t),m(e,i),m(i,r),m(e,l)},p(s,a){a&8&&o!==(o=s[14].name+"")&&ge(r,o)},d(s){s&&k(e)}}}function Px(n){let e,t,i,o,r,l,s,a,f,c,u,d,h=n[3].length&&Jp(),b=n[5]&&Zp(n),v=n[4],_=[];for(let C=0;C',i=$(),o=g("div"),r=g("p"),r.textContent=`If any of the following changes is part of another collection rule or filter, you'll have to + update it manually!`,l=$(),h&&h.c(),s=$(),a=g("h6"),a.textContent="Changes:",f=$(),c=g("ul"),b&&b.c(),u=$();for(let C=0;C<_.length;C+=1)_[C].c();d=$();for(let C=0;CCancel',t=$(),i=g("button"),i.innerHTML='Confirm',e.autofocus=!0,p(e,"type","button"),p(e,"class","btn btn-secondary"),p(i,"type","button"),p(i,"class","btn btn-expanded")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s),e.focus(),o||(r=[X(e,"click",n[8]),X(i,"click",n[9])],o=!0)},p:le,d(l){l&&k(e),l&&k(t),l&&k(i),o=!1,rt(r)}}}function Ix(n){let e,t,i={class:"confirm-changes-panel",popup:!0,$$slots:{footer:[Lx],header:[Fx],default:[Px]},$$scope:{ctx:n}};return e=new Ai({props:i}),n[10](e),e.$on("hide",n[11]),e.$on("show",n[12]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,[r]){const l={};r&524346&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[10](null),q(e,o)}}}function Rx(n,e,t){let i,o,r;const l=yn();let s,a;async function f(y){t(1,a=y),await Bi(),!i&&!o.length&&!r.length?u():s==null||s.show()}function c(){s==null||s.hide()}function u(){c(),l("confirm")}const d=()=>c(),h=()=>u();function b(y){he[y?"unshift":"push"](()=>{s=y,t(2,s)})}function v(y){ft.call(this,n,y)}function _(y){ft.call(this,n,y)}return n.$$.update=()=>{n.$$.dirty&2&&t(5,i=(a==null?void 0:a.originalName)!=(a==null?void 0:a.name)),n.$$.dirty&2&&t(4,o=(a==null?void 0:a.schema.filter(y=>y.id&&!y.toDelete&&y.originalName!=y.name))||[]),n.$$.dirty&2&&t(3,r=(a==null?void 0:a.schema.filter(y=>y.id&&y.toDelete))||[])},[c,a,s,r,o,i,u,f,d,h,b,v,_]}class Nx extends Ie{constructor(e){super(),Le(this,e,Rx,Ix,Ee,{show:7,hide:0})}get show(){return this.$$.ctx[7]}get hide(){return this.$$.ctx[0]}}function eh(n){let e,t,i,o;function r(s){n[26](s)}let l={};return n[2]!==void 0&&(l.collection=n[2]),t=new Ex({props:l}),he.push(()=>Fe(t,"collection",r)),{c(){e=g("div"),V(t.$$.fragment),p(e,"class","tab-item active")},m(s,a){w(s,e,a),H(t,e,null),o=!0},p(s,a){const f={};!i&&a[0]&4&&(i=!0,f.collection=s[2],Re(()=>i=!1)),t.$set(f)},i(s){o||(T(t.$$.fragment,s),o=!0)},o(s){F(t.$$.fragment,s),o=!1},d(s){s&&k(e),q(t)}}}function jx(n){let e,t,i,o,r,l;function s(c){n[25](c)}let a={};n[2]!==void 0&&(a.collection=n[2]),i=new yx({props:a}),he.push(()=>Fe(i,"collection",s));let f=n[9]===qr&&eh(n);return{c(){e=g("div"),t=g("div"),V(i.$$.fragment),r=$(),f&&f.c(),p(t,"class","tab-item"),ne(t,"active",n[9]===ho),p(e,"class","tabs-content svelte-b10vi")},m(c,u){w(c,e,u),m(e,t),H(i,t,null),m(e,r),f&&f.m(e,null),l=!0},p(c,u){const d={};!o&&u[0]&4&&(o=!0,d.collection=c[2],Re(()=>o=!1)),i.$set(d),u[0]&512&&ne(t,"active",c[9]===ho),c[9]===qr?f?(f.p(c,u),u[0]&512&&T(f,1)):(f=eh(c),f.c(),T(f,1),f.m(e,null)):f&&(Ae(),F(f,1,1,()=>{f=null}),De())},i(c){l||(T(i.$$.fragment,c),T(f),l=!0)},o(c){F(i.$$.fragment,c),F(f),l=!1},d(c){c&&k(e),q(i),f&&f.d()}}}function th(n){let e,t,i,o,r,l,s;return l=new vo({props:{class:"dropdown dropdown-right m-t-5",$$slots:{default:[zx]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=$(),i=g("button"),o=g("i"),r=$(),V(l.$$.fragment),p(e,"class","flex-fill"),p(o,"class","ri-more-line"),p(i,"type","button"),p(i,"class","btn btn-sm btn-circle btn-secondary flex-gap-0")},m(a,f){w(a,e,f),w(a,t,f),w(a,i,f),m(i,o),m(i,r),H(l,i,null),s=!0},p(a,f){const c={};f[1]&256&&(c.$$scope={dirty:f,ctx:a}),l.$set(c)},i(a){s||(T(l.$$.fragment,a),s=!0)},o(a){F(l.$$.fragment,a),s=!1},d(a){a&&k(e),a&&k(t),a&&k(i),q(l)}}}function zx(n){let e,t,i;return{c(){e=g("button"),e.innerHTML=` + Delete`,p(e,"type","button"),p(e,"class","dropdown-item closable")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[20]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function nh(n){let e;return{c(){e=g("div"),e.textContent="System collection",p(e,"class","help-block")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Hx(n){let e,t,i,o,r,l,s,a,f,c,u,d,h=n[2].system&&nh();return{c(){e=g("label"),t=j("Name"),o=$(),r=g("input"),f=$(),h&&h.c(),c=lt(),p(e,"for",i=n[38]),p(r,"type","text"),p(r,"id",l=n[38]),r.required=!0,r.disabled=n[11],p(r,"spellcheck","false"),r.autofocus=s=n[2].isNew,p(r,"placeholder",'eg. "posts"'),r.value=a=n[2].name},m(b,v){w(b,e,v),m(e,t),w(b,o,v),w(b,r,v),w(b,f,v),h&&h.m(b,v),w(b,c,v),n[2].isNew&&r.focus(),u||(d=X(r,"input",n[21]),u=!0)},p(b,v){v[1]&128&&i!==(i=b[38])&&p(e,"for",i),v[1]&128&&l!==(l=b[38])&&p(r,"id",l),v[0]&2048&&(r.disabled=b[11]),v[0]&4&&s!==(s=b[2].isNew)&&(r.autofocus=s),v[0]&4&&a!==(a=b[2].name)&&r.value!==a&&(r.value=a),b[2].system?h||(h=nh(),h.c(),h.m(c.parentNode,c)):h&&(h.d(1),h=null)},d(b){b&&k(e),b&&k(o),b&&k(r),b&&k(f),h&&h.d(b),b&&k(c),u=!1,d()}}}function ih(n){let e,t,i,o,r,l;return{c(){e=g("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(s,a){w(s,e,a),o=!0,r||(l=Xe(t=St.call(null,e,n[12])),r=!0)},p(s,a){t&&Yn(t.update)&&a[0]&4096&&t.update.call(null,s[12])},i(s){o||(s&&Dt(()=>{i||(i=ct(e,Bn,{duration:150,start:.7},!0)),i.run(1)}),o=!0)},o(s){s&&(i||(i=ct(e,Bn,{duration:150,start:.7},!1)),i.run(0)),o=!1},d(s){s&&k(e),s&&i&&i.end(),r=!1,l()}}}function oh(n){let e,t,i,o,r;return{c(){e=g("i"),p(e,"class","ri-error-warning-fill txt-danger")},m(l,s){w(l,e,s),i=!0,o||(r=Xe(St.call(null,e,"Has errors")),o=!0)},i(l){i||(l&&Dt(()=>{t||(t=ct(e,Bn,{duration:150,start:.7},!0)),t.run(1)}),i=!0)},o(l){l&&(t||(t=ct(e,Bn,{duration:150,start:.7},!1)),t.run(0)),i=!1},d(l){l&&k(e),l&&t&&t.end(),o=!1,r()}}}function qx(n){var I,R,G,U,z,K;let e,t=n[2].isNew?"New collection":"Edit collection",i,o,r,l,s,a,f,c,u,d,h,b,v=!B.isEmpty((I=n[4])==null?void 0:I.schema),_,y,S,C,x=!B.isEmpty((R=n[4])==null?void 0:R.listRule)||!B.isEmpty((G=n[4])==null?void 0:G.viewRule)||!B.isEmpty((U=n[4])==null?void 0:U.createRule)||!B.isEmpty((z=n[4])==null?void 0:z.updateRule)||!B.isEmpty((K=n[4])==null?void 0:K.deleteRule),M,A,O,D=!n[2].isNew&&!n[2].system&&th(n);s=new je({props:{class:"form-field required m-b-0 "+(n[11]?"disabled":""),name:"name",$$slots:{default:[Hx,({uniqueId:Y})=>({38:Y}),({uniqueId:Y})=>[0,Y?128:0]]},$$scope:{ctx:n}}});let E=v&&ih(n),P=x&&oh();return{c(){e=g("h4"),i=j(t),o=$(),D&&D.c(),r=$(),l=g("form"),V(s.$$.fragment),a=$(),f=g("input"),c=$(),u=g("div"),d=g("button"),h=g("span"),h.textContent="Fields",b=$(),E&&E.c(),_=$(),y=g("button"),S=g("span"),S.textContent="API Rules",C=$(),P&&P.c(),p(f,"type","submit"),p(f,"class","hidden"),p(f,"tabindex","-1"),p(l,"class","block"),p(h,"class","txt"),p(d,"type","button"),p(d,"class","tab-item"),ne(d,"active",n[9]===ho),p(S,"class","txt"),p(y,"type","button"),p(y,"class","tab-item"),ne(y,"active",n[9]===qr),p(u,"class","tabs-header stretched")},m(Y,W){w(Y,e,W),m(e,i),w(Y,o,W),D&&D.m(Y,W),w(Y,r,W),w(Y,l,W),H(s,l,null),m(l,a),m(l,f),w(Y,c,W),w(Y,u,W),m(u,d),m(d,h),m(d,b),E&&E.m(d,null),m(u,_),m(u,y),m(y,S),m(y,C),P&&P.m(y,null),M=!0,A||(O=[X(l,"submit",Gt(n[22])),X(d,"click",n[23]),X(y,"click",n[24])],A=!0)},p(Y,W){var ce,ve,oe,J,$e,ee;(!M||W[0]&4)&&t!==(t=Y[2].isNew?"New collection":"Edit collection")&&ge(i,t),!Y[2].isNew&&!Y[2].system?D?(D.p(Y,W),W[0]&4&&T(D,1)):(D=th(Y),D.c(),T(D,1),D.m(r.parentNode,r)):D&&(Ae(),F(D,1,1,()=>{D=null}),De());const te={};W[0]&2048&&(te.class="form-field required m-b-0 "+(Y[11]?"disabled":"")),W[0]&2052|W[1]&384&&(te.$$scope={dirty:W,ctx:Y}),s.$set(te),W[0]&16&&(v=!B.isEmpty((ce=Y[4])==null?void 0:ce.schema)),v?E?(E.p(Y,W),W[0]&16&&T(E,1)):(E=ih(Y),E.c(),T(E,1),E.m(d,null)):E&&(Ae(),F(E,1,1,()=>{E=null}),De()),W[0]&512&&ne(d,"active",Y[9]===ho),W[0]&16&&(x=!B.isEmpty((ve=Y[4])==null?void 0:ve.listRule)||!B.isEmpty((oe=Y[4])==null?void 0:oe.viewRule)||!B.isEmpty((J=Y[4])==null?void 0:J.createRule)||!B.isEmpty(($e=Y[4])==null?void 0:$e.updateRule)||!B.isEmpty((ee=Y[4])==null?void 0:ee.deleteRule)),x?P?W[0]&16&&T(P,1):(P=oh(),P.c(),T(P,1),P.m(y,null)):P&&(Ae(),F(P,1,1,()=>{P=null}),De()),W[0]&512&&ne(y,"active",Y[9]===qr)},i(Y){M||(T(D),T(s.$$.fragment,Y),T(E),T(P),M=!0)},o(Y){F(D),F(s.$$.fragment,Y),F(E),F(P),M=!1},d(Y){Y&&k(e),Y&&k(o),D&&D.d(Y),Y&&k(r),Y&&k(l),q(s),Y&&k(c),Y&&k(u),E&&E.d(),P&&P.d(),A=!1,rt(O)}}}function Vx(n){let e,t,i,o,r,l=n[2].isNew?"Create":"Save changes",s,a,f,c;return{c(){e=g("button"),t=g("span"),t.textContent="Cancel",i=$(),o=g("button"),r=g("span"),s=j(l),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-secondary"),e.disabled=n[7],p(r,"class","txt"),p(o,"type","button"),p(o,"class","btn btn-expanded"),o.disabled=a=!n[10]||n[7],ne(o,"btn-loading",n[7])},m(u,d){w(u,e,d),m(e,t),w(u,i,d),w(u,o,d),m(o,r),m(r,s),f||(c=[X(e,"click",n[18]),X(o,"click",n[19])],f=!0)},p(u,d){d[0]&128&&(e.disabled=u[7]),d[0]&4&&l!==(l=u[2].isNew?"Create":"Save changes")&&ge(s,l),d[0]&1152&&a!==(a=!u[10]||u[7])&&(o.disabled=a),d[0]&128&&ne(o,"btn-loading",u[7])},d(u){u&&k(e),u&&k(i),u&&k(o),f=!1,rt(c)}}}function Bx(n){let e,t,i,o,r={class:"overlay-panel-lg colored-header collection-panel",beforeHide:n[27],$$slots:{footer:[Vx],header:[qx],default:[jx]},$$scope:{ctx:n}};e=new Ai({props:r}),n[28](e),e.$on("hide",n[29]),e.$on("show",n[30]);let l={};return i=new Nx({props:l}),n[31](i),i.$on("confirm",n[32]),{c(){V(e.$$.fragment),t=$(),V(i.$$.fragment)},m(s,a){H(e,s,a),w(s,t,a),H(i,s,a),o=!0},p(s,a){const f={};a[0]&264&&(f.beforeHide=s[27]),a[0]&7828|a[1]&256&&(f.$$scope={dirty:a,ctx:s}),e.$set(f);const c={};i.$set(c)},i(s){o||(T(e.$$.fragment,s),T(i.$$.fragment,s),o=!0)},o(s){F(e.$$.fragment,s),F(i.$$.fragment,s),o=!1},d(s){n[28](null),q(e,s),s&&k(t),n[31](null),q(i,s)}}}const ho="fields",qr="api_rules";function Aa(n){return JSON.stringify(n)}function Ux(n,e,t){let i,o,r,l,s,a;pn(n,fi,ee=>t(34,s=ee)),pn(n,go,ee=>t(4,a=ee));const f=yn();let c,u,d=null,h=new En,b=!1,v=!1,_=ho,y=Aa(h);function S(ee){t(9,_=ee)}function C(ee){return M(ee),t(8,v=!0),S(ho),c==null?void 0:c.show()}function x(){return c==null?void 0:c.hide()}async function M(ee){Ui({}),typeof ee!="undefined"?(d=ee,t(2,h=ee==null?void 0:ee.clone())):(d=null,t(2,h=new En)),t(2,h.schema=h.schema||[],h),t(2,h.originalName=h.name||"",h),await Bi(),t(17,y=Aa(h))}function A(){if(h.isNew)return O();u==null||u.show(h)}function O(){if(b)return;t(7,b=!0);const ee=D();let _e;h.isNew?_e=Se.Collections.create(ee):_e=Se.Collections.update(h.id,ee),_e.then(fe=>{t(8,v=!1),x(),hn(h.isNew?"Successfully created collection.":"Successfully updated collection."),bC(fe),h.isNew&&jb(fi,s=fe,s),f("save",fe)}).catch(fe=>{Se.errorResponseHandler(fe)}).finally(()=>{t(7,b=!1)})}function D(){const ee=h.export();ee.schema=ee.schema.slice(0);for(let _e=ee.schema.length-1;_e>=0;_e--)ee.schema[_e].toDelete&&ee.schema.splice(_e,1);return ee}function E(){!(d!=null&&d.id)||xi(`Do you really want to delete collection "${d==null?void 0:d.name}" and all its records?`,()=>Se.Collections.delete(d==null?void 0:d.id).then(()=>{x(),hn(`Successfully deleted collection "${d==null?void 0:d.name}".`),f("delete",d),gC(d)}).catch(ee=>{Se.errorResponseHandler(ee)}))}const P=()=>x(),I=()=>A(),R=()=>E(),G=ee=>{t(2,h.name=B.slugify(ee.target.value),h),ee.target.value=h.name},U=()=>{l&&A()},z=()=>S(ho),K=()=>S(qr);function Y(ee){h=ee,t(2,h)}function W(ee){h=ee,t(2,h)}const te=()=>r&&v?(xi("You have unsaved changes. Do you really want to close the panel?",()=>{t(8,v=!1),x()}),!1):!0;function ce(ee){he[ee?"unshift":"push"](()=>{c=ee,t(5,c)})}function ve(ee){ft.call(this,n,ee)}function oe(ee){ft.call(this,n,ee)}function J(ee){he[ee?"unshift":"push"](()=>{u=ee,t(6,u)})}const $e=()=>O();return n.$$.update=()=>{n.$$.dirty[0]&16&&t(12,i=typeof B.getNestedVal(a,"schema.message",null)=="string"?B.getNestedVal(a,"schema.message"):"Has errors"),n.$$.dirty[0]&4&&t(11,o=!h.isNew&&h.system),n.$$.dirty[0]&131076&&t(3,r=y!=Aa(h)),n.$$.dirty[0]&12&&t(10,l=h.isNew||r)},[S,x,h,r,a,c,u,b,v,_,l,o,i,A,O,E,C,y,P,I,R,G,U,z,K,Y,W,te,ce,ve,oe,J,$e]}class hc extends Ie{constructor(e){super(),Le(this,e,Ux,Bx,Ee,{changeTab:0,show:16,hide:1},null,[-1,-1])}get changeTab(){return this.$$.ctx[0]}get show(){return this.$$.ctx[16]}get hide(){return this.$$.ctx[1]}}function rh(n,e,t){const i=n.slice();return i[13]=e[t],i}function lh(n){let e,t=n[1].length&&sh();return{c(){t&&t.c(),e=lt()},m(i,o){t&&t.m(i,o),w(i,e,o)},p(i,o){i[1].length?t||(t=sh(),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){t&&t.d(i),i&&k(e)}}}function sh(n){let e;return{c(){e=g("p"),e.textContent="No collections found.",p(e,"class","txt-hint m-t-10 m-b-10 txt-center")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Wx(n){let e;return{c(){e=g("i"),p(e,"class","ri-folder-2-line")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Yx(n){let e;return{c(){e=g("i"),p(e,"class","ri-folder-open-line")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function ah(n,e){let t,i,o,r=e[13].name+"",l,s,a,f;function c(b,v){var _;return((_=b[5])==null?void 0:_.id)===b[13].id?Yx:Wx}let u=c(e),d=u(e);function h(){return e[10](e[13])}return{key:n,first:null,c(){var b;t=g("div"),d.c(),i=$(),o=g("span"),l=j(r),s=$(),p(o,"class","txt"),p(t,"tabindex","0"),p(t,"class","sidebar-list-item"),ne(t,"active",((b=e[5])==null?void 0:b.id)===e[13].id),this.first=t},m(b,v){w(b,t,v),d.m(t,null),m(t,i),m(t,o),m(o,l),m(t,s),a||(f=X(t,"click",h),a=!0)},p(b,v){var _;e=b,u!==(u=c(e))&&(d.d(1),d=u(e),d&&(d.c(),d.m(t,i))),v&8&&r!==(r=e[13].name+"")&&ge(l,r),v&40&&ne(t,"active",((_=e[5])==null?void 0:_.id)===e[13].id)},d(b){b&&k(t),d.d(),a=!1,f()}}}function Gx(n){let e,t,i,o,r,l,s,a,f,c,u,d=[],h=new Map,b,v,_,y,S,C,x,M,A=n[3];const O=P=>P[13].id;for(let P=0;P',l=$(),s=g("input"),a=$(),f=g("hr"),c=$(),u=g("div");for(let P=0;P + New collection`,y=$(),V(S.$$.fragment),p(r,"type","button"),p(r,"class","btn btn-xs btn-secondary btn-circle btn-clear"),ne(r,"hidden",!n[4]),p(o,"class","form-field-addon"),p(s,"type","text"),p(s,"placeholder","Search collections..."),p(i,"class","form-field search"),ne(i,"active",n[4]),p(t,"class","sidebar-header"),p(f,"class","m-t-5 m-b-xs"),p(u,"class","sidebar-content"),p(_,"type","button"),p(_,"class","btn btn-block btn-outline"),p(v,"class","sidebar-footer"),p(e,"class","page-sidebar collection-sidebar")},m(P,I){w(P,e,I),m(e,t),m(t,i),m(i,o),m(o,r),m(i,l),m(i,s),Me(s,n[0]),m(e,a),m(e,f),m(e,c),m(e,u);for(let R=0;Rt(5,l=_)),pn(n,Go,_=>t(7,s=_));let a,f="";function c(_){jb(fi,l=_,l)}const u=()=>t(0,f="");function d(){f=this.value,t(0,f)}const h=_=>c(_),b=()=>a==null?void 0:a.show();function v(_){he[_?"unshift":"push"](()=>{a=_,t(2,a)})}return n.$$.update=()=>{n.$$.dirty&1&&t(1,i=f.replace(/\s+/g,"").toLowerCase()),n.$$.dirty&1&&t(4,o=f!==""),n.$$.dirty&131&&t(3,r=s.filter(_=>_.name!="profiles"&&(_.id==f||_.name.replace(/\s+/g,"").toLowerCase().includes(i))))},[f,i,a,r,o,l,c,s,u,d,h,b,v]}class Jx extends Ie{constructor(e){super(),Le(this,e,Kx,Gx,Ee,{})}}function Zx(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y,S,C,x,M,A,O,D,E,P,I,R,G,U,z,K,Y,W,te,ce,ve,oe,J,$e,ee,_e,fe,ie,ye,Ne,Pe,ze,se,re,ke,He;return{c(){e=g("p"),e.innerHTML=`The syntax basically follows the format + OPERAND + OPERATOR + OPERAND, where:`,t=$(),i=g("ul"),o=g("li"),o.innerHTML=`OPERAND - could be any of the above field literal, string (single or double + quoted), number, null, true, false`,r=$(),l=g("li"),s=g("code"),s.textContent="OPERATOR",a=j(` - is one of: + `),f=g("br"),c=$(),u=g("ul"),d=g("li"),h=g("code"),h.textContent="=",b=$(),v=g("span"),v.textContent="Equal",_=$(),y=g("li"),S=g("code"),S.textContent="!=",C=$(),x=g("span"),x.textContent="NOT equal",M=$(),A=g("li"),O=g("code"),O.textContent=">",D=$(),E=g("span"),E.textContent="Greater than",P=$(),I=g("li"),R=g("code"),R.textContent=">=",G=$(),U=g("span"),U.textContent="Greater than or equal",z=$(),K=g("li"),Y=g("code"),Y.textContent="<",W=$(),te=g("span"),te.textContent="Less than or equal",ce=$(),ve=g("li"),oe=g("code"),oe.textContent="<=",J=$(),$e=g("span"),$e.textContent="Less than or equal",ee=$(),_e=g("li"),fe=g("code"),fe.textContent="~",ie=$(),ye=g("span"),ye.textContent=`Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for wildcard + match)`,Ne=$(),Pe=g("li"),ze=g("code"),ze.textContent="!~",se=$(),re=g("span"),re.textContent=`NOT Like/Contains (if not specified auto wraps the right string OPERAND in a "%" for + wildcard match)`,ke=$(),He=g("p"),He.innerHTML=`To group and combine several expressions you could use brackets + (...), && (AND) and || (OR) tokens.`,p(s,"class","txt-danger"),p(h,"class","filter-op svelte-1w7s5nw"),p(v,"class","txt-hint"),p(S,"class","filter-op svelte-1w7s5nw"),p(x,"class","txt-hint"),p(O,"class","filter-op svelte-1w7s5nw"),p(E,"class","txt-hint"),p(R,"class","filter-op svelte-1w7s5nw"),p(U,"class","txt-hint"),p(Y,"class","filter-op svelte-1w7s5nw"),p(te,"class","txt-hint"),p(oe,"class","filter-op svelte-1w7s5nw"),p($e,"class","txt-hint"),p(fe,"class","filter-op svelte-1w7s5nw"),p(ye,"class","txt-hint"),p(ze,"class","filter-op svelte-1w7s5nw"),p(re,"class","txt-hint")},m(qe,Je){w(qe,e,Je),w(qe,t,Je),w(qe,i,Je),m(i,o),m(i,r),m(i,l),m(l,s),m(l,a),m(l,f),m(l,c),m(l,u),m(u,d),m(d,h),m(d,b),m(d,v),m(u,_),m(u,y),m(y,S),m(y,C),m(y,x),m(u,M),m(u,A),m(A,O),m(A,D),m(A,E),m(u,P),m(u,I),m(I,R),m(I,G),m(I,U),m(u,z),m(u,K),m(K,Y),m(K,W),m(K,te),m(u,ce),m(u,ve),m(ve,oe),m(ve,J),m(ve,$e),m(u,ee),m(u,_e),m(_e,fe),m(_e,ie),m(_e,ye),m(u,Ne),m(u,Pe),m(Pe,ze),m(Pe,se),m(Pe,re),w(qe,ke,Je),w(qe,He,Je)},p:le,i:le,o:le,d(qe){qe&&k(e),qe&&k(t),qe&&k(i),qe&&k(ke),qe&&k(He)}}}class Xx extends Ie{constructor(e){super(),Le(this,e,null,Zx,Ee,{})}}function fh(n,e,t){const i=n.slice();return i[8]=e[t],i}function ch(n,e,t){const i=n.slice();return i[8]=e[t],i}function uh(n,e,t){const i=n.slice();return i[13]=e[t],i}function dh(n,e,t){const i=n.slice();return i[13]=e[t],i}function ph(n){let e;return{c(){e=g("p"),e.innerHTML="Requires Authorization: Admin TOKEN header",p(e,"class","txt-hint txt-sm txt-right")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function hh(n,e){let t,i=e[13].lang+"",o,r,l,s;function a(){return e[6](e[13])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[3]===e[13].lang),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&16&&i!==(i=e[13].lang+"")&&ge(o,i),c&24&&ne(t,"active",e[3]===e[13].lang)},d(f){f&&k(t),l=!1,s()}}}function mh(n,e){let t,i,o,r;return i=new tn({props:{content:e[13].code}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[3]===e[13].lang),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&16&&(a.content=e[13].code),i.$set(a),s&24&&ne(t,"active",e[3]===e[13].lang)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function bh(n,e){let t,i=e[8].code+"",o,r,l,s;function a(){return e[7](e[8])}return{key:n,first:null,c(){t=g("div"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[8].code),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&36&&ne(t,"active",e[2]===e[8].code)},d(f){f&&k(t),l=!1,s()}}}function gh(n,e){let t,i,o,r;return i=new tn({props:{content:e[8].body}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[8].code),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l,s&36&&ne(t,"active",e[2]===e[8].code)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function Qx(n){let e,t,i,o,r,l,s,a=n[0].name+"",f,c,u,d,h,b,v,_,y=n[0].name+"",S,C,x,M,A,O,D,E=[],P=new Map,I,R,G=[],U=new Map,z,K,Y,W,te,ce,ve,oe,J,$e,ee,_e,fe,ie,ye,Ne,Pe,ze,se,re,ke,He,qe,Je,be,Oe,Z,ae,Ve,yt,it,bt,at,vt,qt,Mt,$t,me,Ce,Ye,ot,cn,ue,we,Ze,Kt,zt,ni,pe,L,N,Q,de,Te,Ue,tt,Ge,nt=[],Ke=new Map,et,gt,Ft=[],nn=new Map,Fn,Vt=n[1]&&ph(),wo=n[4];const So=xe=>xe[13].lang;for(let xe=0;xexe[13].lang;for(let xe=0;xe'2022-01-01') + `}}),Mt=new Xx({}),Ze=new tn({props:{content:` + ?expand=rel1,rel2.subrel21.subrel22 + `}});let Co=n[5];const il=xe=>xe[8].code;for(let xe=0;xexe[8].code;for(let xe=0;xeParam + Type + Description`,ce=$(),ve=g("tbody"),oe=g("tr"),oe.innerHTML=`page + Number + The page (aka. offset) of the paginated list (default to 1).`,J=$(),$e=g("tr"),$e.innerHTML=`perPage + Number + Specify the max returned records per page (default to 30).`,ee=$(),_e=g("tr"),fe=g("td"),fe.textContent="sort",ie=$(),ye=g("td"),ye.innerHTML='String',Ne=$(),Pe=g("td"),ze=j("Specify the records order attribute(s). "),se=g("br"),re=j(` + Add `),ke=g("code"),ke.textContent="-",He=j(" / "),qe=g("code"),qe.textContent="+",Je=j(` (default) in front of the attribute for DESC / ASC order. + Ex.: + `),V(be.$$.fragment),Oe=$(),Z=g("tr"),ae=g("td"),ae.textContent="filter",Ve=$(),yt=g("td"),yt.innerHTML='String',it=$(),bt=g("td"),at=j(`Filter the returned records. Ex.: + `),V(vt.$$.fragment),qt=$(),V(Mt.$$.fragment),$t=$(),me=g("tr"),Ce=g("td"),Ce.textContent="expand",Ye=$(),ot=g("td"),ot.innerHTML='String',cn=$(),ue=g("td"),we=j(`Auto expand nested record relations. Ex.: + `),V(Ze.$$.fragment),Kt=j(` + Supports up to 6-levels depth nested relations expansion. `),zt=g("br"),ni=j(` + The expanded relations will be appended to each individual record under the + `),pe=g("code"),pe.textContent="@expand",L=j(" property (eg. "),N=g("code"),N.textContent='"@expand": {"rel1": {...}, ...}',Q=j(")."),de=$(),Te=g("div"),Te.textContent="Responses",Ue=$(),tt=g("div"),Ge=g("div");for(let xe=0;xet(3,l=u.lang),c=u=>t(2,r=u.code);return n.$$set=u=>{"collection"in u&&t(0,o=u.collection)},n.$$.update=()=>{n.$$.dirty&1&&t(1,i=(o==null?void 0:o.listRule)===null),n.$$.dirty&3&&o!=null&&o.id&&(s.push({code:200,body:JSON.stringify({page:1,perPage:30,totalItems:2,items:[B.dummyCollectionRecord(o),B.dummyCollectionRecord(o)]},null,2)}),s.push({code:400,body:` + { + "code": 400, + "message": "Something went wrong while processing your request. Invalid filter.", + "data": {} + } + `}),i&&s.push({code:403,body:` + { + "code": 403, + "message": "Only admins can access this action.", + "data": {} + } + `}),s.push({code:404,body:` + { + "code": 404, + "message": "The requested resource wasn't found.", + "data": {} + } + `})),n.$$.dirty&1&&t(4,a=[{lang:"JavaScript",code:` + import PocketBase from 'pocketbase'; + + const client = new PocketBase("${Se.baseUrl}"); + + client.Records.getList("${o==null?void 0:o.name}", { page: 2 }) + .then(function (list) { + // success... + }).catch(function (error) { + // error... + }); + + // alternatively you can also fetch all records at once via getFullList: + client.Records.getFullList("${o==null?void 0:o.name}", 200 /* batch size */); + .then(function (records) { + // success... + }).catch(function (error) { + // error... + }); + `}])},[o,i,r,l,a,s,f,c]}class t6 extends Ie{constructor(e){super(),Le(this,e,e6,Qx,Ee,{collection:0})}}function _h(n,e,t){const i=n.slice();return i[8]=e[t],i}function vh(n,e,t){const i=n.slice();return i[8]=e[t],i}function yh(n,e,t){const i=n.slice();return i[13]=e[t],i}function kh(n,e,t){const i=n.slice();return i[13]=e[t],i}function wh(n){let e;return{c(){e=g("p"),e.innerHTML="Requires Authorization: Admin TOKEN header",p(e,"class","txt-hint txt-sm txt-right")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Sh(n,e){let t,i=e[13].lang+"",o,r,l,s;function a(){return e[6](e[13])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[3]===e[13].lang),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&16&&i!==(i=e[13].lang+"")&&ge(o,i),c&24&&ne(t,"active",e[3]===e[13].lang)},d(f){f&&k(t),l=!1,s()}}}function Ch(n,e){let t,i,o,r;return i=new tn({props:{content:e[13].code}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[3]===e[13].lang),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&16&&(a.content=e[13].code),i.$set(a),s&24&&ne(t,"active",e[3]===e[13].lang)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function xh(n,e){let t,i=e[8].code+"",o,r,l,s;function a(){return e[7](e[8])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[8].code),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&36&&ne(t,"active",e[2]===e[8].code)},d(f){f&&k(t),l=!1,s()}}}function Mh(n,e){let t,i,o,r;return i=new tn({props:{content:e[8].body}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[8].code),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l,s&36&&ne(t,"active",e[2]===e[8].code)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function n6(n){let e,t,i,o,r,l,s,a=n[0].name+"",f,c,u,d,h,b,v,_,y,S=n[0].name+"",C,x,M,A,O,D,E,P=[],I=new Map,R,G,U=[],z=new Map,K,Y,W,te,ce,ve,oe,J,$e,ee,_e,fe,ie,ye,Ne,Pe,ze,se,re,ke,He,qe,Je,be,Oe,Z,ae,Ve,yt,it,bt,at=[],vt=new Map,qt,Mt,$t=[],me=new Map,Ce,Ye=n[1]&&wh(),ot=n[4];const cn=pe=>pe[13].lang;for(let pe=0;pepe[13].lang;for(let pe=0;pepe[8].code;for(let pe=0;pepe[8].code;for(let pe=0;peParam + Type + Description + id + String + ID of the record to view.`,ce=$(),ve=g("div"),ve.textContent="Query parameters",oe=$(),J=g("table"),$e=g("thead"),$e.innerHTML=`Param + Type + Description`,ee=$(),_e=g("tbody"),fe=g("tr"),ie=g("td"),ie.textContent="expand",ye=$(),Ne=g("td"),Ne.innerHTML='String',Pe=$(),ze=g("td"),se=j(`Auto expand nested record relations. Ex.: + `),V(re.$$.fragment),ke=j(` + Supports up to 6-levels depth nested relations expansion. `),He=g("br"),qe=j(` + The expanded relations will be appended to the record under the + `),Je=g("code"),Je.textContent="@expand",be=j(" property (eg. "),Oe=g("code"),Oe.textContent='"@expand": {"rel1": {...}, ...}',Z=j(")."),ae=$(),Ve=g("div"),Ve.textContent="Responses",yt=$(),it=g("div"),bt=g("div");for(let pe=0;pet(3,l=u.lang),c=u=>t(2,r=u.code);return n.$$set=u=>{"collection"in u&&t(0,o=u.collection)},n.$$.update=()=>{n.$$.dirty&1&&t(1,i=(o==null?void 0:o.viewRule)===null),n.$$.dirty&3&&o!=null&&o.id&&(s.push({code:200,body:JSON.stringify(B.dummyCollectionRecord(o),null,2)}),i&&s.push({code:403,body:` + { + "code": 403, + "message": "Only admins can access this action.", + "data": {} + } + `}),s.push({code:404,body:` + { + "code": 404, + "message": "The requested resource wasn't found.", + "data": {} + } + `})),n.$$.dirty&1&&t(4,a=[{lang:"JavaScript",code:` + import PocketBase from 'pocketbase'; + + const client = new PocketBase("${Se.baseUrl}"); + + client.Records.getOne("${o==null?void 0:o.name}", "RECORD_ID") + .then(function (record) { + // success... + }).catch(function (error) { + // error... + }); + `}])},[o,i,r,l,a,s,f,c]}class o6 extends Ie{constructor(e){super(),Le(this,e,i6,n6,Ee,{collection:0})}}function $h(n,e,t){const i=n.slice();return i[8]=e[t],i}function Ah(n,e,t){const i=n.slice();return i[8]=e[t],i}function Dh(n,e,t){const i=n.slice();return i[13]=e[t],i}function Oh(n,e,t){const i=n.slice();return i[16]=e[t],i}function Th(n,e,t){const i=n.slice();return i[16]=e[t],i}function Eh(n){let e;return{c(){e=g("p"),e.innerHTML="Requires Authorization: Admin TOKEN header",p(e,"class","txt-hint txt-sm txt-right")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Ph(n,e){let t,i=e[16].lang+"",o,r,l,s;function a(){return e[6](e[16])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[16].lang),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&16&&i!==(i=e[16].lang+"")&&ge(o,i),c&20&&ne(t,"active",e[2]===e[16].lang)},d(f){f&&k(t),l=!1,s()}}}function Fh(n,e){let t,i,o,r;return i=new tn({props:{content:e[16].code}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[16].lang),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&16&&(a.content=e[16].code),i.$set(a),s&20&&ne(t,"active",e[2]===e[16].lang)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function r6(n){let e;return{c(){e=g("span"),e.textContent="Optional",p(e,"class","label label-warning")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function l6(n){let e;return{c(){e=g("span"),e.textContent="Required",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function s6(n){var r;let e,t=((r=n[13].options)==null?void 0:r.maxSelect)>1?"ids":"id",i,o;return{c(){e=j("User "),i=j(t),o=j(".")},m(l,s){w(l,e,s),w(l,i,s),w(l,o,s)},p(l,s){var a;s&1&&t!==(t=((a=l[13].options)==null?void 0:a.maxSelect)>1?"ids":"id")&&ge(i,t)},d(l){l&&k(e),l&&k(i),l&&k(o)}}}function a6(n){var r;let e,t=((r=n[13].options)==null?void 0:r.maxSelect)>1?"ids":"id",i,o;return{c(){e=j("Relation record "),i=j(t),o=j(".")},m(l,s){w(l,e,s),w(l,i,s),w(l,o,s)},p(l,s){var a;s&1&&t!==(t=((a=l[13].options)==null?void 0:a.maxSelect)>1?"ids":"id")&&ge(i,t)},d(l){l&&k(e),l&&k(i),l&&k(o)}}}function f6(n){let e,t,i,o,r;return{c(){e=j("FormData object."),t=g("br"),i=j(` + Set to `),o=g("code"),o.textContent="null",r=j(" to delete already uploaded file(s).")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s),w(l,o,s),w(l,r,s)},p:le,d(l){l&&k(e),l&&k(t),l&&k(i),l&&k(o),l&&k(r)}}}function c6(n){let e;return{c(){e=j("URL address.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function u6(n){let e;return{c(){e=j("Email address.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function d6(n){let e;return{c(){e=j("JSON array or object.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function p6(n){let e;return{c(){e=j("Number value.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function h6(n){let e;return{c(){e=j("Plain text value.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function Lh(n,e){let t,i,o,r,l,s=e[13].name+"",a,f,c,u,d=B.getFieldValueType(e[13])+"",h,b,v,_;function y(O,D){return O[13].required?l6:r6}let S=y(e),C=S(e);function x(O,D){if(O[13].type==="text")return h6;if(O[13].type==="number")return p6;if(O[13].type==="json")return d6;if(O[13].type==="email")return u6;if(O[13].type==="url")return c6;if(O[13].type==="file")return f6;if(O[13].type==="relation")return a6;if(O[13].type==="user")return s6}let M=x(e),A=M&&M(e);return{key:n,first:null,c(){t=g("tr"),i=g("td"),o=g("div"),C.c(),r=$(),l=g("span"),a=j(s),f=$(),c=g("td"),u=g("span"),h=j(d),b=$(),v=g("td"),A&&A.c(),_=$(),p(o,"class","inline-flex"),p(u,"class","label"),this.first=t},m(O,D){w(O,t,D),m(t,i),m(i,o),C.m(o,null),m(o,r),m(o,l),m(l,a),m(t,f),m(t,c),m(c,u),m(u,h),m(t,b),m(t,v),A&&A.m(v,null),m(t,_)},p(O,D){e=O,S!==(S=y(e))&&(C.d(1),C=S(e),C&&(C.c(),C.m(o,r))),D&1&&s!==(s=e[13].name+"")&&ge(a,s),D&1&&d!==(d=B.getFieldValueType(e[13])+"")&&ge(h,d),M===(M=x(e))&&A?A.p(e,D):(A&&A.d(1),A=M&&M(e),A&&(A.c(),A.m(v,null)))},d(O){O&&k(t),C.d(),A&&A.d()}}}function Ih(n,e){let t,i=e[8].code+"",o,r,l,s;function a(){return e[7](e[8])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[1]===e[8].code),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&8&&i!==(i=e[8].code+"")&&ge(o,i),c&10&&ne(t,"active",e[1]===e[8].code)},d(f){f&&k(t),l=!1,s()}}}function Rh(n,e){let t,i,o,r;return i=new tn({props:{content:e[8].body}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[1]===e[8].code),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&8&&(a.content=e[8].body),i.$set(a),s&10&&ne(t,"active",e[1]===e[8].code)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function m6(n){var $t;let e,t,i,o,r,l,s,a=n[0].name+"",f,c,u,d,h,b,v,_,y=n[0].name+"",S,C,x,M,A,O,D,E,P,I,R,G=[],U=new Map,z,K,Y=[],W=new Map,te,ce,ve,oe,J,$e,ee,_e=[],fe=new Map,ie,ye,Ne,Pe,ze,se=[],re=new Map,ke,He,qe=[],Je=new Map,be,Oe=n[5]&&Eh(),Z=n[4];const ae=me=>me[16].lang;for(let me=0;meme[16].lang;for(let me=0;meme[13].name;for(let me=0;meme[8].code;for(let me=0;meme[8].code;for(let me=0;meapplication/json or + multipart/form-data.`,A=$(),O=g("p"),O.innerHTML="File upload is supported only via multipart/form-data.",D=$(),E=g("div"),E.textContent="Client SDKs example",P=$(),I=g("div"),R=g("div");for(let me=0;meParam + Type + Description`,$e=$(),ee=g("tbody");for(let me=0;me<_e.length;me+=1)_e[me].c();ie=$(),ye=g("div"),ye.textContent="Responses",Ne=$(),Pe=g("div"),ze=g("div");for(let me=0;met(2,l=u.lang),c=u=>t(1,r=u.code);return n.$$set=u=>{"collection"in u&&t(0,o=u.collection)},n.$$.update=()=>{var u,d;n.$$.dirty&1&&t(5,i=(o==null?void 0:o.createRule)===null),n.$$.dirty&1&&t(3,s=[{code:200,body:JSON.stringify(B.dummyCollectionRecord(o),null,2)},{code:400,body:` + { + "code": 400, + "message": "Failed to create record.", + "data": { + "${(d=(u=o==null?void 0:o.schema)==null?void 0:u[0])==null?void 0:d.name}": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `},{code:403,body:` + { + "code": 403, + "message": "You are not allowed to perform this request.", + "data": {} + } + `}]),n.$$.dirty&1&&t(4,a=[{lang:"JavaScript",code:` + import PocketBase from 'pocketbase'; + + const client = new PocketBase("${Se.baseUrl}"); + + const data = { ... }; + + client.Records.create("${o==null?void 0:o.name}", data) + .then(function (record) { + // success... + }).catch(function (error) { + // error... + }); + `}])},[o,r,l,s,a,i,f,c]}class g6 extends Ie{constructor(e){super(),Le(this,e,b6,m6,Ee,{collection:0})}}function Nh(n,e,t){const i=n.slice();return i[8]=e[t],i}function jh(n,e,t){const i=n.slice();return i[8]=e[t],i}function zh(n,e,t){const i=n.slice();return i[13]=e[t],i}function Hh(n,e,t){const i=n.slice();return i[16]=e[t],i}function qh(n,e,t){const i=n.slice();return i[16]=e[t],i}function Vh(n){let e;return{c(){e=g("p"),e.innerHTML="Requires Authorization: Admin TOKEN header",p(e,"class","txt-hint txt-sm txt-right")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function Bh(n,e){let t,i=e[16].lang+"",o,r,l,s;function a(){return e[6](e[16])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[16].lang),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&16&&i!==(i=e[16].lang+"")&&ge(o,i),c&20&&ne(t,"active",e[2]===e[16].lang)},d(f){f&&k(t),l=!1,s()}}}function Uh(n,e){let t,i,o,r;return i=new tn({props:{content:e[16].code}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[16].lang),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&16&&(a.content=e[16].code),i.$set(a),s&20&&ne(t,"active",e[2]===e[16].lang)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function _6(n){let e;return{c(){e=g("span"),e.textContent="Optional",p(e,"class","label label-warning")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function v6(n){let e;return{c(){e=g("span"),e.textContent="Required",p(e,"class","label label-success")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function y6(n){var r;let e,t=((r=n[13].options)==null?void 0:r.maxSelect)>1?"ids":"id",i,o;return{c(){e=j("User "),i=j(t),o=j(".")},m(l,s){w(l,e,s),w(l,i,s),w(l,o,s)},p(l,s){var a;s&1&&t!==(t=((a=l[13].options)==null?void 0:a.maxSelect)>1?"ids":"id")&&ge(i,t)},d(l){l&&k(e),l&&k(i),l&&k(o)}}}function k6(n){var r;let e,t=((r=n[13].options)==null?void 0:r.maxSelect)>1?"ids":"id",i,o;return{c(){e=j("Relation record "),i=j(t),o=j(".")},m(l,s){w(l,e,s),w(l,i,s),w(l,o,s)},p(l,s){var a;s&1&&t!==(t=((a=l[13].options)==null?void 0:a.maxSelect)>1?"ids":"id")&&ge(i,t)},d(l){l&&k(e),l&&k(i),l&&k(o)}}}function w6(n){let e,t,i,o,r;return{c(){e=j("FormData object."),t=g("br"),i=j(` + Set to `),o=g("code"),o.textContent="null",r=j(" to delete already uploaded file(s).")},m(l,s){w(l,e,s),w(l,t,s),w(l,i,s),w(l,o,s),w(l,r,s)},p:le,d(l){l&&k(e),l&&k(t),l&&k(i),l&&k(o),l&&k(r)}}}function S6(n){let e;return{c(){e=j("URL address.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function C6(n){let e;return{c(){e=j("Email address.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function x6(n){let e;return{c(){e=j("JSON array or object.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function M6(n){let e;return{c(){e=j("Number value.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function $6(n){let e;return{c(){e=j("Plain text value.")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function Wh(n,e){let t,i,o,r,l,s=e[13].name+"",a,f,c,u,d=B.getFieldValueType(e[13])+"",h,b,v,_;function y(O,D){return O[13].required?v6:_6}let S=y(e),C=S(e);function x(O,D){if(O[13].type==="text")return $6;if(O[13].type==="number")return M6;if(O[13].type==="json")return x6;if(O[13].type==="email")return C6;if(O[13].type==="url")return S6;if(O[13].type==="file")return w6;if(O[13].type==="relation")return k6;if(O[13].type==="user")return y6}let M=x(e),A=M&&M(e);return{key:n,first:null,c(){t=g("tr"),i=g("td"),o=g("div"),C.c(),r=$(),l=g("span"),a=j(s),f=$(),c=g("td"),u=g("span"),h=j(d),b=$(),v=g("td"),A&&A.c(),_=$(),p(o,"class","inline-flex"),p(u,"class","label"),this.first=t},m(O,D){w(O,t,D),m(t,i),m(i,o),C.m(o,null),m(o,r),m(o,l),m(l,a),m(t,f),m(t,c),m(c,u),m(u,h),m(t,b),m(t,v),A&&A.m(v,null),m(t,_)},p(O,D){e=O,S!==(S=y(e))&&(C.d(1),C=S(e),C&&(C.c(),C.m(o,r))),D&1&&s!==(s=e[13].name+"")&&ge(a,s),D&1&&d!==(d=B.getFieldValueType(e[13])+"")&&ge(h,d),M===(M=x(e))&&A?A.p(e,D):(A&&A.d(1),A=M&&M(e),A&&(A.c(),A.m(v,null)))},d(O){O&&k(t),C.d(),A&&A.d()}}}function Yh(n,e){let t,i=e[8].code+"",o,r,l,s;function a(){return e[7](e[8])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[1]===e[8].code),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&8&&i!==(i=e[8].code+"")&&ge(o,i),c&10&&ne(t,"active",e[1]===e[8].code)},d(f){f&&k(t),l=!1,s()}}}function Gh(n,e){let t,i,o,r;return i=new tn({props:{content:e[8].body}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[1]===e[8].code),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&8&&(a.content=e[8].body),i.$set(a),s&10&&ne(t,"active",e[1]===e[8].code)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function A6(n){var cn;let e,t,i,o,r,l,s,a=n[0].name+"",f,c,u,d,h,b,v,_,y,S=n[0].name+"",C,x,M,A,O,D,E,P,I,R,G,U=[],z=new Map,K,Y,W=[],te=new Map,ce,ve,oe,J,$e,ee,_e,fe,ie,ye,Ne,Pe=[],ze=new Map,se,re,ke,He,qe,Je=[],be=new Map,Oe,Z,ae=[],Ve=new Map,yt,it=n[5]&&Vh(),bt=n[4];const at=ue=>ue[16].lang;for(let ue=0;ueue[16].lang;for(let ue=0;ueue[13].name;for(let ue=0;ueue[8].code;for(let ue=0;ueue[8].code;for(let ue=0;ueapplication/json or + multipart/form-data.`,O=$(),D=g("p"),D.innerHTML="File upload is supported only via multipart/form-data.",E=$(),P=g("div"),P.textContent="Client SDKs example",I=$(),R=g("div"),G=g("div");for(let ue=0;ueParam + Type + Description + id + String + ID of the record to update.`,$e=$(),ee=g("div"),ee.textContent="Body Parameters",_e=$(),fe=g("table"),ie=g("thead"),ie.innerHTML=`Param + Type + Description`,ye=$(),Ne=g("tbody");for(let ue=0;uet(2,l=u.lang),c=u=>t(1,r=u.code);return n.$$set=u=>{"collection"in u&&t(0,o=u.collection)},n.$$.update=()=>{var u,d;n.$$.dirty&1&&t(5,i=(o==null?void 0:o.updateRule)===null),n.$$.dirty&1&&t(3,s=[{code:200,body:JSON.stringify(B.dummyCollectionRecord(o),null,2)},{code:400,body:` + { + "code": 400, + "message": "Failed to update record.", + "data": { + "${(d=(u=o==null?void 0:o.schema)==null?void 0:u[0])==null?void 0:d.name}": { + "code": "validation_required", + "message": "Missing required value." + } + } + } + `},{code:403,body:` + { + "code": 403, + "message": "You are not allowed to perform this request.", + "data": {} + } + `},{code:404,body:` + { + "code": 404, + "message": "The requested resource wasn't found.", + "data": {} + } + `}]),n.$$.dirty&1&&t(4,a=[{lang:"JavaScript",code:` + import PocketBase from 'pocketbase'; + + const client = new PocketBase("${Se.baseUrl}"); + + const data = { ... }; + + client.Records.update("${o==null?void 0:o.name}", "RECORD_ID", data) + .then(function (record) { + // success... + }).catch(function (error) { + // error... + }); + `}])},[o,r,l,s,a,i,f,c]}class O6 extends Ie{constructor(e){super(),Le(this,e,D6,A6,Ee,{collection:0})}}function Kh(n,e,t){const i=n.slice();return i[8]=e[t],i}function Jh(n,e,t){const i=n.slice();return i[8]=e[t],i}function Zh(n,e,t){const i=n.slice();return i[13]=e[t],i}function Xh(n,e,t){const i=n.slice();return i[13]=e[t],i}function Qh(n){let e;return{c(){e=g("p"),e.innerHTML="Requires Authorization: Admin TOKEN header",p(e,"class","txt-hint txt-sm txt-right")},m(t,i){w(t,e,i)},d(t){t&&k(e)}}}function em(n,e){let t,i=e[13].lang+"",o,r,l,s;function a(){return e[6](e[13])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[3]===e[13].lang),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&16&&i!==(i=e[13].lang+"")&&ge(o,i),c&24&&ne(t,"active",e[3]===e[13].lang)},d(f){f&&k(t),l=!1,s()}}}function tm(n,e){let t,i,o,r;return i=new tn({props:{content:e[13].code}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[3]===e[13].lang),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&16&&(a.content=e[13].code),i.$set(a),s&24&&ne(t,"active",e[3]===e[13].lang)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function nm(n,e){let t,i=e[8].code+"",o,r,l,s;function a(){return e[7](e[8])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[8].code),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&36&&ne(t,"active",e[2]===e[8].code)},d(f){f&&k(t),l=!1,s()}}}function im(n,e){let t,i,o,r;return i=new tn({props:{content:e[8].body}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[2]===e[8].code),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l,s&36&&ne(t,"active",e[2]===e[8].code)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function T6(n){let e,t,i,o,r,l,s,a=n[0].name+"",f,c,u,d,h,b,v,_,y,S=n[0].name+"",C,x,M,A,O,D,E,P=[],I=new Map,R,G,U=[],z=new Map,K,Y,W,te,ce,ve,oe,J,$e,ee=[],_e=new Map,fe,ie,ye=[],Ne=new Map,Pe,ze=n[1]&&Qh(),se=n[4];const re=Z=>Z[13].lang;for(let Z=0;ZZ[13].lang;for(let Z=0;ZZ[8].code;for(let Z=0;ZZ[8].code;for(let Z=0;ZParam + Type + Description + id + String + ID of the record to delete.`,ce=$(),ve=g("div"),ve.textContent="Responses",oe=$(),J=g("div"),$e=g("div");for(let Z=0;Zt(3,l=u.lang),c=u=>t(2,r=u.code);return n.$$set=u=>{"collection"in u&&t(0,o=u.collection)},n.$$.update=()=>{n.$$.dirty&1&&t(1,i=(o==null?void 0:o.deleteRule)===null),n.$$.dirty&3&&o!=null&&o.id&&(s.push({code:204,body:` + null + `}),s.push({code:400,body:` + { + "code": 400, + "message": "Failed to delete record. Make sure that the record is not part of a required relation reference.", + "data": {} + } + `}),i&&s.push({code:403,body:` + { + "code": 403, + "message": "Only admins can access this action.", + "data": {} + } + `}),s.push({code:404,body:` + { + "code": 404, + "message": "The requested resource wasn't found.", + "data": {} + } + `})),n.$$.dirty&1&&t(4,a=[{lang:"JavaScript",code:` + import PocketBase from 'pocketbase'; + + const client = new PocketBase("${Se.baseUrl}"); + + client.Records.delete("${o==null?void 0:o.name}", "RECORD_ID") + .then(function () { + // success... + }).catch(function (error) { + // error... + }); + `}])},[o,i,r,l,a,s,f,c]}class P6 extends Ie{constructor(e){super(),Le(this,e,E6,T6,Ee,{collection:0})}}function om(n,e,t){const i=n.slice();return i[4]=e[t],i}function rm(n,e,t){const i=n.slice();return i[4]=e[t],i}function lm(n,e){let t,i=e[4].lang+"",o,r,l,s;function a(){return e[3](e[4])}return{key:n,first:null,c(){t=g("button"),o=j(i),r=$(),p(t,"class","tab-item"),ne(t,"active",e[1]===e[4].lang),this.first=t},m(f,c){w(f,t,c),m(t,o),m(t,r),l||(s=X(t,"click",a),l=!0)},p(f,c){e=f,c&4&&i!==(i=e[4].lang+"")&&ge(o,i),c&6&&ne(t,"active",e[1]===e[4].lang)},d(f){f&&k(t),l=!1,s()}}}function sm(n,e){let t,i,o,r;return i=new tn({props:{content:e[4].code}}),{key:n,first:null,c(){t=g("div"),V(i.$$.fragment),o=$(),p(t,"class","tab-item"),ne(t,"active",e[1]===e[4].lang),this.first=t},m(l,s){w(l,t,s),H(i,t,null),m(t,o),r=!0},p(l,s){e=l;const a={};s&4&&(a.content=e[4].code),i.$set(a),s&6&&ne(t,"active",e[1]===e[4].lang)},i(l){r||(T(i.$$.fragment,l),r=!0)},o(l){F(i.$$.fragment,l),r=!1},d(l){l&&k(t),q(i)}}}function F6(n){let e,t,i,o,r,l,s,a,f=[],c=new Map,u,d,h=[],b=new Map,v,_,y,S,C,x=n[2];const M=D=>D[4].lang;for(let D=0;DD[4].lang;for(let D=0;DSSE +

    /api/realtime

    `,t=$(),i=g("div"),i.innerHTML=`

    Subscribe to realtime changes via Server-Sent Events (SSE).

    +

    Events are send for create, update + and delete record operations (see "Event data format" section below).

    +
    +

    You could subscribe to a single record or to an entire collection.

    +

    When you subscribe to a single record, the collection's + ViewRule will be used to determine whether the subscriber has access to receive + the event message.

    +

    When you subscribe to an entire collection, the collection's + ListRule will be used to determine whether the subscriber has access to receive + the event message.

    `,o=$(),r=g("div"),r.textContent="Client SDKs example",l=$(),s=g("div"),a=g("div");for(let D=0;Dt(1,o=s.lang);return n.$$set=s=>{"collection"in s&&t(0,i=s.collection)},n.$$.update=()=>{n.$$.dirty&1&&t(2,r=[{lang:"JavaScript",code:` + import PocketBase from 'pocketbase'; + + const client = new PocketBase("${Se.baseUrl}"); + + // (Optionally) authenticate + client.Users.authViaEmail("test@example.com", "123456"); + + // Subscribe to changes in any record from the collection + client.Realtime.subscribe("${i==null?void 0:i.name}", function (e) { + console.log(e.data); + }); + + // Subscribe to changes in a single record + client.Realtime.subscribe("${i==null?void 0:i.name}/RECORD_ID", function (e) { + console.log(e.data); + }); + + // Unsubscribe + client.Realtime.unsubscribe() // remove all subscriptions + client.Realtime.unsubscribe("${i==null?void 0:i.name}") // remove the collection subscription + client.Realtime.unsubscribe("${i==null?void 0:i.name}/RECORD_ID") // remove the record subscription + `}])},[i,o,r,l]}class I6 extends Ie{constructor(e){super(),Le(this,e,L6,F6,Ee,{collection:0})}}function am(n,e,t){const i=n.slice();return i[14]=e[t],i}function fm(n,e,t){const i=n.slice();return i[14]=e[t],i}function cm(n){let e,t,i,o;var r=n[14].component;function l(s){return{props:{collection:s[3]}}}return r&&(t=new r(l(n))),{c(){e=g("div"),t&&V(t.$$.fragment),i=$(),p(e,"class","tab-item active")},m(s,a){w(s,e,a),t&&H(t,e,null),m(e,i),o=!0},p(s,a){const f={};if(a&8&&(f.collection=s[3]),r!==(r=s[14].component)){if(t){Ae();const c=t;F(c.$$.fragment,1,0,()=>{q(c,1)}),De()}r?(t=new r(l(s)),V(t.$$.fragment),T(t.$$.fragment,1),H(t,e,i)):t=null}else r&&t.$set(f)},i(s){o||(t&&T(t.$$.fragment,s),o=!0)},o(s){t&&F(t.$$.fragment,s),o=!1},d(s){s&&k(e),t&&q(t)}}}function um(n,e){let t,i,o,r=e[4]===e[14].id&&cm(e);return{key:n,first:null,c(){t=lt(),r&&r.c(),i=lt(),this.first=t},m(l,s){w(l,t,s),r&&r.m(l,s),w(l,i,s),o=!0},p(l,s){e=l,e[4]===e[14].id?r?(r.p(e,s),s&16&&T(r,1)):(r=cm(e),r.c(),T(r,1),r.m(i.parentNode,i)):r&&(Ae(),F(r,1,1,()=>{r=null}),De())},i(l){o||(T(r),o=!0)},o(l){F(r),o=!1},d(l){l&&k(t),r&&r.d(l),l&&k(i)}}}function R6(n){let e,t=[],i=new Map,o,r=n[5];const l=s=>s[14].id;for(let s=0;sd[14].id;for(let d=0;dClose',p(e,"type","button"),p(e,"class","btn btn-secondary")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[8]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function z6(n){let e,t,i={class:"overlay-panel-xl colored-header collection-panel",$$slots:{footer:[j6],header:[N6],default:[R6]},$$scope:{ctx:n}};return e=new Ai({props:i}),n[11](e),e.$on("hide",n[12]),e.$on("show",n[13]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,[r]){const l={};r&524312&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[11](null),q(e,o)}}}function H6(n,e,t){const i=[{id:"list",label:"List",component:t6},{id:"view",label:"View",component:o6},{id:"create",label:"Create",component:g6},{id:"update",label:"Update",component:O6},{id:"delete",label:"Delete",component:P6},{id:"realtime",label:"Realtime",component:I6}];let o,r=new En,l=i[0].id;function s(y){return t(3,r=y),f(i[0].id),o==null?void 0:o.show()}function a(){return o==null?void 0:o.hide()}function f(y){t(4,l=y)}function c(y,S){(y.code==="Enter"||y.code==="Space")&&(y.preventDefault(),f(S))}const u=()=>a(),d=y=>f(y.id),h=(y,S)=>c(S,y.id);function b(y){he[y?"unshift":"push"](()=>{o=y,t(2,o)})}function v(y){ft.call(this,n,y)}function _(y){ft.call(this,n,y)}return[a,f,o,r,l,i,c,s,u,d,h,b,v,_]}class q6 extends Ie{constructor(e){super(),Le(this,e,H6,z6,Ee,{show:7,hide:0,changeTab:1})}get show(){return this.$$.ctx[7]}get hide(){return this.$$.ctx[0]}get changeTab(){return this.$$.ctx[1]}}function V6(n){let e,t,i,o=[n[3]],r={};for(let l=0;l{s&&(t(1,s.style.height="",s),t(1,s.style.height=Math.min(s.scrollHeight+2,l)+"px",s))},0)}function c(h){if((h==null?void 0:h.code)==="Enter"&&!(h!=null&&h.shiftKey)){h.preventDefault();const b=s.closest("form");b!=null&&b.requestSubmit&&b.requestSubmit()}}function u(h){he[h?"unshift":"push"](()=>{s=h,t(1,s)})}function d(){r=this.value,t(0,r)}return n.$$set=h=>{e=ut(ut({},e),ui(h)),t(3,o=Wt(e,i)),"value"in h&&t(0,r=h.value),"maxHeight"in h&&t(4,l=h.maxHeight)},n.$$.update=()=>{n.$$.dirty&3&&s&&typeof r!==void 0&&f()},[r,s,c,o,l,u,d]}class U6 extends Ie{constructor(e){super(),Le(this,e,B6,V6,Ee,{value:0,maxHeight:4})}}function W6(n){let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d;function h(v){n[2](v)}let b={id:n[3],required:n[1].required};return n[0]!==void 0&&(b.value=n[0]),c=new U6({props:b}),he.push(()=>Fe(c,"value",h)),{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),V(c.$$.fragment),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[3])},m(v,_){w(v,e,_),m(e,t),m(e,o),m(e,r),m(r,s),w(v,f,_),H(c,v,_),d=!0},p(v,_){(!d||_&2&&i!==(i=B.getFieldTypeIcon(v[1].type)))&&p(t,"class",i),(!d||_&2)&&l!==(l=v[1].name+"")&&ge(s,l),(!d||_&8&&a!==(a=v[3]))&&p(e,"for",a);const y={};_&8&&(y.id=v[3]),_&2&&(y.required=v[1].required),!u&&_&1&&(u=!0,y.value=v[0],Re(()=>u=!1)),c.$set(y)},i(v){d||(T(c.$$.fragment,v),d=!0)},o(v){F(c.$$.fragment,v),d=!1},d(v){v&&k(e),v&&k(f),q(c,v)}}}function Y6(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[W6,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&27&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function G6(n,e,t){let{field:i=new kn}=e,{value:o=void 0}=e;function r(l){o=l,t(0,o)}return n.$$set=l=>{"field"in l&&t(1,i=l.field),"value"in l&&t(0,o=l.value)},[o,i,r]}class K6 extends Ie{constructor(e){super(),Le(this,e,G6,Y6,Ee,{field:1,value:0})}}function J6(n){let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h,b,v,_;return{c(){var y,S;e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),c=g("input"),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[3]),p(c,"type","number"),p(c,"id",u=n[3]),c.required=d=n[1].required,p(c,"min",h=(y=n[1].options)==null?void 0:y.min),p(c,"max",b=(S=n[1].options)==null?void 0:S.max)},m(y,S){w(y,e,S),m(e,t),m(e,o),m(e,r),m(r,s),w(y,f,S),w(y,c,S),Me(c,n[0]),v||(_=X(c,"input",n[2]),v=!0)},p(y,S){var C,x;S&2&&i!==(i=B.getFieldTypeIcon(y[1].type))&&p(t,"class",i),S&2&&l!==(l=y[1].name+"")&&ge(s,l),S&8&&a!==(a=y[3])&&p(e,"for",a),S&8&&u!==(u=y[3])&&p(c,"id",u),S&2&&d!==(d=y[1].required)&&(c.required=d),S&2&&h!==(h=(C=y[1].options)==null?void 0:C.min)&&p(c,"min",h),S&2&&b!==(b=(x=y[1].options)==null?void 0:x.max)&&p(c,"max",b),S&1&&At(c.value)!==y[0]&&Me(c,y[0])},d(y){y&&k(e),y&&k(f),y&&k(c),v=!1,_()}}}function Z6(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[J6,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&27&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function X6(n,e,t){let{field:i=new kn}=e,{value:o=void 0}=e;function r(){o=At(this.value),t(0,o)}return n.$$set=l=>{"field"in l&&t(1,i=l.field),"value"in l&&t(0,o=l.value)},[o,i,r]}class Q6 extends Ie{constructor(e){super(),Le(this,e,X6,Z6,Ee,{field:1,value:0})}}function eM(n){let e,t,i,o,r=n[1].name+"",l,s,a,f;return{c(){e=g("input"),i=$(),o=g("label"),l=j(r),p(e,"type","checkbox"),p(e,"id",t=n[3]),p(o,"for",s=n[3])},m(c,u){w(c,e,u),e.checked=n[0],w(c,i,u),w(c,o,u),m(o,l),a||(f=X(e,"change",n[2]),a=!0)},p(c,u){u&8&&t!==(t=c[3])&&p(e,"id",t),u&1&&(e.checked=c[0]),u&2&&r!==(r=c[1].name+"")&&ge(l,r),u&8&&s!==(s=c[3])&&p(o,"for",s)},d(c){c&&k(e),c&&k(i),c&&k(o),a=!1,f()}}}function tM(n){let e,t;return e=new je({props:{class:"form-field form-field-toggle "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[eM,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field form-field-toggle "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&27&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function nM(n,e,t){let{field:i=new kn}=e,{value:o=!1}=e;function r(){o=this.checked,t(0,o)}return n.$$set=l=>{"field"in l&&t(1,i=l.field),"value"in l&&t(0,o=l.value)},[o,i,r]}class iM extends Ie{constructor(e){super(),Le(this,e,nM,tM,Ee,{field:1,value:0})}}function oM(n){let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h,b;return{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),c=g("input"),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[3]),p(c,"type","email"),p(c,"id",u=n[3]),c.required=d=n[1].required},m(v,_){w(v,e,_),m(e,t),m(e,o),m(e,r),m(r,s),w(v,f,_),w(v,c,_),Me(c,n[0]),h||(b=X(c,"input",n[2]),h=!0)},p(v,_){_&2&&i!==(i=B.getFieldTypeIcon(v[1].type))&&p(t,"class",i),_&2&&l!==(l=v[1].name+"")&&ge(s,l),_&8&&a!==(a=v[3])&&p(e,"for",a),_&8&&u!==(u=v[3])&&p(c,"id",u),_&2&&d!==(d=v[1].required)&&(c.required=d),_&1&&c.value!==v[0]&&Me(c,v[0])},d(v){v&&k(e),v&&k(f),v&&k(c),h=!1,b()}}}function rM(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[oM,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&27&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function lM(n,e,t){let{field:i=new kn}=e,{value:o=void 0}=e;function r(){o=this.value,t(0,o)}return n.$$set=l=>{"field"in l&&t(1,i=l.field),"value"in l&&t(0,o=l.value)},[o,i,r]}class sM extends Ie{constructor(e){super(),Le(this,e,lM,rM,Ee,{field:1,value:0})}}function aM(n){let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h,b;return{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),c=g("input"),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[3]),p(c,"type","url"),p(c,"id",u=n[3]),c.required=d=n[1].required},m(v,_){w(v,e,_),m(e,t),m(e,o),m(e,r),m(r,s),w(v,f,_),w(v,c,_),Me(c,n[0]),h||(b=X(c,"input",n[2]),h=!0)},p(v,_){_&2&&i!==(i=B.getFieldTypeIcon(v[1].type))&&p(t,"class",i),_&2&&l!==(l=v[1].name+"")&&ge(s,l),_&8&&a!==(a=v[3])&&p(e,"for",a),_&8&&u!==(u=v[3])&&p(c,"id",u),_&2&&d!==(d=v[1].required)&&(c.required=d),_&1&&Me(c,v[0])},d(v){v&&k(e),v&&k(f),v&&k(c),h=!1,b()}}}function fM(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[aM,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&27&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function cM(n,e,t){let{field:i=new kn}=e,{value:o=void 0}=e;function r(){o=this.value,t(0,o)}return n.$$set=l=>{"field"in l&&t(1,i=l.field),"value"in l&&t(0,o=l.value)},[o,i,r]}class uM extends Ie{constructor(e){super(),Le(this,e,cM,fM,Ee,{field:1,value:0})}}function dM(n){let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h;function b(_){n[2](_)}let v={id:n[3],options:B.defaultFlatpickrOptions(),value:n[0]};return n[0]!==void 0&&(v.formattedValue=n[0]),u=new pc({props:v}),he.push(()=>Fe(u,"formattedValue",b)),{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),a=j(" (UTC)"),c=$(),V(u.$$.fragment),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",f=n[3])},m(_,y){w(_,e,y),m(e,t),m(e,o),m(e,r),m(r,s),m(r,a),w(_,c,y),H(u,_,y),h=!0},p(_,y){(!h||y&2&&i!==(i=B.getFieldTypeIcon(_[1].type)))&&p(t,"class",i),(!h||y&2)&&l!==(l=_[1].name+"")&&ge(s,l),(!h||y&8&&f!==(f=_[3]))&&p(e,"for",f);const S={};y&8&&(S.id=_[3]),y&1&&(S.value=_[0]),!d&&y&1&&(d=!0,S.formattedValue=_[0],Re(()=>d=!1)),u.$set(S)},i(_){h||(T(u.$$.fragment,_),h=!0)},o(_){F(u.$$.fragment,_),h=!1},d(_){_&&k(e),_&&k(c),q(u,_)}}}function pM(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[dM,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&27&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function hM(n,e,t){let{field:i=new kn}=e,{value:o=void 0}=e;function r(l){o=l,t(0,o)}return n.$$set=l=>{"field"in l&&t(1,i=l.field),"value"in l&&t(0,o=l.value)},[o,i,r]}class mM extends Ie{constructor(e){super(),Le(this,e,hM,pM,Ee,{field:1,value:0})}}function pm(n){let e,t,i=n[1].options.maxSelect+"",o,r;return{c(){e=g("div"),t=j("Select up to "),o=j(i),r=j(" items."),p(e,"class","help-block")},m(l,s){w(l,e,s),m(e,t),m(e,o),m(e,r)},p(l,s){s&2&&i!==(i=l[1].options.maxSelect+"")&&ge(o,i)},d(l){l&&k(e)}}}function bM(n){var S,C,x;let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h,b;function v(M){n[3](M)}let _={id:n[4],toggle:!n[1].required||n[2],multiple:n[2],items:(S=n[1].options)==null?void 0:S.values,searchable:((C=n[1].options)==null?void 0:C.values)>5};n[0]!==void 0&&(_.selected=n[0]),c=new D1({props:_}),he.push(()=>Fe(c,"selected",v));let y=((x=n[1].options)==null?void 0:x.maxSelect)>1&&pm(n);return{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),V(c.$$.fragment),d=$(),y&&y.c(),h=lt(),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[4])},m(M,A){w(M,e,A),m(e,t),m(e,o),m(e,r),m(r,s),w(M,f,A),H(c,M,A),w(M,d,A),y&&y.m(M,A),w(M,h,A),b=!0},p(M,A){var D,E,P;(!b||A&2&&i!==(i=B.getFieldTypeIcon(M[1].type)))&&p(t,"class",i),(!b||A&2)&&l!==(l=M[1].name+"")&&ge(s,l),(!b||A&16&&a!==(a=M[4]))&&p(e,"for",a);const O={};A&16&&(O.id=M[4]),A&6&&(O.toggle=!M[1].required||M[2]),A&4&&(O.multiple=M[2]),A&2&&(O.items=(D=M[1].options)==null?void 0:D.values),A&2&&(O.searchable=((E=M[1].options)==null?void 0:E.values)>5),!u&&A&1&&(u=!0,O.selected=M[0],Re(()=>u=!1)),c.$set(O),((P=M[1].options)==null?void 0:P.maxSelect)>1?y?y.p(M,A):(y=pm(M),y.c(),y.m(h.parentNode,h)):y&&(y.d(1),y=null)},i(M){b||(T(c.$$.fragment,M),b=!0)},o(M){F(c.$$.fragment,M),b=!1},d(M){M&&k(e),M&&k(f),q(c,M),M&&k(d),y&&y.d(M),M&&k(h)}}}function gM(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[bM,({uniqueId:i})=>({4:i}),({uniqueId:i})=>i?16:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&55&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function _M(n,e,t){let i,{field:o=new kn}=e,{value:r=void 0}=e;function l(s){r=s,t(0,r),t(2,i),t(1,o)}return n.$$set=s=>{"field"in s&&t(1,o=s.field),"value"in s&&t(0,r=s.value)},n.$$.update=()=>{var s;n.$$.dirty&2&&t(2,i=((s=o.options)==null?void 0:s.maxSelect)>1),n.$$.dirty&5&&typeof r=="undefined"&&t(0,r=i?[]:null),n.$$.dirty&7&&i&&Array.isArray(r)&&r.length>o.options.maxSelect&&t(0,r=r.slice(r.length-o.options.maxSelect))},[r,o,i,l]}class vM extends Ie{constructor(e){super(),Le(this,e,_M,gM,Ee,{field:1,value:0})}}function yM(n){let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h,b;return{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),c=g("textarea"),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[3]),p(c,"id",u=n[3]),c.required=d=n[1].required,p(c,"class","txt-mono txt-sm")},m(v,_){w(v,e,_),m(e,t),m(e,o),m(e,r),m(r,s),w(v,f,_),w(v,c,_),Me(c,n[0]),h||(b=X(c,"input",n[2]),h=!0)},p(v,_){_&2&&i!==(i=B.getFieldTypeIcon(v[1].type))&&p(t,"class",i),_&2&&l!==(l=v[1].name+"")&&ge(s,l),_&8&&a!==(a=v[3])&&p(e,"for",a),_&8&&u!==(u=v[3])&&p(c,"id",u),_&2&&d!==(d=v[1].required)&&(c.required=d),_&1&&Me(c,v[0])},d(v){v&&k(e),v&&k(f),v&&k(c),h=!1,b()}}}function kM(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[yM,({uniqueId:i})=>({3:i}),({uniqueId:i})=>i?8:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&27&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function wM(n,e,t){let{field:i=new kn}=e,{value:o=void 0}=e;function r(){o=this.value,t(0,o)}return n.$$set=l=>{"field"in l&&t(1,i=l.field),"value"in l&&t(0,o=l.value)},n.$$.update=()=>{n.$$.dirty&1&&typeof o!="undefined"&&typeof o!="string"&&o!==null&&t(0,o=JSON.stringify(o,null,2))},[o,i,r]}class SM extends Ie{constructor(e){super(),Le(this,e,wM,kM,Ee,{field:1,value:0})}}function CM(n){let e,t;return{c(){e=g("i"),p(e,"class","ri-file-line"),p(e,"alt",t=n[0].name)},m(i,o){w(i,e,o)},p(i,o){o&1&&t!==(t=i[0].name)&&p(e,"alt",t)},d(i){i&&k(e)}}}function xM(n){let e,t,i;return{c(){e=g("img"),Qn(e.src,t=n[2])||p(e,"src",t),p(e,"width",n[1]),p(e,"height",n[1]),p(e,"alt",i=n[0].name)},m(o,r){w(o,e,r)},p(o,r){r&4&&!Qn(e.src,t=o[2])&&p(e,"src",t),r&2&&p(e,"width",o[1]),r&2&&p(e,"height",o[1]),r&1&&i!==(i=o[0].name)&&p(e,"alt",i)},d(o){o&&k(e)}}}function MM(n){let e;function t(r,l){return r[2]?xM:CM}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,[l]){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},i:le,o:le,d(r){o.d(r),r&&k(e)}}}function $M(n,e,t){let i,{file:o}=e,{size:r=50}=e;function l(){t(2,i=""),B.hasImageExtension(o==null?void 0:o.name)&&B.generateThumb(o,r,r).then(s=>{t(2,i=s)}).catch(s=>{console.warn("Unable to generate thumb: ",s)})}return n.$$set=s=>{"file"in s&&t(0,o=s.file),"size"in s&&t(1,r=s.size)},n.$$.update=()=>{n.$$.dirty&1&&typeof o!="undefined"&&l()},t(2,i=""),[o,r,i]}class AM extends Ie{constructor(e){super(),Le(this,e,$M,MM,Ee,{file:0,size:1})}}function DM(n){let e,t;return{c(){e=g("img"),Qn(e.src,t=n[2])||p(e,"src",t),p(e,"alt","Preview")},m(i,o){w(i,e,o)},p(i,o){o&4&&!Qn(e.src,t=i[2])&&p(e,"src",t)},d(i){i&&k(e)}}}function OM(n){let e,t,i=n[2].substring(n[2].lastIndexOf("/")+1)+"",o,r,l,s,a,f,c;return{c(){e=g("a"),t=j("/../"),o=j(i),r=$(),l=g("div"),s=$(),a=g("button"),a.textContent="Close",p(e,"href",n[2]),p(e,"class","link-hint txt-ellipsis"),p(l,"class","flex-fill"),p(a,"type","button"),p(a,"class","btn btn-secondary")},m(u,d){w(u,e,d),m(e,t),m(e,o),w(u,r,d),w(u,l,d),w(u,s,d),w(u,a,d),f||(c=X(a,"click",n[0]),f=!0)},p(u,d){d&4&&i!==(i=u[2].substring(u[2].lastIndexOf("/")+1)+"")&&ge(o,i),d&4&&p(e,"href",u[2])},d(u){u&&k(e),u&&k(r),u&&k(l),u&&k(s),u&&k(a),f=!1,c()}}}function TM(n){let e,t,i={class:"image-preview",popup:!0,$$slots:{footer:[OM],default:[DM]},$$scope:{ctx:n}};return e=new Ai({props:i}),n[4](e),e.$on("show",n[5]),e.$on("hide",n[6]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,[r]){const l={};r&132&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[4](null),q(e,o)}}}function EM(n,e,t){let i,o="";function r(c){c!==""&&B.checkImageUrl(c).then(()=>{t(2,o=c),i==null||i.show()}).catch(()=>{console.warn("Invalid image preview url: ",c),l()})}function l(){return i==null?void 0:i.hide()}function s(c){he[c?"unshift":"push"](()=>{i=c,t(1,i)})}function a(c){ft.call(this,n,c)}function f(c){ft.call(this,n,c)}return[l,i,o,r,s,a,f]}class PM extends Ie{constructor(e){super(),Le(this,e,EM,TM,Ee,{show:3,hide:0})}get show(){return this.$$.ctx[3]}get hide(){return this.$$.ctx[0]}}function FM(n){let e;return{c(){e=g("i"),p(e,"class","ri-file-line")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function LM(n){let e,t,i,o;return{c(){e=g("img"),Qn(e.src,t=n[1])||p(e,"src",t),p(e,"alt",n[0])},m(r,l){w(r,e,l),i||(o=X(e,"error",n[2]),i=!0)},p(r,l){l&2&&!Qn(e.src,t=r[1])&&p(e,"src",t),l&1&&p(e,"alt",r[0])},d(r){r&&k(e),i=!1,o()}}}function IM(n){let e;function t(r,l){return r[1]?LM:FM}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,[l]){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},i:le,o:le,d(r){o.d(r),r&&k(e)}}}function RM(n,e,t){let{record:i}=e,{filename:o}=e,r="";function l(){t(1,r="")}return n.$$set=s=>{"record"in s&&t(3,i=s.record),"filename"in s&&t(0,o=s.filename)},n.$$.update=()=>{n.$$.dirty&9&&B.hasImageExtension(o)&&t(1,r=Se.Records.getFileUrl(i,`${o}?thumb=100x100`))},[o,r,l,i]}class P1 extends Ie{constructor(e){super(),Le(this,e,RM,IM,Ee,{record:3,filename:0})}}function hm(n,e,t){const i=n.slice();return i[25]=e[t],i[27]=t,i}function mm(n,e,t){const i=n.slice();return i[28]=e[t],i[27]=t,i}function NM(n){let e,t,i;function o(){return n[16](n[27])}return{c(){e=g("button"),e.innerHTML='',p(e,"type","button"),p(e,"class","btn btn-secondary btn-sm btn-circle btn-remove txt-hint")},m(r,l){w(r,e,l),t||(i=[Xe(St.call(null,e,"Remove file")),X(e,"click",o)],t=!0)},p(r,l){n=r},d(r){r&&k(e),t=!1,rt(i)}}}function jM(n){let e,t,i;function o(){return n[15](n[27])}return{c(){e=g("button"),e.innerHTML='Restore',p(e,"type","button"),p(e,"class","btn btn-sm btn-danger btn-secondary")},m(r,l){w(r,e,l),t||(i=X(e,"click",o),t=!0)},p(r,l){n=r},d(r){r&&k(e),t=!1,i()}}}function bm(n,e){let t,i,o,r,l,s,a,f=e[28]+"",c,u,d,h,b,v,_,y;o=new P1({props:{record:e[2],filename:e[28]}});function S(){return e[14](e[28])}function C(A,O){return O&18&&(b=null),b==null&&(b=!!A[1].includes(A[27])),b?jM:NM}let x=C(e,-1),M=x(e);return{key:n,first:null,c(){t=g("div"),i=g("figute"),V(o.$$.fragment),l=$(),s=g("a"),a=j("/.../"),c=j(f),h=$(),M.c(),p(i,"class","thumb"),p(i,"title",r=B.hasImageExtension(e[28])?"Preview":""),ne(i,"fade",e[1].includes(e[27])),ne(i,"link-fade",B.hasImageExtension(e[28])),p(s,"href",u=Se.Records.getFileUrl(e[2],e[28])),p(s,"class","filename"),p(s,"title",d="Download "+e[28]),p(s,"target","_blank"),p(s,"rel","noopener"),p(s,"download",""),ne(s,"txt-strikethrough",e[1].includes(e[27])),p(t,"class","list-item"),this.first=t},m(A,O){w(A,t,O),m(t,i),H(o,i,null),m(t,l),m(t,s),m(s,a),m(s,c),m(t,h),M.m(t,null),v=!0,_||(y=X(i,"click",S),_=!0)},p(A,O){e=A;const D={};O&4&&(D.record=e[2]),O&16&&(D.filename=e[28]),o.$set(D),(!v||O&16&&r!==(r=B.hasImageExtension(e[28])?"Preview":""))&&p(i,"title",r),O&18&&ne(i,"fade",e[1].includes(e[27])),O&16&&ne(i,"link-fade",B.hasImageExtension(e[28])),(!v||O&16)&&f!==(f=e[28]+"")&&ge(c,f),(!v||O&20&&u!==(u=Se.Records.getFileUrl(e[2],e[28])))&&p(s,"href",u),(!v||O&16&&d!==(d="Download "+e[28]))&&p(s,"title",d),O&18&&ne(s,"txt-strikethrough",e[1].includes(e[27])),x===(x=C(e,O))&&M?M.p(e,O):(M.d(1),M=x(e),M&&(M.c(),M.m(t,null)))},i(A){v||(T(o.$$.fragment,A),v=!0)},o(A){F(o.$$.fragment,A),v=!1},d(A){A&&k(t),q(o),M.d(),_=!1,y()}}}function gm(n){let e,t,i,o,r,l,s,a,f=n[25].name+"",c,u,d,h,b,v,_,y;i=new AM({props:{file:n[25]}});function S(){return n[17](n[27])}return{c(){e=g("div"),t=g("figute"),V(i.$$.fragment),o=$(),r=g("div"),l=g("small"),l.textContent="New",s=$(),a=g("span"),c=j(f),d=$(),h=g("button"),h.innerHTML='',p(t,"class","thumb"),p(l,"class","label label-success m-r-5"),p(a,"class","txt"),p(r,"class","filename"),p(r,"title",u=n[25].name),p(h,"type","button"),p(h,"class","btn btn-secondary btn-sm btn-circle btn-remove"),p(e,"class","list-item")},m(C,x){w(C,e,x),m(e,t),H(i,t,null),m(e,o),m(e,r),m(r,l),m(r,s),m(r,a),m(a,c),m(e,d),m(e,h),v=!0,_||(y=[Xe(b=St.call(null,h,"Remove file")),X(h,"click",S)],_=!0)},p(C,x){n=C;const M={};x&1&&(M.file=n[25]),i.$set(M),(!v||x&1)&&f!==(f=n[25].name+"")&&ge(c,f),(!v||x&1&&u!==(u=n[25].name))&&p(r,"title",u)},i(C){v||(T(i.$$.fragment,C),v=!0)},o(C){F(i.$$.fragment,C),v=!1},d(C){C&&k(e),q(i),_=!1,rt(y)}}}function _m(n){let e,t,i,o,r,l;return{c(){e=g("div"),t=g("input"),i=$(),o=g("button"),o.innerHTML=` + Upload new file`,p(t,"type","file"),p(t,"class","hidden"),t.multiple=n[5],p(o,"type","button"),p(o,"class","btn btn-secondary btn-sm btn-block"),p(e,"class","list-item btn-list-item")},m(s,a){w(s,e,a),m(e,t),n[18](t),m(e,i),m(e,o),r||(l=[X(t,"change",n[19]),X(o,"click",n[20])],r=!0)},p(s,a){a&32&&(t.multiple=s[5])},d(s){s&&k(e),n[18](null),r=!1,rt(l)}}}function zM(n){let e,t,i,o,r,l=n[3].name+"",s,a,f,c,u=[],d=new Map,h,b,v,_=n[4];const y=A=>A[28];for(let A=0;A<_.length;A+=1){let O=mm(n,_,A),D=y(O);d.set(D,u[A]=bm(D,O))}let S=n[0],C=[];for(let A=0;AF(C[A],1,1,()=>{C[A]=null});let M=!n[9]&&_m(n);return{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),c=g("div");for(let A=0;A({24:l}),({uniqueId:l})=>l?16777216:0]},$$scope:{ctx:n}}});let r={};return i=new PM({props:r}),n[22](i),{c(){V(e.$$.fragment),t=$(),V(i.$$.fragment)},m(l,s){H(e,l,s),w(l,t,s),H(i,l,s),o=!0},p(l,[s]){const a={};s&8&&(a.class="form-field form-field-file "+(l[3].required?"required":"")),s&8&&(a.name=l[3].name),s&1090520063&&(a.$$scope={dirty:s,ctx:l}),e.$set(a);const f={};i.$set(f)},i(l){o||(T(e.$$.fragment,l),T(i.$$.fragment,l),o=!0)},o(l){F(e.$$.fragment,l),F(i.$$.fragment,l),o=!1},d(l){q(e,l),l&&k(t),n[22](null),q(i,l)}}}function qM(n,e,t){let i,o,r,{record:l}=e,{value:s=null}=e,{uploadedFiles:a=[]}=e,{deletedFileIndexes:f=[]}=e,{field:c=new kn}=e,u,d,h;function b(I){B.removeByValue(f,I),t(1,f)}function v(I){B.pushUnique(f,I),t(1,f)}function _(I){B.isEmpty(a[I])||a.splice(I,1),t(0,a)}function y(){h==null||h.dispatchEvent(new CustomEvent("change",{detail:{value:s,uploadedFiles:a,deletedFileIndexes:f},bubbles:!0}))}const S=I=>B.hasImageExtension(I)?d==null?void 0:d.show(Se.Records.getFileUrl(l,I)):!1,C=I=>b(I),x=I=>v(I),M=I=>_(I);function A(I){he[I?"unshift":"push"](()=>{u=I,t(6,u)})}const O=()=>{for(let I of u.files)a.push(I);t(0,a),t(6,u.value=null,u)},D=()=>u==null?void 0:u.click();function E(I){he[I?"unshift":"push"](()=>{h=I,t(8,h)})}function P(I){he[I?"unshift":"push"](()=>{d=I,t(7,d)})}return n.$$set=I=>{"record"in I&&t(2,l=I.record),"value"in I&&t(13,s=I.value),"uploadedFiles"in I&&t(0,a=I.uploadedFiles),"deletedFileIndexes"in I&&t(1,f=I.deletedFileIndexes),"field"in I&&t(3,c=I.field)},n.$$.update=()=>{var I,R;n.$$.dirty&1&&(Array.isArray(a)||t(0,a=B.toArray(a))),n.$$.dirty&2&&(Array.isArray(f)||t(1,f=B.toArray(f))),n.$$.dirty&8&&t(5,i=((I=c.options)==null?void 0:I.maxSelect)>1),n.$$.dirty&8224&&(typeof s=="undefined"||s===null)&&t(13,s=i?[]:null),n.$$.dirty&8192&&t(4,o=B.toArray(s)),n.$$.dirty&27&&t(9,r=(o.length||a.length)&&((R=c.options)==null?void 0:R.maxSelect)<=o.length+a.length-f.length),n.$$.dirty&3&&(a!==-1||f!==-1)&&y()},[a,f,l,c,o,i,u,d,h,r,b,v,_,s,S,C,x,M,A,O,D,E,P]}class VM extends Ie{constructor(e){super(),Le(this,e,qM,HM,Ee,{record:2,value:13,uploadedFiles:0,deletedFileIndexes:1,field:3})}}function vm(n){let e,t;return{c(){e=g("small"),t=j(n[1]),p(e,"class","block txt-hint txt-ellipsis")},m(i,o){w(i,e,o),m(e,t)},p(i,o){o&2&&ge(t,i[1])},d(i){i&&k(e)}}}function BM(n){let e,t,i,o,r,l=n[0].id+"",s,a,f,c,u=n[1]!==""&&n[1]!==n[0].id&&vm(n);return{c(){e=g("i"),i=$(),o=g("div"),r=g("div"),s=j(l),a=$(),u&&u.c(),p(e,"class","ri-information-line link-hint"),p(r,"class","block txt-ellipsis"),p(o,"class","content svelte-1gjwqyd")},m(d,h){w(d,e,h),w(d,i,h),w(d,o,h),m(o,r),m(r,s),m(o,a),u&&u.m(o,null),f||(c=Xe(t=St.call(null,e,{text:JSON.stringify(n[0],null,2),position:"left",class:"code"})),f=!0)},p(d,[h]){t&&Yn(t.update)&&h&1&&t.update.call(null,{text:JSON.stringify(d[0],null,2),position:"left",class:"code"}),h&1&&l!==(l=d[0].id+"")&&ge(s,l),d[1]!==""&&d[1]!==d[0].id?u?u.p(d,h):(u=vm(d),u.c(),u.m(o,null)):u&&(u.d(1),u=null)},i:le,o:le,d(d){d&&k(e),d&&k(i),d&&k(o),u&&u.d(),f=!1,c()}}}function UM(n,e,t){let i;const o=["id","created","updated","@collectionId","@collectionName"];let{item:r={}}=e;function l(s){s=s||{};const a=["name","title","label","key","email","heading","content",...Object.keys(s)];for(const f of a)if(typeof s[f]=="string"&&!B.isEmpty(s[f])&&!o.includes(f))return f+": "+s[f];return""}return n.$$set=s=>{"item"in s&&t(0,r=s.item)},n.$$.update=()=>{n.$$.dirty&1&&t(1,i=l(r))},[r,i]}class WM extends Ie{constructor(e){super(),Le(this,e,UM,BM,Ee,{item:0})}}function ym(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Load more',p(e,"type","button"),p(e,"class","btn btn-block btn-sm"),ne(e,"btn-loading",n[6]),ne(e,"btn-disabled",n[6])},m(o,r){w(o,e,r),t||(i=X(e,"click",Vn(n[14])),t=!0)},p(o,r){r&64&&ne(e,"btn-loading",o[6]),r&64&&ne(e,"btn-disabled",o[6])},d(o){o&&k(e),t=!1,i()}}}function YM(n){let e,t=n[7]&&ym(n);return{c(){t&&t.c(),e=lt()},m(i,o){t&&t.m(i,o),w(i,e,o)},p(i,o){i[7]?t?t.p(i,o):(t=ym(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){t&&t.d(i),i&&k(e)}}}function GM(n){let e,t,i,o;const r=[{selectPlaceholder:n[8]?"Loading...":n[3]},{items:n[5]},{searchable:n[5].length>5},{selectionKey:"id"},{labelComponent:n[4]},{optionComponent:n[4]},{multiple:n[2]},{class:"records-select block-options"},n[10]];function l(f){n[15](f)}function s(f){n[16](f)}let a={$$slots:{afterOptions:[YM]},$$scope:{ctx:n}};for(let f=0;fFe(e,"keyOfSelected",l)),he.push(()=>Fe(e,"selected",s)),e.$on("show",n[17]),e.$on("hide",n[18]),{c(){V(e.$$.fragment)},m(f,c){H(e,f,c),o=!0},p(f,[c]){const u=c&1340?bn(r,[c&264&&{selectPlaceholder:f[8]?"Loading...":f[3]},c&32&&{items:f[5]},c&32&&{searchable:f[5].length>5},r[3],c&16&&{labelComponent:f[4]},c&16&&{optionComponent:f[4]},c&4&&{multiple:f[2]},r[7],c&1024&&pi(f[10])]):{};c&4194496&&(u.$$scope={dirty:c,ctx:f}),!t&&c&2&&(t=!0,u.keyOfSelected=f[1],Re(()=>t=!1)),!i&&c&1&&(i=!0,u.selected=f[0],Re(()=>i=!1)),e.$set(u)},i(f){o||(T(e.$$.fragment,f),o=!0)},o(f){F(e.$$.fragment,f),o=!1},d(f){q(e,f)}}}function KM(n,e,t){let i,o;const r=["multiple","selected","keyOfSelected","selectPlaceholder","optionComponent","collectionId"];let l=Wt(e,r);const s="select_"+B.randomString(5);let{multiple:a=!1}=e,{selected:f=a?[]:void 0}=e,{keyOfSelected:c=a?[]:void 0}=e,{selectPlaceholder:u="- Select -"}=e,{optionComponent:d=WM}=e,{collectionId:h}=e,b=[],v=1,_=0,y=!1,S=!1;C();async function C(){const P=B.toArray(c);if(!(!h||!P.length)){t(13,S=!0);try{const I=[];for(const R of P)I.push(`id="${R}"`);t(0,f=await Se.Records.getFullList(h,200,{sort:"-created",filter:I.join("||"),$cancelKey:s+"loadSelected"})),t(5,b=B.filterDuplicatesByKey(b.concat(f)))}catch(I){Se.errorResponseHandler(I)}t(13,S=!1)}}async function x(P=!1){if(!!h){t(6,y=!0);try{const I=P?1:v+1,R=await Se.Records.getList(h,I,200,{sort:"-created",$cancelKey:s+"loadList"});P&&t(5,b=[]),t(5,b=B.filterDuplicatesByKey(b.concat(R.items))),v=R.page,t(12,_=R.totalItems)}catch(I){Se.errorResponseHandler(I)}t(6,y=!1)}}const M=()=>x();function A(P){c=P,t(1,c)}function O(P){f=P,t(0,f)}function D(P){ft.call(this,n,P)}function E(P){ft.call(this,n,P)}return n.$$set=P=>{e=ut(ut({},e),ui(P)),t(10,l=Wt(e,r)),"multiple"in P&&t(2,a=P.multiple),"selected"in P&&t(0,f=P.selected),"keyOfSelected"in P&&t(1,c=P.keyOfSelected),"selectPlaceholder"in P&&t(3,u=P.selectPlaceholder),"optionComponent"in P&&t(4,d=P.optionComponent),"collectionId"in P&&t(11,h=P.collectionId)},n.$$.update=()=>{n.$$.dirty&2048&&h&&x(),n.$$.dirty&8256&&t(8,i=y||S),n.$$.dirty&4128&&t(7,o=_>b.length)},[f,c,a,u,d,b,y,o,i,x,l,h,_,S,M,A,O,D,E]}class JM extends Ie{constructor(e){super(),Le(this,e,KM,GM,Ee,{multiple:2,selected:0,keyOfSelected:1,selectPlaceholder:3,optionComponent:4,collectionId:11})}}function km(n){let e,t,i=n[1].options.maxSelect+"",o,r;return{c(){e=g("div"),t=j("Select up to "),o=j(i),r=j(" items."),p(e,"class","help-block")},m(l,s){w(l,e,s),m(e,t),m(e,o),m(e,r)},p(l,s){s&2&&i!==(i=l[1].options.maxSelect+"")&&ge(o,i)},d(l){l&&k(e)}}}function ZM(n){var S,C;let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h,b;function v(x){n[3](x)}let _={toggle:!0,id:n[4],multiple:n[2],collectionId:(S=n[1].options)==null?void 0:S.collectionId};n[0]!==void 0&&(_.keyOfSelected=n[0]),c=new JM({props:_}),he.push(()=>Fe(c,"keyOfSelected",v));let y=((C=n[1].options)==null?void 0:C.maxSelect)>1&&km(n);return{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),V(c.$$.fragment),d=$(),y&&y.c(),h=lt(),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[4])},m(x,M){w(x,e,M),m(e,t),m(e,o),m(e,r),m(r,s),w(x,f,M),H(c,x,M),w(x,d,M),y&&y.m(x,M),w(x,h,M),b=!0},p(x,M){var O,D;(!b||M&2&&i!==(i=B.getFieldTypeIcon(x[1].type)))&&p(t,"class",i),(!b||M&2)&&l!==(l=x[1].name+"")&&ge(s,l),(!b||M&16&&a!==(a=x[4]))&&p(e,"for",a);const A={};M&16&&(A.id=x[4]),M&4&&(A.multiple=x[2]),M&2&&(A.collectionId=(O=x[1].options)==null?void 0:O.collectionId),!u&&M&1&&(u=!0,A.keyOfSelected=x[0],Re(()=>u=!1)),c.$set(A),((D=x[1].options)==null?void 0:D.maxSelect)>1?y?y.p(x,M):(y=km(x),y.c(),y.m(h.parentNode,h)):y&&(y.d(1),y=null)},i(x){b||(T(c.$$.fragment,x),b=!0)},o(x){F(c.$$.fragment,x),b=!1},d(x){x&&k(e),x&&k(f),q(c,x),x&&k(d),y&&y.d(x),x&&k(h)}}}function XM(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":""),name:n[1].name,$$slots:{default:[ZM,({uniqueId:i})=>({4:i}),({uniqueId:i})=>i?16:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&2&&(r.class="form-field "+(i[1].required?"required":"")),o&2&&(r.name=i[1].name),o&55&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function QM(n,e,t){let i,{field:o=new kn}=e,{value:r=void 0}=e;function l(s){r=s,t(0,r),t(2,i),t(1,o)}return n.$$set=s=>{"field"in s&&t(1,o=s.field),"value"in s&&t(0,r=s.value)},n.$$.update=()=>{var s;n.$$.dirty&2&&t(2,i=((s=o.options)==null?void 0:s.maxSelect)>1),n.$$.dirty&7&&i&&Array.isArray(r)&&r.length>o.options.maxSelect&&t(0,r=r.slice(o.options.maxSelect-1))},[r,o,i,l]}class e$ extends Ie{constructor(e){super(),Le(this,e,QM,XM,Ee,{field:1,value:0})}}function t$(n){let e,t,i,o,r,l=n[0].id+"",s,a,f,c=n[0].email+"",u,d,h;return{c(){e=g("i"),i=$(),o=g("div"),r=g("div"),s=j(l),a=$(),f=g("small"),u=j(c),p(e,"class","ri-information-line link-hint"),p(r,"class","block txt-ellipsis"),p(f,"class","block txt-hint txt-ellipsis"),p(o,"class","content")},m(b,v){w(b,e,v),w(b,i,v),w(b,o,v),m(o,r),m(r,s),m(o,a),m(o,f),m(f,u),d||(h=Xe(t=St.call(null,e,{text:JSON.stringify(n[0],null,2),position:"left",class:"code"})),d=!0)},p(b,[v]){t&&Yn(t.update)&&v&1&&t.update.call(null,{text:JSON.stringify(b[0],null,2),position:"left",class:"code"}),v&1&&l!==(l=b[0].id+"")&&ge(s,l),v&1&&c!==(c=b[0].email+"")&&ge(u,c)},i:le,o:le,d(b){b&&k(e),b&&k(i),b&&k(o),d=!1,h()}}}function n$(n,e,t){let{item:i={}}=e;return n.$$set=o=>{"item"in o&&t(0,i=o.item)},[i]}class bf extends Ie{constructor(e){super(),Le(this,e,n$,t$,Ee,{item:0})}}function wm(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Load more',p(e,"type","button"),p(e,"class","btn btn-block btn-sm"),ne(e,"btn-loading",n[6]),ne(e,"btn-disabled",n[6])},m(o,r){w(o,e,r),t||(i=X(e,"click",Vn(n[13])),t=!0)},p(o,r){r&64&&ne(e,"btn-loading",o[6]),r&64&&ne(e,"btn-disabled",o[6])},d(o){o&&k(e),t=!1,i()}}}function i$(n){let e,t=n[7]&&wm(n);return{c(){t&&t.c(),e=lt()},m(i,o){t&&t.m(i,o),w(i,e,o)},p(i,o){i[7]?t?t.p(i,o):(t=wm(i),t.c(),t.m(e.parentNode,e)):t&&(t.d(1),t=null)},d(i){t&&t.d(i),i&&k(e)}}}function o$(n){let e,t,i,o;const r=[{selectPlaceholder:n[8]?"Loading...":n[3]},{items:n[5]},{searchable:n[5].length>5},{selectionKey:"id"},{labelComponent:bf},{optionComponent:n[4]},{multiple:n[2]},{class:"users-select block-options"},n[10]];function l(f){n[14](f)}function s(f){n[15](f)}let a={$$slots:{afterOptions:[i$]},$$scope:{ctx:n}};for(let f=0;fFe(e,"keyOfSelected",l)),he.push(()=>Fe(e,"selected",s)),e.$on("show",n[16]),e.$on("hide",n[17]),{c(){V(e.$$.fragment)},m(f,c){H(e,f,c),o=!0},p(f,[c]){const u=c&1340?bn(r,[c&264&&{selectPlaceholder:f[8]?"Loading...":f[3]},c&32&&{items:f[5]},c&32&&{searchable:f[5].length>5},r[3],c&0&&{labelComponent:bf},c&16&&{optionComponent:f[4]},c&4&&{multiple:f[2]},r[7],c&1024&&pi(f[10])]):{};c&2097344&&(u.$$scope={dirty:c,ctx:f}),!t&&c&2&&(t=!0,u.keyOfSelected=f[1],Re(()=>t=!1)),!i&&c&1&&(i=!0,u.selected=f[0],Re(()=>i=!1)),e.$set(u)},i(f){o||(T(e.$$.fragment,f),o=!0)},o(f){F(e.$$.fragment,f),o=!1},d(f){q(e,f)}}}function r$(n,e,t){let i,o;const r=["multiple","selected","keyOfSelected","selectPlaceholder","optionComponent"];let l=Wt(e,r);const s="select_"+B.randomString(5);let{multiple:a=!1}=e,{selected:f=a?[]:void 0}=e,{keyOfSelected:c=a?[]:void 0}=e,{selectPlaceholder:u="- Select -"}=e,{optionComponent:d=bf}=e,h=[],b=1,v=0,_=!1,y=!1;C(),S();async function S(){const E=B.toArray(c);if(!!E.length){t(12,y=!0);try{const P=[];for(const I of E)P.push(`id="${I}"`);t(0,f=await Se.Users.getFullList(100,{sort:"-created",filter:P.join("||"),$cancelKey:s+"loadSelected"})),t(5,h=B.filterDuplicatesByKey(h.concat(f)))}catch(P){Se.errorResponseHandler(P)}t(12,y=!1)}}async function C(E=!1){t(6,_=!0);try{const P=E?1:b+1,I=await Se.Users.getList(P,200,{sort:"-created",$cancelKey:s+"loadList"});E&&t(5,h=[]),t(5,h=B.filterDuplicatesByKey(h.concat(I.items))),b=I.page,t(11,v=I.totalItems)}catch(P){Se.errorResponseHandler(P)}t(6,_=!1)}const x=()=>C();function M(E){c=E,t(1,c)}function A(E){f=E,t(0,f)}function O(E){ft.call(this,n,E)}function D(E){ft.call(this,n,E)}return n.$$set=E=>{e=ut(ut({},e),ui(E)),t(10,l=Wt(e,r)),"multiple"in E&&t(2,a=E.multiple),"selected"in E&&t(0,f=E.selected),"keyOfSelected"in E&&t(1,c=E.keyOfSelected),"selectPlaceholder"in E&&t(3,u=E.selectPlaceholder),"optionComponent"in E&&t(4,d=E.optionComponent)},n.$$.update=()=>{n.$$.dirty&4160&&t(8,i=_||y),n.$$.dirty&2080&&t(7,o=v>h.length)},[f,c,a,u,d,h,_,o,i,C,l,v,y,x,M,A,O,D]}class l$ extends Ie{constructor(e){super(),Le(this,e,r$,o$,Ee,{multiple:2,selected:0,keyOfSelected:1,selectPlaceholder:3,optionComponent:4})}}function Sm(n){let e,t,i=n[1].options.maxSelect+"",o,r;return{c(){e=g("div"),t=j("Select up to "),o=j(i),r=j(" users."),p(e,"class","help-block")},m(l,s){w(l,e,s),m(e,t),m(e,o),m(e,r)},p(l,s){s&2&&i!==(i=l[1].options.maxSelect+"")&&ge(o,i)},d(l){l&&k(e)}}}function s$(n){var S;let e,t,i,o,r,l=n[1].name+"",s,a,f,c,u,d,h,b;function v(C){n[4](C)}let _={toggle:!0,id:n[5],multiple:n[2],disabled:n[3]};n[0]!==void 0&&(_.keyOfSelected=n[0]),c=new l$({props:_}),he.push(()=>Fe(c,"keyOfSelected",v));let y=((S=n[1].options)==null?void 0:S.maxSelect)>1&&Sm(n);return{c(){e=g("label"),t=g("i"),o=$(),r=g("span"),s=j(l),f=$(),V(c.$$.fragment),d=$(),y&&y.c(),h=lt(),p(t,"class",i=B.getFieldTypeIcon(n[1].type)),p(r,"class","txt"),p(e,"for",a=n[5])},m(C,x){w(C,e,x),m(e,t),m(e,o),m(e,r),m(r,s),w(C,f,x),H(c,C,x),w(C,d,x),y&&y.m(C,x),w(C,h,x),b=!0},p(C,x){var A;(!b||x&2&&i!==(i=B.getFieldTypeIcon(C[1].type)))&&p(t,"class",i),(!b||x&2)&&l!==(l=C[1].name+"")&&ge(s,l),(!b||x&32&&a!==(a=C[5]))&&p(e,"for",a);const M={};x&32&&(M.id=C[5]),x&4&&(M.multiple=C[2]),x&8&&(M.disabled=C[3]),!u&&x&1&&(u=!0,M.keyOfSelected=C[0],Re(()=>u=!1)),c.$set(M),((A=C[1].options)==null?void 0:A.maxSelect)>1?y?y.p(C,x):(y=Sm(C),y.c(),y.m(h.parentNode,h)):y&&(y.d(1),y=null)},i(C){b||(T(c.$$.fragment,C),b=!0)},o(C){F(c.$$.fragment,C),b=!1},d(C){C&&k(e),C&&k(f),q(c,C),C&&k(d),y&&y.d(C),C&&k(h)}}}function a$(n){let e,t;return e=new je({props:{class:"form-field "+(n[1].required?"required":"")+" "+(n[3]?"disabled":""),name:n[1].name,$$slots:{default:[s$,({uniqueId:i})=>({5:i}),({uniqueId:i})=>i?32:0]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,[o]){const r={};o&10&&(r.class="form-field "+(i[1].required?"required":"")+" "+(i[3]?"disabled":"")),o&2&&(r.name=i[1].name),o&111&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function f$(n,e,t){let i,o,{field:r=new kn}=e,{value:l=void 0}=e;function s(a){l=a,t(0,l),t(2,o),t(1,r)}return n.$$set=a=>{"field"in a&&t(1,r=a.field),"value"in a&&t(0,l=a.value)},n.$$.update=()=>{var a;n.$$.dirty&2&&t(2,o=((a=r.options)==null?void 0:a.maxSelect)>1),n.$$.dirty&7&&o&&Array.isArray(l)&&l.length>r.options.maxSelect&&t(0,l=l.slice(r.options.maxSelect-1)),n.$$.dirty&3&&t(3,i=!B.isEmpty(l)&&r.system)},[l,r,o,i,s]}class c$ extends Ie{constructor(e){super(),Le(this,e,f$,a$,Ee,{field:1,value:0})}}function Cm(n,e,t){const i=n.slice();return i[40]=e[t],i[41]=e,i[42]=t,i}function xm(n){let e,t;return e=new je({props:{class:"form-field disabled",name:"id",$$slots:{default:[u$,({uniqueId:i})=>({43:i}),({uniqueId:i})=>[0,i?4096:0]]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,o){const r={};o[0]&4|o[1]&12288&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function u$(n){let e,t,i,o,r,l,s,a,f,c,u;return{c(){e=g("label"),t=g("i"),i=$(),o=g("span"),o.textContent="id",r=$(),l=g("span"),a=$(),f=g("input"),p(t,"class",B.getFieldTypeIcon("primary")),p(o,"class","txt"),p(l,"class","flex-fill"),p(e,"for",s=n[43]),p(f,"type","text"),p(f,"id",c=n[43]),f.value=u=n[2].id,f.disabled=!0},m(d,h){w(d,e,h),m(e,t),m(e,i),m(e,o),m(e,r),m(e,l),w(d,a,h),w(d,f,h)},p(d,h){h[1]&4096&&s!==(s=d[43])&&p(e,"for",s),h[1]&4096&&c!==(c=d[43])&&p(f,"id",c),h[0]&4&&u!==(u=d[2].id)&&f.value!==u&&(f.value=u)},d(d){d&&k(e),d&&k(a),d&&k(f)}}}function Mm(n){let e;return{c(){e=g("div"),e.innerHTML=`
    No custom fields to be set
    + `,p(e,"class","block txt-center txt-disabled")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function d$(n){let e,t,i;function o(l){n[31](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new c$({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function p$(n){let e,t,i;function o(l){n[30](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new e$({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function h$(n){let e,t,i,o,r;function l(c){n[27](c,n[40])}function s(c){n[28](c,n[40])}function a(c){n[29](c,n[40])}let f={field:n[40],record:n[2]};return n[2][n[40].name]!==void 0&&(f.value=n[2][n[40].name]),n[3][n[40].name]!==void 0&&(f.uploadedFiles=n[3][n[40].name]),n[4][n[40].name]!==void 0&&(f.deletedFileIndexes=n[4][n[40].name]),e=new VM({props:f}),he.push(()=>Fe(e,"value",l)),he.push(()=>Fe(e,"uploadedFiles",s)),he.push(()=>Fe(e,"deletedFileIndexes",a)),{c(){V(e.$$.fragment)},m(c,u){H(e,c,u),r=!0},p(c,u){n=c;const d={};u[0]&1&&(d.field=n[40]),u[0]&4&&(d.record=n[2]),!t&&u[0]&5&&(t=!0,d.value=n[2][n[40].name],Re(()=>t=!1)),!i&&u[0]&9&&(i=!0,d.uploadedFiles=n[3][n[40].name],Re(()=>i=!1)),!o&&u[0]&17&&(o=!0,d.deletedFileIndexes=n[4][n[40].name],Re(()=>o=!1)),e.$set(d)},i(c){r||(T(e.$$.fragment,c),r=!0)},o(c){F(e.$$.fragment,c),r=!1},d(c){q(e,c)}}}function m$(n){let e,t,i;function o(l){n[26](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new SM({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function b$(n){let e,t,i;function o(l){n[25](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new vM({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function g$(n){let e,t,i;function o(l){n[24](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new mM({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function _$(n){let e,t,i;function o(l){n[23](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new uM({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function v$(n){let e,t,i;function o(l){n[22](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new sM({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function y$(n){let e,t,i;function o(l){n[21](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new iM({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function k$(n){let e,t,i;function o(l){n[20](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new Q6({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function w$(n){let e,t,i;function o(l){n[19](l,n[40])}let r={field:n[40]};return n[2][n[40].name]!==void 0&&(r.value=n[2][n[40].name]),e=new K6({props:r}),he.push(()=>Fe(e,"value",o)),{c(){V(e.$$.fragment)},m(l,s){H(e,l,s),i=!0},p(l,s){n=l;const a={};s[0]&1&&(a.field=n[40]),!t&&s[0]&5&&(t=!0,a.value=n[2][n[40].name],Re(()=>t=!1)),e.$set(a)},i(l){i||(T(e.$$.fragment,l),i=!0)},o(l){F(e.$$.fragment,l),i=!1},d(l){q(e,l)}}}function $m(n,e){let t,i,o,r,l;const s=[w$,k$,y$,v$,_$,g$,b$,m$,h$,p$,d$],a=[];function f(c,u){return c[40].type==="text"?0:c[40].type==="number"?1:c[40].type==="bool"?2:c[40].type==="email"?3:c[40].type==="url"?4:c[40].type==="date"?5:c[40].type==="select"?6:c[40].type==="json"?7:c[40].type==="file"?8:c[40].type==="relation"?9:c[40].type==="user"?10:-1}return~(i=f(e))&&(o=a[i]=s[i](e)),{key:n,first:null,c(){t=lt(),o&&o.c(),r=lt(),this.first=t},m(c,u){w(c,t,u),~i&&a[i].m(c,u),w(c,r,u),l=!0},p(c,u){e=c;let d=i;i=f(e),i===d?~i&&a[i].p(e,u):(o&&(Ae(),F(a[d],1,1,()=>{a[d]=null}),De()),~i?(o=a[i],o?o.p(e,u):(o=a[i]=s[i](e),o.c()),T(o,1),o.m(r.parentNode,r)):o=null)},i(c){l||(T(o),l=!0)},o(c){F(o),l=!1},d(c){c&&k(t),~i&&a[i].d(c),c&&k(r)}}}function S$(n){var d;let e,t,i=[],o=new Map,r,l,s,a=!n[2].isNew&&xm(n),f=((d=n[0])==null?void 0:d.schema)||[];const c=h=>h[40].name;for(let h=0;h{a=null}),De()):a?(a.p(h,b),b[0]&4&&T(a,1)):(a=xm(h),a.c(),T(a,1),a.m(e,t)),b[0]&29&&(f=((v=h[0])==null?void 0:v.schema)||[],Ae(),i=st(i,b,c,1,h,f,o,e,Pt,$m,null,Cm),De(),!f.length&&u?u.p(h,b):f.length?u&&(u.d(1),u=null):(u=Mm(),u.c(),u.m(e,null)))},i(h){if(!r){T(a);for(let b=0;b + Delete`,p(e,"tabindex","0"),p(e,"class","dropdown-item closable")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[18]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function x$(n){let e,t=n[2].isNew?"New":"Edit",i,o,r=n[0].name+"",l,s,a,f,c,u=!n[2].isNew&&n[9]&&Am(n);return{c(){e=g("h4"),i=j(t),o=$(),l=j(r),s=j(" record"),a=$(),u&&u.c(),f=lt()},m(d,h){w(d,e,h),m(e,i),m(e,o),m(e,l),m(e,s),w(d,a,h),u&&u.m(d,h),w(d,f,h),c=!0},p(d,h){(!c||h[0]&4)&&t!==(t=d[2].isNew?"New":"Edit")&&ge(i,t),(!c||h[0]&1)&&r!==(r=d[0].name+"")&&ge(l,r),!d[2].isNew&&d[9]?u?(u.p(d,h),h[0]&516&&T(u,1)):(u=Am(d),u.c(),T(u,1),u.m(f.parentNode,f)):u&&(Ae(),F(u,1,1,()=>{u=null}),De())},i(d){c||(T(u),c=!0)},o(d){F(u),c=!1},d(d){d&&k(e),d&&k(a),u&&u.d(d),d&&k(f)}}}function M$(n){let e,t,i,o,r,l=n[2].isNew?"Create":"Save changes",s,a,f,c;return{c(){e=g("button"),t=g("span"),t.textContent="Cancel",i=$(),o=g("button"),r=g("span"),s=j(l),p(t,"class","txt"),p(e,"type","button"),p(e,"class","btn btn-secondary"),e.disabled=n[7],p(r,"class","txt"),p(o,"type","submit"),p(o,"form",n[11]),p(o,"class","btn btn-expanded"),o.disabled=a=!n[10]||n[7],ne(o,"btn-loading",n[7])},m(u,d){w(u,e,d),m(e,t),w(u,i,d),w(u,o,d),m(o,r),m(r,s),f||(c=X(e,"click",n[17]),f=!0)},p(u,d){d[0]&128&&(e.disabled=u[7]),d[0]&4&&l!==(l=u[2].isNew?"Create":"Save changes")&&ge(s,l),d[0]&1152&&a!==(a=!u[10]||u[7])&&(o.disabled=a),d[0]&128&&ne(o,"btn-loading",u[7])},d(u){u&&k(e),u&&k(i),u&&k(o),f=!1,c()}}}function $$(n){let e,t,i={class:"overlay-panel-lg record-panel",beforeHide:n[32],$$slots:{footer:[M$],header:[x$],default:[S$]},$$scope:{ctx:n}};return e=new Ai({props:i}),n[33](e),e.$on("hide",n[34]),e.$on("show",n[35]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,r){const l={};r[0]&288&&(l.beforeHide=o[32]),r[0]&1693|r[1]&8192&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[33](null),q(e,o)}}}function Dm(n){return JSON.stringify(n)}function A$(n,e,t){let i,o,r,l;const s=yn(),a="record_"+B.randomString(5);let{collection:f}=e,c,u=null,d=new Ql,h=!1,b=!1,v={},_={},y="";function S(fe){return x(fe),t(8,b=!0),c==null?void 0:c.show()}function C(){return c==null?void 0:c.hide()}function x(fe){Ui({}),u=fe||{},t(2,d=fe!=null&&fe.clone?fe.clone():new Ql),t(3,v={}),t(4,_={}),t(15,y=Dm(d))}function M(){if(h||!o)return;t(7,h=!0);const fe=O();let ie;d.isNew?ie=Se.Records.create(f==null?void 0:f.id,fe):ie=Se.Records.update(f==null?void 0:f.id,d.id,fe),ie.then(async ye=>{hn(d.isNew?"Successfully created record.":"Successfully updated record."),t(8,b=!1),C(),s("save",ye)}).catch(ye=>{Se.errorResponseHandler(ye)}).finally(()=>{t(7,h=!1)})}function A(){!(u!=null&&u.id)||xi("Do you really want to delete the selected record?",()=>Se.Records.delete(u["@collectionId"],u.id).then(()=>{C(),hn("Successfully deleted record."),s("delete",u)}).catch(fe=>{Se.errorResponseHandler(fe)}))}function O(){const fe=(d==null?void 0:d.export())||{},ie=new FormData,ye={};for(const Ne of(f==null?void 0:f.schema)||[])ye[Ne.name]=Ne;for(const Ne in fe)!ye[Ne]||(typeof fe[Ne]=="undefined"&&(fe[Ne]=null),B.addValueToFormData(ie,Ne,fe[Ne]));for(const Ne in v){const Pe=B.toArray(v[Ne]);for(const ze of Pe)ie.append(Ne,ze)}for(const Ne in _){const Pe=B.toArray(_[Ne]);for(const ze of Pe)ie.append(Ne+"."+ze,"")}return ie}const D=()=>C(),E=()=>A();function P(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function I(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function R(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function G(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function U(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function z(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function K(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function Y(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function W(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function te(fe,ie){n.$$.not_equal(v[ie.name],fe)&&(v[ie.name]=fe,t(3,v))}function ce(fe,ie){n.$$.not_equal(_[ie.name],fe)&&(_[ie.name]=fe,t(4,_))}function ve(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}function oe(fe,ie){n.$$.not_equal(d[ie.name],fe)&&(d[ie.name]=fe,t(2,d))}const J=()=>o&&b?(xi("You have unsaved changes. Do you really want to close the panel?",()=>{t(8,b=!1),C()}),!1):!0;function $e(fe){he[fe?"unshift":"push"](()=>{c=fe,t(6,c)})}function ee(fe){ft.call(this,n,fe)}function _e(fe){ft.call(this,n,fe)}return n.$$set=fe=>{"collection"in fe&&t(0,f=fe.collection)},n.$$.update=()=>{n.$$.dirty[0]&24&&t(16,i=B.hasNonEmptyProps(v)||B.hasNonEmptyProps(_)),n.$$.dirty[0]&98308&&t(5,o=i||y!=Dm(d)),n.$$.dirty[0]&36&&t(10,r=d.isNew||o),n.$$.dirty[0]&1&&t(9,l=(f==null?void 0:f.name)!=="profiles")},[f,C,d,v,_,o,c,h,b,l,r,a,M,A,S,y,i,D,E,P,I,R,G,U,z,K,Y,W,te,ce,ve,oe,J,$e,ee,_e]}class F1 extends Ie{constructor(e){super(),Le(this,e,A$,$$,Ee,{collection:0,show:14,hide:1},null,[-1,-1])}get show(){return this.$$.ctx[14]}get hide(){return this.$$.ctx[1]}}function D$(n){let e;return{c(){e=g("span"),e.textContent="N/A",p(e,"class","txt txt-hint")},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function O$(n){let e,t;return{c(){e=g("span"),t=j(n[1]),p(e,"class","label txt-base txt-mono"),p(e,"title",n[0])},m(i,o){w(i,e,o),m(e,t)},p(i,o){o&2&&ge(t,i[1]),o&1&&p(e,"title",i[0])},d(i){i&&k(e)}}}function T$(n){let e;function t(r,l){return r[0]?O$:D$}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,[l]){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},i:le,o:le,d(r){o.d(r),r&&k(e)}}}function E$(n,e,t){let{id:i=""}=e,o=i;return n.$$set=r=>{"id"in r&&t(0,i=r.id)},n.$$.update=()=>{n.$$.dirty&1&&typeof i=="string"&&i.length>27&&t(1,o=i.substring(0,5)+"..."+i.substring(i.length-10))},[i,o]}class Ls extends Ie{constructor(e){super(),Le(this,e,E$,T$,Ee,{id:0})}}function Om(n,e,t){const i=n.slice();return i[8]=e[t],i}function Tm(n,e,t){const i=n.slice();return i[3]=e[t],i}function Em(n,e,t){const i=n.slice();return i[3]=e[t],i}function P$(n){let e,t=n[0][n[1].name]+"",i,o;return{c(){e=g("span"),i=j(t),p(e,"class","txt txt-ellipsis"),p(e,"title",o=n[0][n[1].name])},m(r,l){w(r,e,l),m(e,i)},p(r,l){l&3&&t!==(t=r[0][r[1].name]+"")&&ge(i,t),l&3&&o!==(o=r[0][r[1].name])&&p(e,"title",o)},i:le,o:le,d(r){r&&k(e)}}}function F$(n){let e,t,i=B.toArray(n[0][n[1].name]),o=[];for(let l=0;lF(o[l],1,1,()=>{o[l]=null});return{c(){e=g("div");for(let l=0;lF(o[l],1,1,()=>{o[l]=null});return{c(){e=g("div");for(let l=0;l{a[d]=null}),De(),o=a[i],o?o.p(c,u):(o=a[i]=s[i](c),o.c()),T(o,1),o.m(e,null)),(!l||u&2&&r!==(r="col-type-"+c[1].type+" col-field-"+c[1].name))&&p(e,"class",r)},i(c){l||(T(o),l=!0)},o(c){F(o),l=!1},d(c){c&&k(e),a[i].d()}}}function V$(n,e,t){let{record:i}=e,{field:o}=e;function r(l){ft.call(this,n,l)}return n.$$set=l=>{"record"in l&&t(0,i=l.record),"field"in l&&t(1,o=l.field)},[i,o,r]}class L1 extends Ie{constructor(e){super(),Le(this,e,V$,q$,Ee,{record:0,field:1})}}function Im(n,e,t){const i=n.slice();return i[35]=e[t],i}function Rm(n,e,t){const i=n.slice();return i[38]=e[t],i}function Nm(n,e,t){const i=n.slice();return i[38]=e[t],i}function B$(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="id",p(t,"class",B.getFieldTypeIcon("primary")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function U$(n){let e,t,i,o,r,l=n[38].name+"",s;return{c(){e=g("div"),t=g("i"),o=$(),r=g("span"),s=j(l),p(t,"class",i=B.getFieldTypeIcon(n[38].type)),p(r,"class","txt"),p(e,"class","col-header-content")},m(a,f){w(a,e,f),m(e,t),m(e,o),m(e,r),m(r,s)},p(a,f){f[0]&2048&&i!==(i=B.getFieldTypeIcon(a[38].type))&&p(t,"class",i),f[0]&2048&&l!==(l=a[38].name+"")&&ge(s,l)},d(a){a&&k(e)}}}function jm(n,e){let t,i,o,r;function l(a){e[22](a)}let s={class:"col-type-"+e[38].type+" col-field-"+e[38].name,name:e[38].name,$$slots:{default:[U$]},$$scope:{ctx:e}};return e[0]!==void 0&&(s.sort=e[0]),i=new en({props:s}),he.push(()=>Fe(i,"sort",l)),{key:n,first:null,c(){t=lt(),V(i.$$.fragment),this.first=t},m(a,f){w(a,t,f),H(i,a,f),r=!0},p(a,f){e=a;const c={};f[0]&2048&&(c.class="col-type-"+e[38].type+" col-field-"+e[38].name),f[0]&2048&&(c.name=e[38].name),f[0]&2048|f[1]&4096&&(c.$$scope={dirty:f,ctx:e}),!o&&f[0]&1&&(o=!0,c.sort=e[0],Re(()=>o=!1)),i.$set(c)},i(a){r||(T(i.$$.fragment,a),r=!0)},o(a){F(i.$$.fragment,a),r=!1},d(a){a&&k(t),q(i,a)}}}function W$(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="created",p(t,"class",B.getFieldTypeIcon("date")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function Y$(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="updated",p(t,"class",B.getFieldTypeIcon("date")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function zm(n){let e;function t(r,l){return r[8]?K$:G$}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,l){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},d(r){o.d(r),r&&k(e)}}}function G$(n){var s;let e,t,i,o,r,l=((s=n[1])==null?void 0:s.length)&&Hm(n);return{c(){e=g("tr"),t=g("td"),i=g("h6"),i.textContent="No records found.",o=$(),l&&l.c(),r=$(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,f){w(a,e,f),m(e,t),m(t,i),m(t,o),l&&l.m(t,null),m(e,r)},p(a,f){var c;(c=a[1])!=null&&c.length?l?l.p(a,f):(l=Hm(a),l.c(),l.m(t,null)):l&&(l.d(1),l=null)},d(a){a&&k(e),l&&l.d()}}}function K$(n){let e;return{c(){e=g("tr"),e.innerHTML=` + `},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function Hm(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[28]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function qm(n,e){let t,i,o;return i=new L1({props:{record:e[35],field:e[38]}}),{key:n,first:null,c(){t=lt(),V(i.$$.fragment),this.first=t},m(r,l){w(r,t,l),H(i,r,l),o=!0},p(r,l){e=r;const s={};l[0]&8&&(s.record=e[35]),l[0]&2048&&(s.field=e[38]),i.$set(s)},i(r){o||(T(i.$$.fragment,r),o=!0)},o(r){F(i.$$.fragment,r),o=!1},d(r){r&&k(t),q(i,r)}}}function Vm(n,e){let t,i,o,r,l,s,a,f,c,u,d,h,b,v=[],_=new Map,y,S,C,x,M,A,O,D,E,P,I,R;function G(){return e[25](e[35])}h=new Ls({props:{id:e[35].id}});let U=e[11];const z=W=>W[38].name;for(let W=0;W',E=$(),p(r,"type","checkbox"),p(r,"id",l="checkbox_"+e[35].id),r.checked=s=e[5][e[35].id],p(f,"for",c="checkbox_"+e[35].id),p(o,"class","form-field"),p(i,"class","bulk-select-col min-width"),p(d,"class","col-type-text col-field-id"),p(S,"class","col-type-date col-field-created"),p(M,"class","col-type-date col-field-updated"),p(D,"class","col-type-action min-width"),p(t,"tabindex","0"),p(t,"class","row-handle"),this.first=t},m(W,te){w(W,t,te),m(t,i),m(i,o),m(o,r),m(o,a),m(o,f),m(t,u),m(t,d),H(h,d,null),m(t,b);for(let ce=0;ceReset',u=$(),d=g("div"),h=$(),b=g("button"),b.innerHTML='Delete selected',p(t,"class","txt"),p(c,"type","button"),p(c,"class","btn btn-xs btn-secondary btn-outline p-l-5 p-r-5"),ne(c,"btn-disabled",n[9]),p(d,"class","flex-fill"),p(b,"type","button"),p(b,"class","btn btn-sm btn-secondary btn-danger"),ne(b,"btn-loading",n[9]),ne(b,"btn-disabled",n[9]),p(e,"class","bulkbar")},m(C,x){w(C,e,x),m(e,t),m(t,i),m(t,o),m(o,r),m(t,l),m(t,a),m(e,f),m(e,c),m(e,u),m(e,d),m(e,h),m(e,b),_=!0,y||(S=[X(c,"click",n[30]),X(b,"click",n[31])],y=!0)},p(C,x){(!_||x[0]&64)&&ge(r,C[6]),(!_||x[0]&64)&&s!==(s=C[6]===1?"record":"records")&&ge(a,s),x[0]&512&&ne(c,"btn-disabled",C[9]),x[0]&512&&ne(b,"btn-loading",C[9]),x[0]&512&&ne(b,"btn-disabled",C[9])},i(C){_||(C&&Dt(()=>{v||(v=ct(e,ti,{duration:150,y:5},!0)),v.run(1)}),_=!0)},o(C){C&&(v||(v=ct(e,ti,{duration:150,y:5},!1)),v.run(0)),_=!1},d(C){C&&k(e),C&&v&&v.end(),y=!1,rt(S)}}}function J$(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v=[],_=new Map,y,S,C,x,M,A,O,D,E,P,I=[],R=new Map,G,U,z,K,Y,W,te;function ce(re){n[21](re)}let ve={class:"col-type-text col-field-id",name:"id",$$slots:{default:[B$]},$$scope:{ctx:n}};n[0]!==void 0&&(ve.sort=n[0]),d=new en({props:ve}),he.push(()=>Fe(d,"sort",ce));let oe=n[11];const J=re=>re[38].name;for(let re=0;reFe(S,"sort",$e));function _e(re){n[24](re)}let fe={class:"col-type-date col-field-updated",name:"updated",$$slots:{default:[Y$]},$$scope:{ctx:n}};n[0]!==void 0&&(fe.sort=n[0]),M=new en({props:fe}),he.push(()=>Fe(M,"sort",_e));let ie=n[3];const ye=re=>re[35].id;for(let re=0;reh=!1)),d.$set(He),ke[0]&2049&&(oe=re[11],Ae(),v=st(v,ke,J,1,re,oe,_,o,Pt,jm,y,Nm),De());const qe={};ke[1]&4096&&(qe.$$scope={dirty:ke,ctx:re}),!C&&ke[0]&1&&(C=!0,qe.sort=re[0],Re(()=>C=!1)),S.$set(qe);const Je={};ke[1]&4096&&(Je.$$scope={dirty:ke,ctx:re}),!A&&ke[0]&1&&(A=!0,Je.sort=re[0],Re(()=>A=!1)),M.$set(Je),ke[0]&76074&&(ie=re[3],Ae(),I=st(I,ke,ye,1,re,ie,R,P,Pt,Vm,null,Im),De(),!ie.length&&Ne?Ne.p(re,ke):ie.length?Ne&&(Ne.d(1),Ne=null):(Ne=zm(re),Ne.c(),Ne.m(P,null))),ke[0]&256&&ne(t,"table-loading",re[8]),re[3].length?Pe?Pe.p(re,ke):(Pe=Bm(re),Pe.c(),Pe.m(U.parentNode,U)):Pe&&(Pe.d(1),Pe=null),re[3].length&&re[12]?ze?ze.p(re,ke):(ze=Um(re),ze.c(),ze.m(z.parentNode,z)):ze&&(ze.d(1),ze=null),re[6]?se?(se.p(re,ke),ke[0]&64&&T(se,1)):(se=Wm(re),se.c(),T(se,1),se.m(K.parentNode,K)):se&&(Ae(),F(se,1,1,()=>{se=null}),De())},i(re){if(!Y){T(d.$$.fragment,re);for(let ke=0;ke{oe<=1&&S(),t(8,v=!1),t(3,u=u.concat(J.items)),t(7,d=J.page),t(4,h=J.totalItems),s("load",u)}).catch(J=>{J!==null&&(t(8,v=!1),console.warn(J),S(),Se.errorResponseHandler(J,!1))})}function S(){t(3,u=[]),t(7,d=1),t(4,h=0),t(5,b={})}function C(){l?x():M()}function x(){t(5,b={})}function M(){for(const oe of u)t(5,b[oe.id]=oe,b);t(5,b)}function A(oe){b[oe.id]?delete b[oe.id]:t(5,b[oe.id]=oe,b),t(5,b)}function O(){xi(`Do you really want to delete the selected ${r===1?"record":"records"}?`,D)}async function D(){if(_||!r)return;let oe=[];for(const J of Object.keys(b))oe.push(Se.Records.delete(a==null?void 0:a.id,J));return t(9,_=!0),Promise.all(oe).then(()=>{hn(`Successfully deleted the selected ${r===1?"record":"records"}.`),x()}).catch(J=>{Se.errorResponseHandler(J)}).finally(()=>(t(9,_=!1),y()))}function E(oe){ft.call(this,n,oe)}const P=()=>C();function I(oe){f=oe,t(0,f)}function R(oe){f=oe,t(0,f)}function G(oe){f=oe,t(0,f)}function U(oe){f=oe,t(0,f)}const z=oe=>A(oe),K=oe=>s("select",oe),Y=(oe,J)=>{J.code==="Enter"&&(J.preventDefault(),s("select",oe))},W=()=>t(1,c=""),te=()=>y(d+1),ce=()=>x(),ve=()=>O();return n.$$set=oe=>{"collection"in oe&&t(18,a=oe.collection),"sort"in oe&&t(0,f=oe.sort),"filter"in oe&&t(1,c=oe.filter)},n.$$.update=()=>{n.$$.dirty[0]&262147&&a&&a.id&&f!==-1&&c!==-1&&(S(),y(1)),n.$$.dirty[0]&24&&t(12,i=h>u.length),n.$$.dirty[0]&262144&&t(11,o=(a==null?void 0:a.schema)||[]),n.$$.dirty[0]&32&&t(6,r=Object.keys(b).length),n.$$.dirty[0]&72&&t(10,l=u.length&&r===u.length)},[f,c,y,u,h,b,r,d,v,_,l,o,i,s,C,x,A,O,a,E,P,I,R,G,U,z,K,Y,W,te,ce,ve]}class X$ extends Ie{constructor(e){super(),Le(this,e,Z$,J$,Ee,{collection:18,sort:0,filter:1,load:2},null,[-1,-1])}get load(){return this.$$.ctx[2]}}function Q$(n){let e,t,i,o,r,l,s,a,f=n[2].name+"",c,u,d,h,b,v,_,y,S,C,x,M,A,O,D,E,P;e=new Jx({}),C=new Ds({props:{value:n[0],autocompleteCollection:n[2]}}),C.$on("submit",n[15]);function I(U){n[17](U)}function R(U){n[18](U)}let G={collection:n[2]};return n[0]!==void 0&&(G.filter=n[0]),n[1]!==void 0&&(G.sort=n[1]),M=new X$({props:G}),n[16](M),he.push(()=>Fe(M,"filter",I)),he.push(()=>Fe(M,"sort",R)),M.$on("select",n[19]),{c(){V(e.$$.fragment),t=$(),i=g("main"),o=g("header"),r=g("nav"),l=g("div"),l.textContent="Collections",s=$(),a=g("div"),c=j(f),u=$(),d=g("button"),d.innerHTML='',h=$(),b=g("div"),v=g("button"),v.innerHTML=` + API Preview`,_=$(),y=g("button"),y.innerHTML=` + New record`,S=$(),V(C.$$.fragment),x=$(),V(M.$$.fragment),p(l,"class","breadcrumb-item"),p(a,"class","breadcrumb-item"),p(r,"class","breadcrumbs"),p(d,"type","button"),p(d,"class","btn btn-secondary btn-circle"),p(v,"type","button"),p(v,"class","btn btn-outline"),p(y,"type","button"),p(y,"class","btn btn-expanded"),p(b,"class","btns-group"),p(o,"class","page-header"),p(i,"class","page-wrapper")},m(U,z){H(e,U,z),w(U,t,z),w(U,i,z),m(i,o),m(o,r),m(r,l),m(r,s),m(r,a),m(a,c),m(o,u),m(o,d),m(o,h),m(o,b),m(b,v),m(b,_),m(b,y),m(i,S),H(C,i,null),m(i,x),H(M,i,null),D=!0,E||(P=[Xe(St.call(null,d,{text:"Edit collection",position:"right"})),X(d,"click",n[12]),X(v,"click",n[13]),X(y,"click",n[14])],E=!0)},p(U,z){(!D||z&4)&&f!==(f=U[2].name+"")&&ge(c,f);const K={};z&1&&(K.value=U[0]),z&4&&(K.autocompleteCollection=U[2]),C.$set(K);const Y={};z&4&&(Y.collection=U[2]),!A&&z&1&&(A=!0,Y.filter=U[0],Re(()=>A=!1)),!O&&z&2&&(O=!0,Y.sort=U[1],Re(()=>O=!1)),M.$set(Y)},i(U){D||(T(e.$$.fragment,U),T(C.$$.fragment,U),T(M.$$.fragment,U),D=!0)},o(U){F(e.$$.fragment,U),F(C.$$.fragment,U),F(M.$$.fragment,U),D=!1},d(U){q(e,U),U&&k(t),U&&k(i),q(C),n[16](null),q(M),E=!1,rt(P)}}}function eA(n){let e,t,i,o,r,l,s,a;return{c(){e=g("div"),t=g("div"),t.innerHTML='',i=$(),o=g("h1"),o.textContent="Create your first collection to add records!",r=$(),l=g("button"),l.innerHTML=` + Create new collection`,p(t,"class","icon"),p(o,"class","m-b-10"),p(l,"type","button"),p(l,"class","btn btn-expanded-lg btn-lg"),p(e,"class","placeholder-section m-b-base")},m(f,c){w(f,e,c),m(e,t),m(e,i),m(e,o),m(e,r),m(e,l),s||(a=X(l,"click",n[11]),s=!0)},p:le,i:le,o:le,d(f){f&&k(e),s=!1,a()}}}function tA(n){let e;return{c(){e=g("div"),e.innerHTML=` +

    Loading collections...

    `,p(e,"class","placeholder-section m-b-base")},m(t,i){w(t,e,i)},p:le,i:le,o:le,d(t){t&&k(e)}}}function nA(n){let e,t,i,o,r,l,s,a,f;const c=[tA,eA,Q$],u=[];function d(_,y){return _[8]?0:_[7].length?2:1}e=d(n),t=u[e]=c[e](n);let h={};o=new hc({props:h}),n[20](o);let b={};l=new q6({props:b}),n[21](l);let v={collection:n[2]};return a=new F1({props:v}),n[22](a),a.$on("save",n[23]),a.$on("delete",n[24]),{c(){t.c(),i=$(),V(o.$$.fragment),r=$(),V(l.$$.fragment),s=$(),V(a.$$.fragment)},m(_,y){u[e].m(_,y),w(_,i,y),H(o,_,y),w(_,r,y),H(l,_,y),w(_,s,y),H(a,_,y),f=!0},p(_,[y]){let S=e;e=d(_),e===S?u[e].p(_,y):(Ae(),F(u[S],1,1,()=>{u[S]=null}),De(),t=u[e],t?t.p(_,y):(t=u[e]=c[e](_),t.c()),T(t,1),t.m(i.parentNode,i));const C={};o.$set(C);const x={};l.$set(x);const M={};y&4&&(M.collection=_[2]),a.$set(M)},i(_){f||(T(t),T(o.$$.fragment,_),T(l.$$.fragment,_),T(a.$$.fragment,_),f=!0)},o(_){F(t),F(o.$$.fragment,_),F(l.$$.fragment,_),F(a.$$.fragment,_),f=!1},d(_){u[e].d(_),_&&k(i),n[20](null),q(o,_),_&&k(r),n[21](null),q(l,_),_&&k(s),n[22](null),q(a,_)}}}function iA(n,e,t){var G;let i,o,r,l;pn(n,fi,U=>t(2,o=U)),pn(n,Go,U=>t(10,r=U)),pn(n,pf,U=>t(8,l=U));const s=B.getQueryParams((G=window.location)==null?void 0:G.href);let a,f,c,u,d=s.filter||"",h=s.sort||"-created",b=s.collectionId;B.setDocumentTitle("Collections"),_C(b);const v=()=>a==null?void 0:a.show(),_=()=>a==null?void 0:a.show(o),y=()=>f==null?void 0:f.show(o),S=()=>c==null?void 0:c.show(),C=U=>t(0,d=U.detail);function x(U){he[U?"unshift":"push"](()=>{u=U,t(6,u)})}function M(U){d=U,t(0,d),t(2,o),t(9,b)}function A(U){h=U,t(1,h),t(2,o),t(9,b)}const O=U=>c==null?void 0:c.show(U==null?void 0:U.detail);function D(U){he[U?"unshift":"push"](()=>{a=U,t(3,a)})}function E(U){he[U?"unshift":"push"](()=>{f=U,t(4,f)})}function P(U){he[U?"unshift":"push"](()=>{c=U,t(5,c)})}const I=()=>u==null?void 0:u.load(),R=()=>u==null?void 0:u.load();return n.$$.update=()=>{n.$$.dirty&1024&&t(7,i=r.filter(U=>U.name!="profiles")),n.$$.dirty&516&&(o==null?void 0:o.id)&&b!=o.id&&(t(9,b=o.id),t(1,h="-created"),t(0,d="")),n.$$.dirty&7&&(h||d||(o==null?void 0:o.id))&&B.replaceClientQueryParams({collectionId:o==null?void 0:o.id,filter:d,sort:h})},[d,h,o,a,f,c,u,i,l,b,r,v,_,y,S,C,x,M,A,O,D,E,P,I,R]}class oA extends Ie{constructor(e){super(),Le(this,e,iA,nA,Ee,{})}}function Ym(n){let e,t;return e=new je({props:{class:"form-field disabled",name:"id",$$slots:{default:[rA,({uniqueId:i})=>({31:i}),({uniqueId:i})=>[0,i?1:0]]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,o){const r={};o[0]&2|o[1]&3&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function rA(n){let e,t,i,o,r,l,s,a,f;return{c(){e=g("label"),t=g("i"),i=$(),o=g("span"),o.textContent="ID",l=$(),s=g("input"),p(t,"class",B.getFieldTypeIcon("primary")),p(o,"class","txt"),p(e,"for",r=n[31]),p(s,"type","text"),p(s,"id",a=n[31]),s.value=f=n[1].id,s.disabled=!0},m(c,u){w(c,e,u),m(e,t),m(e,i),m(e,o),w(c,l,u),w(c,s,u)},p(c,u){u[1]&1&&r!==(r=c[31])&&p(e,"for",r),u[1]&1&&a!==(a=c[31])&&p(s,"id",a),u[0]&2&&f!==(f=c[1].id)&&s.value!==f&&(s.value=f)},d(c){c&&k(e),c&&k(l),c&&k(s)}}}function Gm(n){let e,t,i,o;return{c(){e=g("div"),e.innerHTML='',p(e,"class","form-field-addon txt-success")},m(r,l){w(r,e,l),i||(o=Xe(t=St.call(null,e,"Verified")),i=!0)},d(r){r&&k(e),i=!1,o()}}}function lA(n){let e,t,i,o,r,l,s,a,f,c,u,d=n[1].verified&&Gm();return{c(){e=g("label"),t=g("i"),i=$(),o=g("span"),o.textContent="Email",l=$(),d&&d.c(),s=$(),a=g("input"),p(t,"class",B.getFieldTypeIcon("email")),p(o,"class","txt"),p(e,"for",r=n[31]),p(a,"type","email"),p(a,"autocomplete","off"),p(a,"id",f=n[31]),a.required=!0},m(h,b){w(h,e,b),m(e,t),m(e,i),m(e,o),w(h,l,b),d&&d.m(h,b),w(h,s,b),w(h,a,b),Me(a,n[2]),c||(u=X(a,"input",n[19]),c=!0)},p(h,b){b[1]&1&&r!==(r=h[31])&&p(e,"for",r),h[1].verified?d||(d=Gm(),d.c(),d.m(s.parentNode,s)):d&&(d.d(1),d=null),b[1]&1&&f!==(f=h[31])&&p(a,"id",f),b[0]&4&&a.value!==h[2]&&Me(a,h[2])},d(h){h&&k(e),h&&k(l),d&&d.d(h),h&&k(s),h&&k(a),c=!1,u()}}}function Km(n){let e,t;return e=new je({props:{class:"form-field form-field-toggle",$$slots:{default:[sA,({uniqueId:i})=>({31:i}),({uniqueId:i})=>[0,i?1:0]]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,o){const r={};o[0]&8|o[1]&3&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function sA(n){let e,t,i,o,r,l,s,a;return{c(){e=g("input"),i=$(),o=g("label"),r=j("Change password"),p(e,"type","checkbox"),p(e,"id",t=n[31]),p(o,"for",l=n[31])},m(f,c){w(f,e,c),e.checked=n[3],w(f,i,c),w(f,o,c),m(o,r),s||(a=X(e,"change",n[20]),s=!0)},p(f,c){c[1]&1&&t!==(t=f[31])&&p(e,"id",t),c[0]&8&&(e.checked=f[3]),c[1]&1&&l!==(l=f[31])&&p(o,"for",l)},d(f){f&&k(e),f&&k(i),f&&k(o),s=!1,a()}}}function Jm(n){let e,t,i,o,r,l,s,a,f;return o=new je({props:{class:"form-field required",name:"password",$$slots:{default:[aA,({uniqueId:c})=>({31:c}),({uniqueId:c})=>[0,c?1:0]]},$$scope:{ctx:n}}}),s=new je({props:{class:"form-field required",name:"passwordConfirm",$$slots:{default:[fA,({uniqueId:c})=>({31:c}),({uniqueId:c})=>[0,c?1:0]]},$$scope:{ctx:n}}}),{c(){e=g("div"),t=g("div"),i=g("div"),V(o.$$.fragment),r=$(),l=g("div"),V(s.$$.fragment),p(i,"class","col-sm-6"),p(l,"class","col-sm-6"),p(t,"class","grid"),p(e,"class","col-12")},m(c,u){w(c,e,u),m(e,t),m(t,i),H(o,i,null),m(t,r),m(t,l),H(s,l,null),f=!0},p(c,u){const d={};u[0]&128|u[1]&3&&(d.$$scope={dirty:u,ctx:c}),o.$set(d);const h={};u[0]&256|u[1]&3&&(h.$$scope={dirty:u,ctx:c}),s.$set(h)},i(c){f||(T(o.$$.fragment,c),T(s.$$.fragment,c),Dt(()=>{a||(a=ct(t,fn,{duration:150},!0)),a.run(1)}),f=!0)},o(c){F(o.$$.fragment,c),F(s.$$.fragment,c),a||(a=ct(t,fn,{duration:150},!1)),a.run(0),f=!1},d(c){c&&k(e),q(o),q(s),c&&a&&a.end()}}}function aA(n){let e,t,i,o,r,l,s,a,f,c;return{c(){e=g("label"),t=g("i"),i=$(),o=g("span"),o.textContent="Password",l=$(),s=g("input"),p(t,"class","ri-lock-line"),p(o,"class","txt"),p(e,"for",r=n[31]),p(s,"type","password"),p(s,"autocomplete","new-password"),p(s,"id",a=n[31]),s.required=!0},m(u,d){w(u,e,d),m(e,t),m(e,i),m(e,o),w(u,l,d),w(u,s,d),Me(s,n[7]),f||(c=X(s,"input",n[21]),f=!0)},p(u,d){d[1]&1&&r!==(r=u[31])&&p(e,"for",r),d[1]&1&&a!==(a=u[31])&&p(s,"id",a),d[0]&128&&s.value!==u[7]&&Me(s,u[7])},d(u){u&&k(e),u&&k(l),u&&k(s),f=!1,c()}}}function fA(n){let e,t,i,o,r,l,s,a,f,c;return{c(){e=g("label"),t=g("i"),i=$(),o=g("span"),o.textContent="Password confirm",l=$(),s=g("input"),p(t,"class","ri-lock-line"),p(o,"class","txt"),p(e,"for",r=n[31]),p(s,"type","password"),p(s,"autocomplete","new-password"),p(s,"id",a=n[31]),s.required=!0},m(u,d){w(u,e,d),m(e,t),m(e,i),m(e,o),w(u,l,d),w(u,s,d),Me(s,n[8]),f||(c=X(s,"input",n[22]),f=!0)},p(u,d){d[1]&1&&r!==(r=u[31])&&p(e,"for",r),d[1]&1&&a!==(a=u[31])&&p(s,"id",a),d[0]&256&&s.value!==u[8]&&Me(s,u[8])},d(u){u&&k(e),u&&k(l),u&&k(s),f=!1,c()}}}function Zm(n){let e,t;return e=new je({props:{class:"form-field form-field-toggle",$$slots:{default:[cA,({uniqueId:i})=>({31:i}),({uniqueId:i})=>[0,i?1:0]]},$$scope:{ctx:n}}}),{c(){V(e.$$.fragment)},m(i,o){H(e,i,o),t=!0},p(i,o){const r={};o[0]&512|o[1]&3&&(r.$$scope={dirty:o,ctx:i}),e.$set(r)},i(i){t||(T(e.$$.fragment,i),t=!0)},o(i){F(e.$$.fragment,i),t=!1},d(i){q(e,i)}}}function cA(n){let e,t,i,o,r,l,s,a;return{c(){e=g("input"),i=$(),o=g("label"),r=j("Send verification email"),p(e,"type","checkbox"),p(e,"id",t=n[31]),p(o,"for",l=n[31])},m(f,c){w(f,e,c),e.checked=n[9],w(f,i,c),w(f,o,c),m(o,r),s||(a=X(e,"change",n[23]),s=!0)},p(f,c){c[1]&1&&t!==(t=f[31])&&p(e,"id",t),c[0]&512&&(e.checked=f[9]),c[1]&1&&l!==(l=f[31])&&p(o,"for",l)},d(f){f&&k(e),f&&k(i),f&&k(o),s=!1,a()}}}function uA(n){let e,t,i,o,r,l,s,a,f,c=!n[1].isNew&&Ym(n);i=new je({props:{class:"form-field required",name:"email",$$slots:{default:[lA,({uniqueId:b})=>({31:b}),({uniqueId:b})=>[0,b?1:0]]},$$scope:{ctx:n}}});let u=!n[1].isNew&&Km(n),d=(n[1].isNew||n[3])&&Jm(n),h=n[1].isNew&&Zm(n);return{c(){e=g("form"),c&&c.c(),t=$(),V(i.$$.fragment),o=$(),u&&u.c(),r=$(),d&&d.c(),l=$(),h&&h.c(),p(e,"id",n[11]),p(e,"class","grid"),p(e,"autocomplete","off")},m(b,v){w(b,e,v),c&&c.m(e,null),m(e,t),H(i,e,null),m(e,o),u&&u.m(e,null),m(e,r),d&&d.m(e,null),m(e,l),h&&h.m(e,null),s=!0,a||(f=X(e,"submit",Gt(n[12])),a=!0)},p(b,v){b[1].isNew?c&&(Ae(),F(c,1,1,()=>{c=null}),De()):c?(c.p(b,v),v[0]&2&&T(c,1)):(c=Ym(b),c.c(),T(c,1),c.m(e,t));const _={};v[0]&6|v[1]&3&&(_.$$scope={dirty:v,ctx:b}),i.$set(_),b[1].isNew?u&&(Ae(),F(u,1,1,()=>{u=null}),De()):u?(u.p(b,v),v[0]&2&&T(u,1)):(u=Km(b),u.c(),T(u,1),u.m(e,r)),b[1].isNew||b[3]?d?(d.p(b,v),v[0]&10&&T(d,1)):(d=Jm(b),d.c(),T(d,1),d.m(e,l)):d&&(Ae(),F(d,1,1,()=>{d=null}),De()),b[1].isNew?h?(h.p(b,v),v[0]&2&&T(h,1)):(h=Zm(b),h.c(),T(h,1),h.m(e,null)):h&&(Ae(),F(h,1,1,()=>{h=null}),De())},i(b){s||(T(c),T(i.$$.fragment,b),T(u),T(d),T(h),s=!0)},o(b){F(c),F(i.$$.fragment,b),F(u),F(d),F(h),s=!1},d(b){b&&k(e),c&&c.d(),q(i),u&&u.d(),d&&d.d(),h&&h.d(),a=!1,f()}}}function dA(n){let e,t=n[1].isNew?"New user":"Edit user",i;return{c(){e=g("h4"),i=j(t)},m(o,r){w(o,e,r),m(e,i)},p(o,r){r[0]&2&&t!==(t=o[1].isNew?"New user":"Edit user")&&ge(i,t)},d(o){o&&k(e)}}}function Xm(n){let e,t,i,o,r,l,s,a,f;return l=new vo({props:{class:"dropdown dropdown-upside dropdown-left dropdown-nowrap",$$slots:{default:[pA]},$$scope:{ctx:n}}}),{c(){e=g("button"),t=g("span"),i=$(),o=g("i"),r=$(),V(l.$$.fragment),s=$(),a=g("div"),p(o,"class","ri-more-line"),p(e,"type","button"),p(e,"class","btn btn-sm btn-circle btn-secondary"),p(a,"class","flex-fill")},m(c,u){w(c,e,u),m(e,t),m(e,i),m(e,o),m(e,r),H(l,e,null),w(c,s,u),w(c,a,u),f=!0},p(c,u){const d={};u[0]&2|u[1]&2&&(d.$$scope={dirty:u,ctx:c}),l.$set(d)},i(c){f||(T(l.$$.fragment,c),f=!0)},o(c){F(l.$$.fragment,c),f=!1},d(c){c&&k(e),q(l),c&&k(s),c&&k(a)}}}function Qm(n){let e,t,i;return{c(){e=g("button"),e.innerHTML=` + Send verification email`,p(e,"type","button"),p(e,"class","dropdown-item")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[16]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function pA(n){let e,t,i,o,r=!n[1].verified&&Qm(n);return{c(){r&&r.c(),e=$(),t=g("button"),t.innerHTML=` + Delete`,p(t,"type","button"),p(t,"class","dropdown-item")},m(l,s){r&&r.m(l,s),w(l,e,s),w(l,t,s),i||(o=X(t,"click",n[17]),i=!0)},p(l,s){l[1].verified?r&&(r.d(1),r=null):r?r.p(l,s):(r=Qm(l),r.c(),r.m(e.parentNode,e))},d(l){r&&r.d(l),l&&k(e),l&&k(t),i=!1,o()}}}function hA(n){let e,t,i,o,r,l,s=n[1].isNew?"Create":"Save changes",a,f,c,u,d,h=!n[1].isNew&&Xm(n);return{c(){h&&h.c(),e=$(),t=g("button"),i=g("span"),i.textContent="Cancel",o=$(),r=g("button"),l=g("span"),a=j(s),p(i,"class","txt"),p(t,"type","button"),p(t,"class","btn btn-secondary"),t.disabled=n[5],p(l,"class","txt"),p(r,"type","submit"),p(r,"form",n[11]),p(r,"class","btn btn-expanded"),r.disabled=f=!n[10]||n[5],ne(r,"btn-loading",n[5])},m(b,v){h&&h.m(b,v),w(b,e,v),w(b,t,v),m(t,i),w(b,o,v),w(b,r,v),m(r,l),m(l,a),c=!0,u||(d=X(t,"click",n[18]),u=!0)},p(b,v){b[1].isNew?h&&(Ae(),F(h,1,1,()=>{h=null}),De()):h?(h.p(b,v),v[0]&2&&T(h,1)):(h=Xm(b),h.c(),T(h,1),h.m(e.parentNode,e)),(!c||v[0]&32)&&(t.disabled=b[5]),(!c||v[0]&2)&&s!==(s=b[1].isNew?"Create":"Save changes")&&ge(a,s),(!c||v[0]&1056&&f!==(f=!b[10]||b[5]))&&(r.disabled=f),v[0]&32&&ne(r,"btn-loading",b[5])},i(b){c||(T(h),c=!0)},o(b){F(h),c=!1},d(b){h&&h.d(b),b&&k(e),b&&k(t),b&&k(o),b&&k(r),u=!1,d()}}}function mA(n){let e,t,i={popup:!0,class:"user-panel",beforeHide:n[24],$$slots:{footer:[hA],header:[dA],default:[uA]},$$scope:{ctx:n}};return e=new Ai({props:i}),n[25](e),e.$on("hide",n[26]),e.$on("show",n[27]),{c(){V(e.$$.fragment)},m(o,r){H(e,o,r),t=!0},p(o,r){const l={};r[0]&1088&&(l.beforeHide=o[24]),r[0]&1966|r[1]&2&&(l.$$scope={dirty:r,ctx:o}),e.$set(l)},i(o){t||(T(e.$$.fragment,o),t=!0)},o(o){F(e.$$.fragment,o),t=!1},d(o){n[25](null),q(e,o)}}}function bA(n,e,t){let i;const o=yn(),r="user_"+B.randomString(5);let l,s=new es,a=!1,f=!1,c="",u="",d="",h=!1,b=!0;function v(W){return y(W),t(6,f=!0),l==null?void 0:l.show()}function _(){return l==null?void 0:l.hide()}function y(W){Ui({}),t(1,s=W!=null&&W.clone?W.clone():new es),S()}function S(){t(3,h=!1),t(9,b=!0),t(2,c=(s==null?void 0:s.email)||""),t(7,u=""),t(8,d="")}function C(){if(a||!i)return;t(5,a=!0);const W={email:c};(s.isNew||h)&&(W.password=u,W.passwordConfirm=d);let te;s.isNew?te=Se.Users.create(W):te=Se.Users.update(s.id,W),te.then(async ce=>{b&&M(!1),t(6,f=!1),_(),hn(s.isNew?"Successfully created user.":"Successfully updated user."),o("save",ce)}).catch(ce=>{Se.errorResponseHandler(ce)}).finally(()=>{t(5,a=!1)})}function x(){!(s!=null&&s.id)||xi("Do you really want to delete the selected user?",()=>Se.Users.delete(s.id).then(()=>{t(6,f=!1),_(),hn("Successfully deleted user."),o("delete",s)}).catch(W=>{Se.errorResponseHandler(W)}))}function M(W=!0){return Se.Users.requestVerification(s.isNew?c:s.email).then(()=>{t(6,f=!1),_(),W&&hn(`Successfully sent verification email to ${s.email}.`)}).catch(te=>{Se.errorResponseHandler(te)})}const A=()=>M(),O=()=>x(),D=()=>_();function E(){c=this.value,t(2,c)}function P(){h=this.checked,t(3,h)}function I(){u=this.value,t(7,u)}function R(){d=this.value,t(8,d)}function G(){b=this.checked,t(9,b)}const U=()=>i&&f?(xi("You have unsaved changes. Do you really want to close the panel?",()=>{t(6,f=!1),_()}),!1):!0;function z(W){he[W?"unshift":"push"](()=>{l=W,t(4,l)})}function K(W){ft.call(this,n,W)}function Y(W){ft.call(this,n,W)}return n.$$.update=()=>{n.$$.dirty[0]&14&&t(10,i=s.isNew&&c!=""||h||c!==s.email)},[_,s,c,h,l,a,f,u,d,b,i,r,C,x,M,v,A,O,D,E,P,I,R,G,U,z,K,Y]}class gA extends Ie{constructor(e){super(),Le(this,e,bA,mA,Ee,{show:15,hide:0},null,[-1,-1])}get show(){return this.$$.ctx[15]}get hide(){return this.$$.ctx[0]}}function eb(n,e,t){const i=n.slice();return i[37]=e[t],i}function tb(n,e,t){const i=n.slice();return i[40]=e[t],i}function nb(n,e,t){const i=n.slice();return i[40]=e[t],i}function _A(n){let e,t,i,o,r,l,s,a,f,c,u,d,h,b,v,_,y,S,C,x,M,A,O=[],D=new Map,E,P,I,R,G,U,z,K,Y,W,te=[],ce=new Map,ve,oe,J,$e,ee;u=new Ds({props:{value:n[3],placeholder:"Search filter, eg. verified=1",extraAutocompleteKeys:["verified","email"]}}),u.$on("submit",n[17]);function _e(Z){n[18](Z)}let fe={class:"col-type-text col-field-id",name:"id",$$slots:{default:[yA]},$$scope:{ctx:n}};n[4]!==void 0&&(fe.sort=n[4]),y=new en({props:fe}),he.push(()=>Fe(y,"sort",_e));function ie(Z){n[19](Z)}let ye={class:"col-type-email col-field-email",name:"email",$$slots:{default:[kA]},$$scope:{ctx:n}};n[4]!==void 0&&(ye.sort=n[4]),x=new en({props:ye}),he.push(()=>Fe(x,"sort",ie));let Ne=n[12];const Pe=Z=>Z[40].name;for(let Z=0;ZFe(P,"sort",ze));function re(Z){n[21](Z)}let ke={class:"col-type-date col-field-updated",name:"updated",$$slots:{default:[SA]},$$scope:{ctx:n}};n[4]!==void 0&&(ke.sort=n[4]),G=new en({props:ke}),he.push(()=>Fe(G,"sort",re));let He=n[1];const qe=Z=>Z[37].id;for(let Z=0;ZUsers',o=$(),r=g("button"),r.innerHTML='',l=$(),s=g("div"),a=$(),f=g("button"),f.innerHTML=` + New user`,c=$(),V(u.$$.fragment),d=$(),h=g("div"),b=g("table"),v=g("thead"),_=g("tr"),V(y.$$.fragment),C=$(),V(x.$$.fragment),A=$();for(let Z=0;ZS=!1)),y.$set(yt);const it={};ae[1]&16384&&(it.$$scope={dirty:ae,ctx:Z}),!M&&ae[0]&16&&(M=!0,it.sort=Z[4],Re(()=>M=!1)),x.$set(it),ae[0]&4096&&(Ne=Z[12],O=st(O,ae,Pe,1,Z,Ne,D,_,an,ib,E,nb));const bt={};ae[1]&16384&&(bt.$$scope={dirty:ae,ctx:Z}),!I&&ae[0]&16&&(I=!0,bt.sort=Z[4],Re(()=>I=!1)),P.$set(bt);const at={};ae[1]&16384&&(at.$$scope={dirty:ae,ctx:Z}),!U&&ae[0]&16&&(U=!0,at.sort=Z[4],Re(()=>U=!1)),G.$set(at),ae[0]&5450&&(He=Z[1],Ae(),te=st(te,ae,qe,1,Z,He,ce,W,Pt,sb,null,eb),De(),!He.length&&Je?Je.p(Z,ae):He.length?Je&&(Je.d(1),Je=null):(Je=ob(Z),Je.c(),Je.m(W,null))),ae[0]&1024&&ne(b,"table-loading",Z[10]),Z[1].length?be?be.p(Z,ae):(be=ab(Z),be.c(),be.m(e,oe)):be&&(be.d(1),be=null),Z[1].length&&Z[13]?Oe?Oe.p(Z,ae):(Oe=fb(Z),Oe.c(),Oe.m(e,null)):Oe&&(Oe.d(1),Oe=null)},i(Z){if(!J){T(u.$$.fragment,Z),T(y.$$.fragment,Z),T(x.$$.fragment,Z),T(P.$$.fragment,Z),T(G.$$.fragment,Z);for(let ae=0;ae +

    Loading users...

    `,p(e,"class","placeholder-section m-b-base")},m(t,i){w(t,e,i)},p:le,i:le,o:le,d(t){t&&k(e)}}}function yA(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="id",p(t,"class",B.getFieldTypeIcon("primary")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function kA(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="email",p(t,"class",B.getFieldTypeIcon("email")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function ib(n,e){let t,i,o,r,l,s,a,f=e[40].name+"",c,u,d;return{key:n,first:null,c(){t=g("th"),i=g("div"),o=g("i"),l=$(),s=g("span"),a=j("profile."),c=j(f),p(o,"class",r=B.getFieldTypeIcon(e[40].type)),p(s,"class","txt"),p(i,"class","col-header-content"),p(t,"class",u="col-type-"+e[40].type+" col-field-"+e[40].name),p(t,"name",d=e[40].name),this.first=t},m(h,b){w(h,t,b),m(t,i),m(i,o),m(i,l),m(i,s),m(s,a),m(s,c)},p(h,b){e=h,b[0]&4096&&r!==(r=B.getFieldTypeIcon(e[40].type))&&p(o,"class",r),b[0]&4096&&f!==(f=e[40].name+"")&&ge(c,f),b[0]&4096&&u!==(u="col-type-"+e[40].type+" col-field-"+e[40].name)&&p(t,"class",u),b[0]&4096&&d!==(d=e[40].name)&&p(t,"name",d)},d(h){h&&k(t)}}}function wA(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="created",p(t,"class",B.getFieldTypeIcon("date")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function SA(n){let e,t,i,o;return{c(){e=g("div"),t=g("i"),i=$(),o=g("span"),o.textContent="updated",p(t,"class",B.getFieldTypeIcon("date")),p(o,"class","txt"),p(e,"class","col-header-content")},m(r,l){w(r,e,l),m(e,t),m(e,i),m(e,o)},p:le,d(r){r&&k(e)}}}function ob(n){let e;function t(r,l){return r[10]?xA:CA}let i=t(n),o=i(n);return{c(){o.c(),e=lt()},m(r,l){o.m(r,l),w(r,e,l)},p(r,l){i===(i=t(r))&&o?o.p(r,l):(o.d(1),o=i(r),o&&(o.c(),o.m(e.parentNode,e)))},d(r){o.d(r),r&&k(e)}}}function CA(n){var s;let e,t,i,o,r,l=((s=n[3])==null?void 0:s.length)&&rb(n);return{c(){e=g("tr"),t=g("td"),i=g("h6"),i.textContent="No users found.",o=$(),l&&l.c(),r=$(),p(t,"colspan","99"),p(t,"class","txt-center txt-hint p-xs")},m(a,f){w(a,e,f),m(e,t),m(t,i),m(t,o),l&&l.m(t,null),m(e,r)},p(a,f){var c;(c=a[3])!=null&&c.length?l?l.p(a,f):(l=rb(a),l.c(),l.m(t,null)):l&&(l.d(1),l=null)},d(a){a&&k(e),l&&l.d()}}}function xA(n){let e;return{c(){e=g("tr"),e.innerHTML=` + `},m(t,i){w(t,e,i)},p:le,d(t){t&&k(e)}}}function rb(n){let e,t,i;return{c(){e=g("button"),e.innerHTML='Clear filters',p(e,"type","button"),p(e,"class","btn btn-hint btn-expanded m-t-sm")},m(o,r){w(o,e,r),t||(i=X(e,"click",n[24]),t=!0)},p:le,d(o){o&&k(e),t=!1,i()}}}function lb(n,e){let t,i,o;return i=new L1({props:{field:e[40],record:e[37].profile||{}}}),{key:n,first:null,c(){t=lt(),V(i.$$.fragment),this.first=t},m(r,l){w(r,t,l),H(i,r,l),o=!0},p(r,l){e=r;const s={};l[0]&4096&&(s.field=e[40]),l[0]&2&&(s.record=e[37].profile||{}),i.$set(s)},i(r){o||(T(i.$$.fragment,r),o=!0)},o(r){F(i.$$.fragment,r),o=!1},d(r){r&&k(t),q(i,r)}}}function sb(n,e){let t,i,o,r,l,s,a,f=e[37].email+"",c,u,d,h,b=e[37].verified?"Verified":"Unverified",v,_,y=[],S=new Map,C,x,M,A,O,D,E,P,I,R,G,U,z,K,Y;o=new Ls({props:{id:e[37].id}});let W=e[12];const te=oe=>oe[40].name;for(let oe=0;oe