@x47base/pocketbase-addon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.dockerignore +9 -0
  2. package/Dockerfile +26 -0
  3. package/FEATURES.md +32 -0
  4. package/LICENSE.md +17 -0
  5. package/MIGRATION.md +49 -0
  6. package/NOTICE.md +5 -0
  7. package/README.md +26 -0
  8. package/adapter.go +31 -0
  9. package/admin/register.go +60 -0
  10. package/admin/register_test.go +37 -0
  11. package/backups/backup_encryption_test.go +82 -0
  12. package/backups/encryption.go +138 -0
  13. package/backups/integration_test.go +51 -0
  14. package/backups/register.go +120 -0
  15. package/backups/restore.go +119 -0
  16. package/backups/s3_test.go +52 -0
  17. package/backups/swap.go +53 -0
  18. package/backups/swap_test.go +75 -0
  19. package/backups/upload.go +52 -0
  20. package/bin/pocketbase-extension.mjs +25 -0
  21. package/cmd/edge/main.go +123 -0
  22. package/cmd/import-fork/main.go +37 -0
  23. package/cmd/loadtest/main.go +61 -0
  24. package/cmd/loadtest/sandbox.go +73 -0
  25. package/cmd/loadtest/sandbox_test.go +20 -0
  26. package/cmd/pocketbase/main.go +78 -0
  27. package/deploy/README.md +148 -0
  28. package/deploy/app/hooks/README.md +2 -0
  29. package/deploy/app/migrations/1789000000_notes.js +16 -0
  30. package/deploy/app/public/README.md +2 -0
  31. package/deploy/compose.secrets.yaml +8 -0
  32. package/deploy/compose.yaml +68 -0
  33. package/deploy/edge.json +13 -0
  34. package/edge/gateway.go +295 -0
  35. package/edge/gateway_test.go +296 -0
  36. package/edge/openapi.json +1 -0
  37. package/edge/policy.go +150 -0
  38. package/features/collection_singleton.go +13 -0
  39. package/features/collection_singleton_test.go +46 -0
  40. package/features/dimensions_test.go +65 -0
  41. package/features/duplicate.go +178 -0
  42. package/features/duplicate_test.go +128 -0
  43. package/features/field_color.go +46 -0
  44. package/features/field_date_only.go +39 -0
  45. package/features/field_json_schema.go +92 -0
  46. package/features/field_scalar_extensions_test.go +66 -0
  47. package/features/files.go +36 -0
  48. package/features/filter_has_any_test.go +81 -0
  49. package/features/generate_test.go +72 -0
  50. package/features/has_any_visibility_test.go +62 -0
  51. package/features/json.go +50 -0
  52. package/features/membership.go +64 -0
  53. package/features/register.go +45 -0
  54. package/features/schema_test.go +58 -0
  55. package/features/ui/main.js +133 -0
  56. package/features/ui/settings.js +31 -0
  57. package/go.mod +54 -0
  58. package/go.sum +159 -0
  59. package/internal/archive/create.go +91 -0
  60. package/internal/archive/create_test.go +125 -0
  61. package/internal/archive/extract.go +99 -0
  62. package/internal/archive/extract_test.go +88 -0
  63. package/jsvm/binds.go +1273 -0
  64. package/jsvm/binds_app_reset_test.go +314 -0
  65. package/jsvm/binds_test.go +1870 -0
  66. package/jsvm/form_data.go +149 -0
  67. package/jsvm/form_data_test.go +225 -0
  68. package/jsvm/internal/types/generated/embed.go +6 -0
  69. package/jsvm/internal/types/generated/types.d.ts +24820 -0
  70. package/jsvm/internal/types/types.go +1408 -0
  71. package/jsvm/jsvm.go +587 -0
  72. package/jsvm/mapper.go +67 -0
  73. package/jsvm/mapper_test.go +42 -0
  74. package/jsvm/pool.go +73 -0
  75. package/jsvm/program_source_test.go +24 -0
  76. package/loadtest/loadtest.go +202 -0
  77. package/loadtest/loadtest_test.go +84 -0
  78. package/localization/README.md +23 -0
  79. package/localization/catalogue.json +483 -0
  80. package/localization/localization.go +94 -0
  81. package/localization/localization_test.go +21 -0
  82. package/mail/register.go +80 -0
  83. package/mail/register_test.go +49 -0
  84. package/mail/resolve.go +91 -0
  85. package/migration/import.go +96 -0
  86. package/migration/import_test.go +58 -0
  87. package/otp/otp.go +56 -0
  88. package/otp/otp_test.go +56 -0
  89. package/package.json +51 -0
  90. package/scripts/check-edge.py +42 -0
  91. package/scripts/check.sh +11 -0
  92. package/scripts/sync-jsvm-types.sh +10 -0
  93. package/security/README.md +94 -0
  94. package/security/assurance_test.go +149 -0
  95. package/security/compatibility_test.go +128 -0
  96. package/security/config.go +78 -0
  97. package/security/dashboard_test.go +103 -0
  98. package/security/management.go +169 -0
  99. package/security/openapi.json +508 -0
  100. package/security/review.go +34 -0
  101. package/security/security.go +503 -0
  102. package/security/security_test.go +146 -0
  103. package/security/state.go +116 -0
  104. package/security/ui/dashboard.css +4 -0
  105. package/security/ui/dashboard.js +83 -0
  106. package/security/ui/main.js +15 -0
  107. package/security/ui/model.js +32 -0
  108. package/security/ui/model.test.mjs +25 -0
  109. package/security/ui/registration.test.mjs +10 -0
  110. package/settings/env_test.go +41 -0
  111. package/settings/openapi.json +193 -0
  112. package/settings/settings.go +155 -0
  113. package/settings/settings_test.go +31 -0
  114. package/watcher/watcher.go +192 -0
  115. package/watcher/watcher_test.go +200 -0
  116. package/web/static.go +99 -0
  117. package/web/static_test.go +48 -0
@@ -0,0 +1,133 @@
1
+ function settings(props) {
2
+ const uniqueId = "f_" + app.utils.randomString();
3
+
4
+ return app.components.fieldSettings(props, {
5
+ content: () =>
6
+ t.div(
7
+ { className: "grid sm" },
8
+ t.div(
9
+ { className: "col-sm-12" },
10
+ t.div(
11
+ { className: "field" },
12
+ t.label({ htmlFor: uniqueId + ".help" }, "Help text"),
13
+ t.input({
14
+ type: "text",
15
+ id: uniqueId + ".help",
16
+ name: () => `fields.${props.fieldIndex}.help`,
17
+ value: () => props.field.help || "",
18
+ oninput: (e) => (props.field.help = e.target.value),
19
+ }),
20
+ ),
21
+ ),
22
+ ),
23
+ footer: () => [
24
+ t.div(
25
+ { className: "field" },
26
+ t.input({
27
+ className: "sm",
28
+ type: "checkbox",
29
+ id: uniqueId + ".required",
30
+ name: () => `fields.${props.fieldIndex}.required`,
31
+ checked: () => !!props.field.required,
32
+ onchange: (e) => (props.field.required = e.target.checked),
33
+ }),
34
+ t.label(
35
+ { htmlFor: uniqueId + ".required" },
36
+ t.span({ className: "txt" }, "Required"),
37
+ t.i({
38
+ className: "ri-information-line link-hint",
39
+ ariaDescription: app.attrs.tooltip("Requires a nonempty value."),
40
+ }),
41
+ ),
42
+ ),
43
+ ],
44
+ });
45
+ }
46
+
47
+ window.app.fieldTypes.dateOnly = {
48
+ icon: "ri-calendar-line",
49
+ label: "Calendar date",
50
+ settings,
51
+ view: app.fieldTypes.text.view,
52
+ input: (props) => {
53
+ const id = "dateonly_" + app.utils.randomString();
54
+ return t.div(
55
+ { className: "field" },
56
+ t.label({ htmlFor: id }, () => props.field.name),
57
+ t.input({
58
+ id,
59
+ type: "date",
60
+ name: () => props.field.name,
61
+ required: () => props.field.required,
62
+ value: () => props.record[props.field.name] || "",
63
+ onchange: (e) => props.record[props.field.name] = e.target.value,
64
+ }),
65
+ );
66
+ },
67
+ dummyData: () => "2026-01-01",
68
+ };
69
+ window.app.fieldTypes.color = {
70
+ icon: "ri-palette-line",
71
+ label: "Color",
72
+ settings,
73
+ view: app.fieldTypes.text.view,
74
+ input: (props) => {
75
+ const id = "color_" + app.utils.randomString();
76
+ return t.div(
77
+ { className: "field" },
78
+ t.label({ htmlFor: id }, () => props.field.name),
79
+ t.input({
80
+ id,
81
+ type: "text",
82
+ name: () => props.field.name,
83
+ required: () => props.field.required,
84
+ pattern: "#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})",
85
+ value: () => props.record[props.field.name] || "",
86
+ oninput: (e) => props.record[props.field.name] = e.target.value,
87
+ }),
88
+ t.input({
89
+ type: "color",
90
+ ariaLabel: () => `Pick ${props.field.name}`,
91
+ value: () =>
92
+ /^#[0-9a-f]{6}$/i.test(props.record[props.field.name]) ? props.record[props.field.name] : "#000000",
93
+ oninput: (e) => props.record[props.field.name] = e.target.value,
94
+ }),
95
+ );
96
+ },
97
+ dummyData: () => "#1055c9",
98
+ };
99
+
100
+ const originalInput = app.fieldTypes.text.input;
101
+ app.fieldTypes.text.input = props => t.div(null, originalInput(props), () => {
102
+ if (!props.field.autogeneratePattern || props.field.primaryKey) return;
103
+ const local = store({busy:false});
104
+ return t.button({type:"button",className:"btn sm outline",disabled:()=>local.busy,onclick:async()=>{
105
+ local.busy=true;
106
+ try { const result=await app.pb.send('/api/adapter/collections/'+encodeURIComponent(props.collection.id)+'/fields/'+encodeURIComponent(props.field.name)+'/generate',{method:'POST'});props.record[props.field.name]=result.value; }
107
+ catch(error){app.checkApiError(error);}finally{local.busy=false;}
108
+ }},'Generate value');
109
+ });
110
+
111
+ const originalJSONSettings = app.fieldTypes.json.settings;
112
+ app.fieldTypes.json.settings = props => t.div(null, originalJSONSettings(props), t.div({className:'field'},
113
+ t.label('JSON Schema (inline Draft 2020-12)'),t.textarea({rows:8,name:()=>`fields.${props.fieldIndex}.schema`,value:()=>props.field.schema||'',oninput:e=>props.field.schema=e.target.value}),t.small('External schema references are disabled. Maximum 64 KiB.')));
114
+
115
+ app.routes.superuserOnly('#/settings/extensions', async()=>{
116
+ const {extensionPage}=await import(app.pb.buildURL('/_/extensions/adapter-features/settings.js'));
117
+ return extensionPage(app);
118
+ });
119
+ app.store.settingsNavGroups.System.push({href:'#/settings/extensions',label:'x47base extensions',icon:'ri-puzzle-line'});
120
+
121
+ document.addEventListener('mount:appHeader',event=>queueMicrotask(()=>{
122
+ if(event.detail.querySelector('.spink-app-name'))return;
123
+ const name=t.span({className:'spink-app-name txt-ellipsis',style:'max-width:180px;margin-right:12px'},()=>app.store.settings?.meta?.appName||'');
124
+ event.detail.querySelector('.logo')?.after(name);
125
+ }));
126
+ document.addEventListener('mount:pageHeaderSecondaryBtns',event=>{
127
+ const page=event.detail.closest('.page');if(!page)return;
128
+ const key='spink-sidebar-collapsed';let collapsed=localStorage.getItem(key)==='true';
129
+ const apply=()=>{page.classList.toggle('spink-sidebar-collapsed',collapsed);button.setAttribute('aria-expanded',String(!collapsed));};
130
+ const button=t.button({type:'button',className:'btn sm secondary',onclick:()=>{collapsed=!collapsed;localStorage.setItem(key,String(collapsed));apply();}},'Toggle sidebar');
131
+ event.detail.prepend(button);apply();
132
+ });
133
+ const style=document.createElement('style');style.textContent='@media(min-width:900px){.spink-sidebar-collapsed .collections-sidebar{display:none}.spink-sidebar-collapsed .page-content{width:100%;margin-left:0}}@media(max-width:700px){.spink-app-name{display:none}}';document.head.append(style);
@@ -0,0 +1,31 @@
1
+ export function extensionPage(app){
2
+ let cleanup=()=>{};
3
+ return t.div({className:'page pb-security',onmount:root=>{cleanup=mount(root,app)},onunmount:()=>cleanup()});
4
+ }
5
+ function mount(root,app){
6
+ app.store.title='x47base extensions';let closed=false,busy=false,config=null;const token=app.pb.authStore.token;const abort=new AbortController();
7
+ const el=(tag,text)=>{const n=document.createElement(tag);if(text!=null)n.textContent=text;return n};
8
+ const notice=el('p','Loading…');notice.setAttribute('role','status');const form=el('form');const content=el('div');root.append(el('h1','x47base extensions'),el('p','Add-on settings stored with this database.'),notice,content);
9
+ const field=(name,input)=>{const label=el('label');label.className='sec-field';label.append(el('span',name),input);return label};
10
+ const input=(type,value='')=>{const n=el('input');n.type=type;n.value=value;return n};
11
+ const button=(label,action)=>{const b=el('button',label);b.type='button';b.className='btn secondary';b.onclick=action;return b};
12
+ const request=async(path,options={})=>{if(closed||app.pb.authStore.token!==token)throw Error('Session ended');const data=await app.pb.send(path,{...options,signal:abort.signal});if(closed||app.pb.authStore.token!==token)throw Error('Session ended');return data};
13
+ const run=async(action)=>{if(busy||closed)return;busy=true;content.inert=true;notice.textContent='Working…';try{await action()}catch(e){if(!closed)notice.textContent=e.status===409?'Settings changed elsewhere. Your draft remains; reload before saving.':e.message}finally{busy=false;content.inert=false}};
14
+ const unsubscribe=app.pb.authStore.onChange(()=>{if(app.pb.authStore.token!==token){closed=true;abort.abort();root.replaceChildren();window.location.hash='#/login'}});
15
+ function render(keyConfigured){content.replaceChildren();form.replaceChildren();const backup=el('section');backup.className='sec-panel';backup.append(el('h2','Encrypted backups'),el('p','Applies to manual and scheduled backups, with local or configured S3 storage. Keep the key available for restore.'));
16
+ const enabled=input('checkbox');enabled.checked=config.backups.encrypted;enabled.onchange=()=>config.backups.encrypted=enabled.checked;
17
+ const env=input('text',config.backups.encryptionEnv||'PB_BACKUP_ENCRYPTION_KEY');env.pattern='[A-Za-z_][A-Za-z0-9_]{0,127}';env.oninput=()=>config.backups.encryptionEnv=env.value;
18
+ backup.append(field('Encrypt new backups',enabled),field('Server environment variable containing the key',env),el('p',keyConfigured?'A valid key is available for the saved configuration.':'No valid key is configured. Set a 32–1024 byte secret in the server environment and restart before enabling encryption.'));
19
+ const native=el('a','Manage backups, schedules and S3');native.href='#/settings/backups';backup.append(native);
20
+ const upload=input('file');upload.accept='.age';backup.append(field('Upload an encrypted .zip.age backup',upload),button('Upload encrypted backup',()=>run(async()=>{if(!upload.files[0])throw Error('Select an archive first');const data=new FormData();data.append('file',upload.files[0]);await request('/api/spink/backups/upload',{method:'POST',body:data});notice.textContent='Encrypted backup uploaded. Manage it from Backups.';upload.value=''})));
21
+ form.append(backup);
22
+ const mail=el('section');mail.className='sec-panel';mail.append(el('h2','Localized email templates'),el('p','Use a collection ID or name and a lowercase language tag, such as fr or de-ch. Requests select templates with ?lang=fr.'));
23
+ const collection=input('text'),locale=input('text');const kind=el('select');for(const [key,label] of [['otp','OTP'],['verification','Verification'],['passwordReset','Password reset'],['emailChange','Email change'],['authAlert','Login alert']]){const o=el('option',label);o.value=key;kind.append(o)}
24
+ const subject=input('text');const body=el('textarea');body.rows=8;mail.append(field('Collection',collection),field('Email type',kind),field('Language',locale),field('Subject',subject),field('HTML body',body));
25
+ const variants=el('div');function list(){variants.replaceChildren();for(const [c,kinds]of Object.entries(config.emailLocales||{}))for(const [k,locales]of Object.entries(kinds))for(const [l,v]of Object.entries(locales)){const row=el('p');row.append(el('span',`${c} · ${k} · ${l} `),button('Edit',()=>{collection.value=c;kind.value=k;locale.value=l;subject.value=v.subject;body.value=v.body}),button('Remove',()=>{delete config.emailLocales[c][k][l];list()}));variants.append(row)}}
26
+ mail.append(button('Add / update draft template',()=>{const c=collection.value.trim(),l=locale.value.trim();if(!c||!l||!subject.value.trim()||!body.value.trim()){notice.textContent='Fill in collection, language, subject and body.';return}config.emailLocales??={};config.emailLocales[c]??={};config.emailLocales[c][kind.value]??={};config.emailLocales[c][kind.value][l]={subject:subject.value,body:body.value};list();notice.textContent='Template added to draft. Save settings to apply.'}),variants);list();form.append(mail);
27
+ const save=el('button','Save extension settings');save.className='btn';save.type='submit';form.append(save,button('Discard draft and reload',()=>run(load)));form.onsubmit=e=>{e.preventDefault();run(async()=>{config=await request('/api/spink/settings',{method:'PUT',body:config});await load();notice.textContent='Extension settings saved.'})};content.append(form);
28
+ }
29
+ async function load(){const data=await request('/api/spink/settings');config=data.config;render(data.backupKeyConfigured);notice.textContent=''}
30
+ run(load);return()=>{closed=true;abort.abort();unsubscribe();root.replaceChildren()};
31
+ }
package/go.mod ADDED
@@ -0,0 +1,54 @@
1
+ module github.com/spink-dev/pocketbase-extension
2
+
3
+ go 1.27
4
+
5
+ require (
6
+ filippo.io/age v1.3.2
7
+ github.com/dop251/goja v0.0.0-20260901132549-43234fa61381
8
+ github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14
9
+ github.com/fatih/color v1.19.0
10
+ github.com/fsnotify/fsnotify v1.10.1
11
+ github.com/ganigeorgiev/fexpr v0.6.0
12
+ github.com/golang-jwt/jwt/v5 v5.3.1
13
+ github.com/pocketbase/dbx v1.12.0
14
+ github.com/pocketbase/ozzo-validation/v4 v4.3.0
15
+ github.com/pocketbase/pocketbase v0.40.3
16
+ github.com/pocketbase/tygoja v0.1.0
17
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
18
+ github.com/spf13/cast v1.10.0
19
+ github.com/spf13/cobra v1.10.2
20
+ )
21
+
22
+ require (
23
+ filippo.io/hpke v0.4.0 // indirect
24
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
25
+ github.com/disintegration/imaging v1.6.2 // indirect
26
+ github.com/dlclark/regexp2/v2 v2.7.1 // indirect
27
+ github.com/domodwyer/mailyak/v3 v3.6.2 // indirect
28
+ github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 // indirect
29
+ github.com/dustin/go-humanize v1.0.1 // indirect
30
+ github.com/gabriel-vasile/mimetype v1.4.15 // indirect
31
+ github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
32
+ github.com/google/pprof v0.0.0-20260902005441-ca85771921e4 // indirect
33
+ github.com/google/uuid v1.6.0 // indirect
34
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
35
+ github.com/mattn/go-colorable v0.1.15 // indirect
36
+ github.com/mattn/go-isatty v0.0.24 // indirect
37
+ github.com/ncruces/go-strftime v1.0.0 // indirect
38
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
39
+ github.com/spf13/pflag v1.0.10 // indirect
40
+ golang.org/x/crypto v0.56.0 // indirect
41
+ golang.org/x/image v0.45.0 // indirect
42
+ golang.org/x/mod v0.40.0 // indirect
43
+ golang.org/x/net v0.58.0 // indirect
44
+ golang.org/x/oauth2 v0.36.0 // indirect
45
+ golang.org/x/sync v0.22.0 // indirect
46
+ golang.org/x/sys v0.47.0 // indirect
47
+ golang.org/x/text v0.41.0 // indirect
48
+ golang.org/x/tools v0.49.0 // indirect
49
+ gopkg.in/yaml.v3 v3.0.1 // indirect
50
+ modernc.org/libc v1.74.4 // indirect
51
+ modernc.org/mathutil v1.7.1 // indirect
52
+ modernc.org/memory v1.12.0 // indirect
53
+ modernc.org/sqlite v1.57.0 // indirect
54
+ )
package/go.sum ADDED
@@ -0,0 +1,159 @@
1
+ c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs=
2
+ c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
3
+ filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c=
4
+ filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk=
5
+ filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
6
+ filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
7
+ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
8
+ github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
9
+ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
10
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
11
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
12
+ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
13
+ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
14
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15
+ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
16
+ github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
17
+ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
18
+ github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
19
+ github.com/dlclark/regexp2/v2 v2.7.1 h1:yqDtwI1ptXXvEUNpYTk2lad4jLtAcKqkzepn4savSk4=
20
+ github.com/dlclark/regexp2/v2 v2.7.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
21
+ github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8=
22
+ github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c=
23
+ github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY=
24
+ github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM=
25
+ github.com/dop251/goja v0.0.0-20260901132549-43234fa61381 h1:vzmJ+R9qxuaksCAB+Kn6wtGgQ01cIfKzUGwWWcX5zXI=
26
+ github.com/dop251/goja v0.0.0-20260901132549-43234fa61381/go.mod h1:u8yZRUavu+N4EnFFy6J5fVtjE7lEcZ2YyV2GcBXY9c8=
27
+ github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14 h1:3U8dTgyNBhEQ/GVw0jZW5q+93Zw2gAZPRWhJ9TwV3rM=
28
+ github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0=
29
+ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
30
+ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
31
+ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
32
+ github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
33
+ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
34
+ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
35
+ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
36
+ github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
37
+ github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
38
+ github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
39
+ github.com/ganigeorgiev/fexpr v0.6.0 h1:Fza3O/QMBKEudUvxV862qe6GjxM60GJjjKytdp+VQus=
40
+ github.com/ganigeorgiev/fexpr v0.6.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
41
+ github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
42
+ github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
43
+ github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
44
+ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
45
+ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
46
+ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
47
+ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
48
+ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
49
+ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
50
+ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
51
+ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
52
+ github.com/google/pprof v0.0.0-20260902005441-ca85771921e4 h1:/6mPXfWmhv8eKck12I0YNIcIjwHtxP3YRIMKiEgTjWg=
53
+ github.com/google/pprof v0.0.0-20260902005441-ca85771921e4/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
54
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
55
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
56
+ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
57
+ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
58
+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
59
+ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
60
+ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
61
+ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
62
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
63
+ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
64
+ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
65
+ github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
66
+ github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
67
+ github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
68
+ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
69
+ github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
70
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
71
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
72
+ github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
73
+ github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
74
+ github.com/pocketbase/ozzo-validation/v4 v4.3.0 h1:uKBDVma7bZqgR2a6AwE+k9hkuDFfiZMpBHQdZ1z3iQs=
75
+ github.com/pocketbase/ozzo-validation/v4 v4.3.0/go.mod h1:6XNjSTw/Jb2F8LOkKO3oyzIWExbrGiYoS4uVxVwz90g=
76
+ github.com/pocketbase/pocketbase v0.40.3 h1:iLq2DPQjcBRDJ2sjM5ssc7eLV7LmPtCKx4Xs8Rkgud8=
77
+ github.com/pocketbase/pocketbase v0.40.3/go.mod h1:T5rHKBqPqzEOesla8gqBnwqnceXfeHVuGt/MekG00TU=
78
+ github.com/pocketbase/tygoja v0.1.0 h1:NJtUNHr5OlWdCyblKr8zPA9awmTv41t7LBro4EGcZ/Y=
79
+ github.com/pocketbase/tygoja v0.1.0/go.mod h1:hKJWPGFqavk3cdTa47Qvs8g37lnfI57OYdVVbIqW5aE=
80
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
81
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
82
+ github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
83
+ github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
84
+ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
85
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
86
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
87
+ github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
88
+ github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
89
+ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
90
+ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
91
+ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
92
+ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
93
+ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
94
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
95
+ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
96
+ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
97
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
98
+ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
99
+ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
100
+ golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
101
+ golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
102
+ golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
103
+ golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
104
+ golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
105
+ golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
106
+ golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
107
+ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
108
+ golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
109
+ golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
110
+ golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
111
+ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
112
+ golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
113
+ golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
114
+ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
115
+ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
116
+ golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
117
+ golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
118
+ golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
119
+ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
120
+ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
121
+ golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
122
+ golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
123
+ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
124
+ golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
125
+ golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
126
+ google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
127
+ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
128
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
129
+ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
130
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
131
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
132
+ modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
133
+ modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
134
+ modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
135
+ modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
136
+ modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
137
+ modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
138
+ modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
139
+ modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
140
+ modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
141
+ modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
142
+ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
143
+ modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
144
+ modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
145
+ modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
146
+ modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
147
+ modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
148
+ modernc.org/memory v1.12.0 h1:twkmYNkGXCvtYWzoux02jtK6eovjZbdI0uHFUYp6kuU=
149
+ modernc.org/memory v1.12.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
150
+ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
151
+ modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
152
+ modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
153
+ modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
154
+ modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
155
+ modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
156
+ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
157
+ modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
158
+ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
159
+ modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
@@ -0,0 +1,91 @@
1
+ package archive
2
+
3
+ import (
4
+ "archive/zip"
5
+ "compress/flate"
6
+ "errors"
7
+ "io"
8
+ "io/fs"
9
+ "os"
10
+ "path/filepath"
11
+ "strings"
12
+ )
13
+
14
+ // Create creates a new zip archive from src dir content and saves it in dest path.
15
+ //
16
+ // You can specify skipPaths to skip/ignore certain directories and files (relative to src)
17
+ // preventing adding them in the final archive.
18
+ func Create(src string, dest string, skipPaths ...string) error {
19
+ if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
20
+ return err
21
+ }
22
+
23
+ zf, err := os.Create(dest)
24
+ if err != nil {
25
+ return err
26
+ }
27
+
28
+ zw := zip.NewWriter(zf)
29
+
30
+ // register a custom Deflate compressor
31
+ zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
32
+ return flate.NewWriter(out, flate.BestSpeed)
33
+ })
34
+
35
+ err = zipAddFS(zw, os.DirFS(src), skipPaths...)
36
+ if err != nil {
37
+ // try to cleanup at least the created zip file
38
+ return errors.Join(err, zw.Close(), zf.Close(), os.Remove(dest))
39
+ }
40
+
41
+ return errors.Join(zw.Close(), zf.Close())
42
+ }
43
+
44
+ // note remove after similar method is added in the std lib (https://github.com/golang/go/issues/54898)
45
+ func zipAddFS(w *zip.Writer, fsys fs.FS, skipPaths ...string) error {
46
+ return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error {
47
+ if err != nil {
48
+ return err
49
+ }
50
+
51
+ if d.IsDir() {
52
+ return nil
53
+ }
54
+
55
+ // skip
56
+ for _, ignore := range skipPaths {
57
+ if ignore == name ||
58
+ strings.HasPrefix(filepath.Clean(name)+string(os.PathSeparator), filepath.Clean(ignore)+string(os.PathSeparator)) {
59
+ return nil
60
+ }
61
+ }
62
+
63
+ info, err := d.Info()
64
+ if err != nil {
65
+ return err
66
+ }
67
+
68
+ h, err := zip.FileInfoHeader(info)
69
+ if err != nil {
70
+ return err
71
+ }
72
+
73
+ h.Name = name
74
+ h.Method = zip.Deflate
75
+
76
+ fw, err := w.CreateHeader(h)
77
+ if err != nil {
78
+ return err
79
+ }
80
+
81
+ f, err := fsys.Open(name)
82
+ if err != nil {
83
+ return err
84
+ }
85
+ defer f.Close()
86
+
87
+ _, err = io.Copy(fw, f)
88
+
89
+ return err
90
+ })
91
+ }
@@ -0,0 +1,125 @@
1
+ package archive_test
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+
8
+ "github.com/spink-dev/pocketbase-extension/internal/archive"
9
+ )
10
+
11
+ func TestCreateFailure(t *testing.T) {
12
+ testDir := createTestDir(t)
13
+ defer os.RemoveAll(testDir)
14
+
15
+ zipPath := filepath.Join(os.TempDir(), "pb_test.zip")
16
+ defer os.RemoveAll(zipPath)
17
+
18
+ missingDir := filepath.Join(os.TempDir(), "missing")
19
+
20
+ if err := archive.Create(missingDir, zipPath); err == nil {
21
+ t.Fatal("Expected to fail due to missing directory or file")
22
+ }
23
+
24
+ if _, err := os.Stat(zipPath); err == nil {
25
+ t.Fatalf("Expected the zip file not to be created")
26
+ }
27
+ }
28
+
29
+ func TestCreateSuccess(t *testing.T) {
30
+ testDir := createTestDir(t)
31
+ defer os.RemoveAll(testDir)
32
+
33
+ zipName := "pb_test.zip"
34
+ zipPath := filepath.Join(os.TempDir(), zipName)
35
+ defer os.RemoveAll(zipPath)
36
+
37
+ // zip testDir content (excluding test and a/b/c dir)
38
+ if err := archive.Create(testDir, zipPath, "a/b/c", "test"); err != nil {
39
+ t.Fatalf("Failed to create archive: %v", err)
40
+ }
41
+
42
+ info, err := os.Stat(zipPath)
43
+ if err != nil {
44
+ t.Fatalf("Failed to retrieve the generated zip file: %v", err)
45
+ }
46
+
47
+ if name := info.Name(); name != zipName {
48
+ t.Fatalf("Expected zip with name %q, got %q", zipName, name)
49
+ }
50
+
51
+ expectedSize := int64(532)
52
+ if size := info.Size(); size != expectedSize {
53
+ t.Fatalf("Expected zip with size %d, got %d", expectedSize, size)
54
+ }
55
+ }
56
+
57
+ // -------------------------------------------------------------------
58
+
59
+ // note: make sure to call os.RemoveAll(dir) after you are done
60
+ // working with the created test dir.
61
+ func createTestDir(t *testing.T) string {
62
+ dir, err := os.MkdirTemp(os.TempDir(), "pb_zip_test")
63
+ if err != nil {
64
+ t.Fatal(err)
65
+ }
66
+
67
+ if err := os.MkdirAll(filepath.Join(dir, "a/b/c"), os.ModePerm); err != nil {
68
+ t.Fatal(err)
69
+ }
70
+
71
+ {
72
+ f, err := os.OpenFile(filepath.Join(dir, "test"), os.O_WRONLY|os.O_CREATE, 0644)
73
+ if err != nil {
74
+ t.Fatal(err)
75
+ }
76
+ f.Close()
77
+ }
78
+
79
+ {
80
+ f, err := os.OpenFile(filepath.Join(dir, "test2"), os.O_WRONLY|os.O_CREATE, 0644)
81
+ if err != nil {
82
+ t.Fatal(err)
83
+ }
84
+ f.Close()
85
+ }
86
+
87
+ {
88
+ f, err := os.OpenFile(filepath.Join(dir, "a/test"), os.O_WRONLY|os.O_CREATE, 0644)
89
+ if err != nil {
90
+ t.Fatal(err)
91
+ }
92
+ f.Close()
93
+ }
94
+
95
+ {
96
+ f, err := os.OpenFile(filepath.Join(dir, "a/b/sub1"), os.O_WRONLY|os.O_CREATE, 0644)
97
+ if err != nil {
98
+ t.Fatal(err)
99
+ }
100
+ f.Close()
101
+ }
102
+
103
+ {
104
+ f, err := os.OpenFile(filepath.Join(dir, "a/b/c/sub2"), os.O_WRONLY|os.O_CREATE, 0644)
105
+ if err != nil {
106
+ t.Fatal(err)
107
+ }
108
+ f.Close()
109
+ }
110
+
111
+ {
112
+ f, err := os.OpenFile(filepath.Join(dir, "a/b/c/sub3"), os.O_WRONLY|os.O_CREATE, 0644)
113
+ if err != nil {
114
+ t.Fatal(err)
115
+ }
116
+ f.Close()
117
+ }
118
+
119
+ // symbolic link
120
+ if err := os.Symlink(filepath.Join(dir, "test"), filepath.Join(dir, "test_symlink")); err != nil {
121
+ t.Fatal(err)
122
+ }
123
+
124
+ return dir
125
+ }