Compare commits

..

4 Commits

Author SHA1 Message Date
shamoon
9a29195792 Mas testing 2025-12-31 17:06:24 -08:00
shamoon
bdd00498a1 skip_asn_if_exists 2025-12-31 12:09:56 -08:00
shamoon
92deebddd4 "Handoff" ASN when merging or editing PDFs 2025-12-31 11:50:27 -08:00
shamoon
c7efcee3d6 First, release ASNs before document replacement (and restore if needed) 2025-12-31 10:42:07 -08:00
8 changed files with 515 additions and 192 deletions

View File

@@ -63,7 +63,7 @@
"jest": "30.2.0",
"jest-environment-jsdom": "^30.2.0",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^16.0.0",
"jest-preset-angular": "^15.0.3",
"jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.1",

338
src-ui/pnpm-lock.yaml generated
View File

@@ -160,8 +160,8 @@ importers:
specifier: ^16.0.0
version: 16.0.0
jest-preset-angular:
specifier: ^16.0.0
version: 16.0.0(e164a15bea80c603eb581d5724a352ec)
specifier: ^15.0.3
version: 15.0.3(e164a15bea80c603eb581d5724a352ec)
jest-websocket-mock:
specifier: ^2.5.0
version: 2.5.0
@@ -1282,8 +1282,8 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.27.2':
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
'@esbuild/aix-ppc64@0.27.0':
resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
@@ -1300,8 +1300,8 @@ packages:
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.27.2':
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
'@esbuild/android-arm64@0.27.0':
resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
@@ -1318,8 +1318,8 @@ packages:
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.27.2':
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
'@esbuild/android-arm@0.27.0':
resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
@@ -1336,8 +1336,8 @@ packages:
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.27.2':
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
'@esbuild/android-x64@0.27.0':
resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
@@ -1354,8 +1354,8 @@ packages:
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.27.2':
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
'@esbuild/darwin-arm64@0.27.0':
resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
@@ -1372,8 +1372,8 @@ packages:
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.27.2':
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
'@esbuild/darwin-x64@0.27.0':
resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
@@ -1390,8 +1390,8 @@ packages:
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.27.2':
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
'@esbuild/freebsd-arm64@0.27.0':
resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
@@ -1408,8 +1408,8 @@ packages:
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.2':
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
'@esbuild/freebsd-x64@0.27.0':
resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
@@ -1426,8 +1426,8 @@ packages:
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.27.2':
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
'@esbuild/linux-arm64@0.27.0':
resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
@@ -1444,8 +1444,8 @@ packages:
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.27.2':
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
'@esbuild/linux-arm@0.27.0':
resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
@@ -1462,8 +1462,8 @@ packages:
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.27.2':
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
'@esbuild/linux-ia32@0.27.0':
resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
@@ -1480,8 +1480,8 @@ packages:
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.27.2':
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
'@esbuild/linux-loong64@0.27.0':
resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
@@ -1498,8 +1498,8 @@ packages:
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.27.2':
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
'@esbuild/linux-mips64el@0.27.0':
resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
@@ -1516,8 +1516,8 @@ packages:
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.27.2':
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
'@esbuild/linux-ppc64@0.27.0':
resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
@@ -1534,8 +1534,8 @@ packages:
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.27.2':
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
'@esbuild/linux-riscv64@0.27.0':
resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
@@ -1552,8 +1552,8 @@ packages:
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.27.2':
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
'@esbuild/linux-s390x@0.27.0':
resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
@@ -1570,8 +1570,8 @@ packages:
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.27.2':
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
'@esbuild/linux-x64@0.27.0':
resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
@@ -1588,8 +1588,8 @@ packages:
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.27.2':
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
'@esbuild/netbsd-arm64@0.27.0':
resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
@@ -1606,8 +1606,8 @@ packages:
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.2':
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
'@esbuild/netbsd-x64@0.27.0':
resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
@@ -1624,8 +1624,8 @@ packages:
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.27.2':
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
'@esbuild/openbsd-arm64@0.27.0':
resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
@@ -1642,8 +1642,8 @@ packages:
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.2':
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
'@esbuild/openbsd-x64@0.27.0':
resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
@@ -1654,8 +1654,8 @@ packages:
cpu: [arm64]
os: [openharmony]
'@esbuild/openharmony-arm64@0.27.2':
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
'@esbuild/openharmony-arm64@0.27.0':
resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
@@ -1672,8 +1672,8 @@ packages:
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.27.2':
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
'@esbuild/sunos-x64@0.27.0':
resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
@@ -1690,8 +1690,8 @@ packages:
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.27.2':
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
'@esbuild/win32-arm64@0.27.0':
resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
@@ -1708,8 +1708,8 @@ packages:
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.27.2':
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
'@esbuild/win32-ia32@0.27.0':
resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
@@ -1726,8 +1726,8 @@ packages:
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.27.2':
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
'@esbuild/win32-x64@0.27.0':
resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -2833,8 +2833,8 @@ packages:
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@sinclair/typebox@0.34.46':
resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==}
'@sinclair/typebox@0.34.41':
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
'@sinonjs/commons@3.0.1':
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
@@ -3991,13 +3991,18 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
esbuild-wasm@0.25.10:
resolution: {integrity: sha512-IyyfrTA2iiOh/uhlaJj0aUDgW42lFhr29ZeKouVNOz/8mLyuqWbEuVst+B4RBH18pb3AcOHnaOgyskAbsVOe3A==}
engines: {node: '>=18'}
hasBin: true
esbuild-wasm@0.25.5:
resolution: {integrity: sha512-V/rbdOws2gDcnCAECfPrajhuafI0WY4WumUgc8ZHwOLnvmM0doLQ+dqvVFI2qkVxQsvo6880aC9IjpyDqcwwTw==}
engines: {node: '>=18'}
hasBin: true
esbuild-wasm@0.27.2:
resolution: {integrity: sha512-eUTnl8eh+v8UZIZh4MrMOKDAc8Lm7+NqP3pyuTORGFY1s/o9WoiJgKnwXy+te2J3hX7iRbFSHEyig7GsPeeJyw==}
esbuild-wasm@0.27.0:
resolution: {integrity: sha512-4XpLDOY4sOzqgiezPXDkHTn25cBA1MKN5OTotehoFR7/wTpSYCK76OA8rQfpiuz12G27JpGxxdh+/D9GcNl61w==}
engines: {node: '>=18'}
hasBin: true
@@ -4011,8 +4016,8 @@ packages:
engines: {node: '>=18'}
hasBin: true
esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
esbuild@0.27.0:
resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==}
engines: {node: '>=18'}
hasBin: true
@@ -4817,14 +4822,14 @@ packages:
jsdom:
optional: true
jest-preset-angular@16.0.0:
resolution: {integrity: sha512-FVo98EZiJ9cwHeteJozCCIkgJecytt1tu0t8DrAMTyyQ4x/seeZmctkWXP0J9uGyARS0Kcwd+f2YeKqKQOB2yA==}
jest-preset-angular@15.0.3:
resolution: {integrity: sha512-W1OpeQ/tq72ZSGXRvUDktL02QBAWIM5SZPK4KI0zoXNvRGiGhNMbxuaFF7anLi27cojmWHm+TewJQI1Dsm9s6A==}
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0}
peerDependencies:
'@angular/compiler-cli': '>=19.0.0 <22.0.0'
'@angular/core': '>=19.0.0 <22.0.0'
'@angular/platform-browser': '>=19.0.0 <22.0.0'
'@angular/platform-browser-dynamic': '>=19.0.0 <22.0.0'
'@angular/compiler-cli': '>=18.0.0 <21.0.0'
'@angular/core': '>=18.0.0 <21.0.0'
'@angular/platform-browser': '>=18.0.0 <21.0.0'
'@angular/platform-browser-dynamic': '>=18.0.0 <21.0.0'
jest: ^30.0.0
jsdom: '>=26.0.0'
typescript: '>=5.5'
@@ -6386,8 +6391,35 @@ packages:
peerDependencies:
typescript: '>=4.8.4'
ts-jest@29.4.6:
resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==}
ts-jest@29.4.4:
resolution: {integrity: sha512-ccVcRABct5ZELCT5U0+DZwkXMCcOCLi2doHRrKy1nK/s7J7bch6TzJMsrY09WxgUUIP/ITfmcDS8D2yl63rnXw==}
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
'@babel/core': '>=7.0.0-beta.0 <8'
'@jest/transform': ^29.0.0 || ^30.0.0
'@jest/types': ^29.0.0 || ^30.0.0
babel-jest: ^29.0.0 || ^30.0.0
esbuild: '*'
jest: ^29.0.0 || ^30.0.0
jest-util: ^29.0.0 || ^30.0.0
typescript: '>=4.3 <6'
peerDependenciesMeta:
'@babel/core':
optional: true
'@jest/transform':
optional: true
'@jest/types':
optional: true
babel-jest:
optional: true
esbuild:
optional: true
jest-util:
optional: true
ts-jest@29.4.5:
resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==}
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@@ -6762,12 +6794,10 @@ packages:
whatwg-encoding@2.0.0:
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
engines: {node: '>=12'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@3.0.0:
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
@@ -8458,7 +8488,7 @@ snapshots:
'@esbuild/aix-ppc64@0.25.9':
optional: true
'@esbuild/aix-ppc64@0.27.2':
'@esbuild/aix-ppc64@0.27.0':
optional: true
'@esbuild/android-arm64@0.25.5':
@@ -8467,7 +8497,7 @@ snapshots:
'@esbuild/android-arm64@0.25.9':
optional: true
'@esbuild/android-arm64@0.27.2':
'@esbuild/android-arm64@0.27.0':
optional: true
'@esbuild/android-arm@0.25.5':
@@ -8476,7 +8506,7 @@ snapshots:
'@esbuild/android-arm@0.25.9':
optional: true
'@esbuild/android-arm@0.27.2':
'@esbuild/android-arm@0.27.0':
optional: true
'@esbuild/android-x64@0.25.5':
@@ -8485,7 +8515,7 @@ snapshots:
'@esbuild/android-x64@0.25.9':
optional: true
'@esbuild/android-x64@0.27.2':
'@esbuild/android-x64@0.27.0':
optional: true
'@esbuild/darwin-arm64@0.25.5':
@@ -8494,7 +8524,7 @@ snapshots:
'@esbuild/darwin-arm64@0.25.9':
optional: true
'@esbuild/darwin-arm64@0.27.2':
'@esbuild/darwin-arm64@0.27.0':
optional: true
'@esbuild/darwin-x64@0.25.5':
@@ -8503,7 +8533,7 @@ snapshots:
'@esbuild/darwin-x64@0.25.9':
optional: true
'@esbuild/darwin-x64@0.27.2':
'@esbuild/darwin-x64@0.27.0':
optional: true
'@esbuild/freebsd-arm64@0.25.5':
@@ -8512,7 +8542,7 @@ snapshots:
'@esbuild/freebsd-arm64@0.25.9':
optional: true
'@esbuild/freebsd-arm64@0.27.2':
'@esbuild/freebsd-arm64@0.27.0':
optional: true
'@esbuild/freebsd-x64@0.25.5':
@@ -8521,7 +8551,7 @@ snapshots:
'@esbuild/freebsd-x64@0.25.9':
optional: true
'@esbuild/freebsd-x64@0.27.2':
'@esbuild/freebsd-x64@0.27.0':
optional: true
'@esbuild/linux-arm64@0.25.5':
@@ -8530,7 +8560,7 @@ snapshots:
'@esbuild/linux-arm64@0.25.9':
optional: true
'@esbuild/linux-arm64@0.27.2':
'@esbuild/linux-arm64@0.27.0':
optional: true
'@esbuild/linux-arm@0.25.5':
@@ -8539,7 +8569,7 @@ snapshots:
'@esbuild/linux-arm@0.25.9':
optional: true
'@esbuild/linux-arm@0.27.2':
'@esbuild/linux-arm@0.27.0':
optional: true
'@esbuild/linux-ia32@0.25.5':
@@ -8548,7 +8578,7 @@ snapshots:
'@esbuild/linux-ia32@0.25.9':
optional: true
'@esbuild/linux-ia32@0.27.2':
'@esbuild/linux-ia32@0.27.0':
optional: true
'@esbuild/linux-loong64@0.25.5':
@@ -8557,7 +8587,7 @@ snapshots:
'@esbuild/linux-loong64@0.25.9':
optional: true
'@esbuild/linux-loong64@0.27.2':
'@esbuild/linux-loong64@0.27.0':
optional: true
'@esbuild/linux-mips64el@0.25.5':
@@ -8566,7 +8596,7 @@ snapshots:
'@esbuild/linux-mips64el@0.25.9':
optional: true
'@esbuild/linux-mips64el@0.27.2':
'@esbuild/linux-mips64el@0.27.0':
optional: true
'@esbuild/linux-ppc64@0.25.5':
@@ -8575,7 +8605,7 @@ snapshots:
'@esbuild/linux-ppc64@0.25.9':
optional: true
'@esbuild/linux-ppc64@0.27.2':
'@esbuild/linux-ppc64@0.27.0':
optional: true
'@esbuild/linux-riscv64@0.25.5':
@@ -8584,7 +8614,7 @@ snapshots:
'@esbuild/linux-riscv64@0.25.9':
optional: true
'@esbuild/linux-riscv64@0.27.2':
'@esbuild/linux-riscv64@0.27.0':
optional: true
'@esbuild/linux-s390x@0.25.5':
@@ -8593,7 +8623,7 @@ snapshots:
'@esbuild/linux-s390x@0.25.9':
optional: true
'@esbuild/linux-s390x@0.27.2':
'@esbuild/linux-s390x@0.27.0':
optional: true
'@esbuild/linux-x64@0.25.5':
@@ -8602,7 +8632,7 @@ snapshots:
'@esbuild/linux-x64@0.25.9':
optional: true
'@esbuild/linux-x64@0.27.2':
'@esbuild/linux-x64@0.27.0':
optional: true
'@esbuild/netbsd-arm64@0.25.5':
@@ -8611,7 +8641,7 @@ snapshots:
'@esbuild/netbsd-arm64@0.25.9':
optional: true
'@esbuild/netbsd-arm64@0.27.2':
'@esbuild/netbsd-arm64@0.27.0':
optional: true
'@esbuild/netbsd-x64@0.25.5':
@@ -8620,7 +8650,7 @@ snapshots:
'@esbuild/netbsd-x64@0.25.9':
optional: true
'@esbuild/netbsd-x64@0.27.2':
'@esbuild/netbsd-x64@0.27.0':
optional: true
'@esbuild/openbsd-arm64@0.25.5':
@@ -8629,7 +8659,7 @@ snapshots:
'@esbuild/openbsd-arm64@0.25.9':
optional: true
'@esbuild/openbsd-arm64@0.27.2':
'@esbuild/openbsd-arm64@0.27.0':
optional: true
'@esbuild/openbsd-x64@0.25.5':
@@ -8638,13 +8668,13 @@ snapshots:
'@esbuild/openbsd-x64@0.25.9':
optional: true
'@esbuild/openbsd-x64@0.27.2':
'@esbuild/openbsd-x64@0.27.0':
optional: true
'@esbuild/openharmony-arm64@0.25.9':
optional: true
'@esbuild/openharmony-arm64@0.27.2':
'@esbuild/openharmony-arm64@0.27.0':
optional: true
'@esbuild/sunos-x64@0.25.5':
@@ -8653,7 +8683,7 @@ snapshots:
'@esbuild/sunos-x64@0.25.9':
optional: true
'@esbuild/sunos-x64@0.27.2':
'@esbuild/sunos-x64@0.27.0':
optional: true
'@esbuild/win32-arm64@0.25.5':
@@ -8662,7 +8692,7 @@ snapshots:
'@esbuild/win32-arm64@0.25.9':
optional: true
'@esbuild/win32-arm64@0.27.2':
'@esbuild/win32-arm64@0.27.0':
optional: true
'@esbuild/win32-ia32@0.25.5':
@@ -8671,7 +8701,7 @@ snapshots:
'@esbuild/win32-ia32@0.25.9':
optional: true
'@esbuild/win32-ia32@0.27.2':
'@esbuild/win32-ia32@0.27.0':
optional: true
'@esbuild/win32-x64@0.25.5':
@@ -8680,7 +8710,7 @@ snapshots:
'@esbuild/win32-x64@0.25.9':
optional: true
'@esbuild/win32-x64@0.27.2':
'@esbuild/win32-x64@0.27.0':
optional: true
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))':
@@ -9069,7 +9099,7 @@ snapshots:
'@jest/schemas@30.0.5':
dependencies:
'@sinclair/typebox': 0.34.46
'@sinclair/typebox': 0.34.41
'@jest/snapshot-utils@30.2.0':
dependencies:
@@ -9423,7 +9453,7 @@ snapshots:
'@npmcli/fs@4.0.0':
dependencies:
semver: 7.7.2
semver: 7.7.3
'@npmcli/git@6.0.3':
dependencies:
@@ -9433,7 +9463,7 @@ snapshots:
npm-pick-manifest: 10.0.0
proc-log: 5.0.0
promise-retry: 2.0.1
semver: 7.7.2
semver: 7.7.3
which: 5.0.0
'@npmcli/installed-package-contents@3.0.0':
@@ -9450,7 +9480,7 @@ snapshots:
hosted-git-info: 8.1.0
json-parse-even-better-errors: 4.0.0
proc-log: 5.0.0
semver: 7.7.2
semver: 7.7.3
validate-npm-package-license: 3.0.4
'@npmcli/promise-spawn@8.0.3':
@@ -9768,7 +9798,7 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
'@sinclair/typebox@0.34.46': {}
'@sinclair/typebox@0.34.41': {}
'@sinonjs/commons@3.0.1':
dependencies:
@@ -10790,7 +10820,7 @@ snapshots:
postcss-modules-scope: 3.2.1(postcss@8.5.3)
postcss-modules-values: 4.0.0(postcss@8.5.3)
postcss-value-parser: 4.2.0
semver: 7.7.2
semver: 7.7.3
optionalDependencies:
webpack: 5.99.8(esbuild@0.25.5)
@@ -10998,9 +11028,11 @@ snapshots:
dependencies:
es-errors: 1.3.0
esbuild-wasm@0.25.10: {}
esbuild-wasm@0.25.5: {}
esbuild-wasm@0.27.2: {}
esbuild-wasm@0.27.0: {}
esbuild@0.25.5:
optionalDependencies:
@@ -11059,34 +11091,34 @@ snapshots:
'@esbuild/win32-ia32': 0.25.9
'@esbuild/win32-x64': 0.25.9
esbuild@0.27.2:
esbuild@0.27.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2
'@esbuild/android-arm': 0.27.2
'@esbuild/android-arm64': 0.27.2
'@esbuild/android-x64': 0.27.2
'@esbuild/darwin-arm64': 0.27.2
'@esbuild/darwin-x64': 0.27.2
'@esbuild/freebsd-arm64': 0.27.2
'@esbuild/freebsd-x64': 0.27.2
'@esbuild/linux-arm': 0.27.2
'@esbuild/linux-arm64': 0.27.2
'@esbuild/linux-ia32': 0.27.2
'@esbuild/linux-loong64': 0.27.2
'@esbuild/linux-mips64el': 0.27.2
'@esbuild/linux-ppc64': 0.27.2
'@esbuild/linux-riscv64': 0.27.2
'@esbuild/linux-s390x': 0.27.2
'@esbuild/linux-x64': 0.27.2
'@esbuild/netbsd-arm64': 0.27.2
'@esbuild/netbsd-x64': 0.27.2
'@esbuild/openbsd-arm64': 0.27.2
'@esbuild/openbsd-x64': 0.27.2
'@esbuild/openharmony-arm64': 0.27.2
'@esbuild/sunos-x64': 0.27.2
'@esbuild/win32-arm64': 0.27.2
'@esbuild/win32-ia32': 0.27.2
'@esbuild/win32-x64': 0.27.2
'@esbuild/aix-ppc64': 0.27.0
'@esbuild/android-arm': 0.27.0
'@esbuild/android-arm64': 0.27.0
'@esbuild/android-x64': 0.27.0
'@esbuild/darwin-arm64': 0.27.0
'@esbuild/darwin-x64': 0.27.0
'@esbuild/freebsd-arm64': 0.27.0
'@esbuild/freebsd-x64': 0.27.0
'@esbuild/linux-arm': 0.27.0
'@esbuild/linux-arm64': 0.27.0
'@esbuild/linux-ia32': 0.27.0
'@esbuild/linux-loong64': 0.27.0
'@esbuild/linux-mips64el': 0.27.0
'@esbuild/linux-ppc64': 0.27.0
'@esbuild/linux-riscv64': 0.27.0
'@esbuild/linux-s390x': 0.27.0
'@esbuild/linux-x64': 0.27.0
'@esbuild/netbsd-arm64': 0.27.0
'@esbuild/netbsd-x64': 0.27.0
'@esbuild/openbsd-arm64': 0.27.0
'@esbuild/openbsd-x64': 0.27.0
'@esbuild/openharmony-arm64': 0.27.0
'@esbuild/sunos-x64': 0.27.0
'@esbuild/win32-arm64': 0.27.0
'@esbuild/win32-ia32': 0.27.0
'@esbuild/win32-x64': 0.27.0
optional: true
escalade@3.2.0: {}
@@ -11333,10 +11365,6 @@ snapshots:
dependencies:
bser: 2.1.1
fdir@6.5.0(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@@ -12087,15 +12115,15 @@ snapshots:
'@angular/core': 20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser-dynamic': 20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.15)(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)))
bs-logger: 0.2.6
esbuild-wasm: 0.27.2
esbuild-wasm: 0.25.10
jest: 30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3))
jest-environment-jsdom: 29.7.0(canvas@3.0.0)
jest-util: 29.7.0
pretty-format: 29.7.0
ts-jest: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
ts-jest: 29.4.4(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
typescript: 5.8.3
optionalDependencies:
esbuild: 0.27.2
esbuild: 0.27.0
jsdom: 26.1.0(canvas@3.0.0)
transitivePeerDependencies:
- '@babel/core'
@@ -12107,7 +12135,7 @@ snapshots:
- supports-color
- utf-8-validate
jest-preset-angular@16.0.0(e164a15bea80c603eb581d5724a352ec):
jest-preset-angular@15.0.3(e164a15bea80c603eb581d5724a352ec):
dependencies:
'@angular/compiler-cli': 20.3.15(@angular/compiler@20.3.15)(typescript@5.8.3)
'@angular/core': 20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)
@@ -12115,15 +12143,15 @@ snapshots:
'@angular/platform-browser-dynamic': 20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.15)(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.15(@angular/common@20.3.15(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.15(@angular/compiler@20.3.15)(rxjs@7.8.2)(zone.js@0.15.1)))
'@jest/environment-jsdom-abstract': 30.2.0(canvas@3.0.0)(jsdom@26.1.0(canvas@3.0.0))
bs-logger: 0.2.6
esbuild-wasm: 0.27.2
esbuild-wasm: 0.27.0
jest: 30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3))
jest-util: 30.2.0
jsdom: 26.1.0(canvas@3.0.0)
pretty-format: 30.2.0
ts-jest: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
ts-jest: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3)
typescript: 5.8.3
optionalDependencies:
esbuild: 0.27.2
esbuild: 0.27.0
transitivePeerDependencies:
- '@babel/core'
- '@jest/transform'
@@ -12853,7 +12881,7 @@ snapshots:
make-fetch-happen: 14.0.3
nopt: 8.1.0
proc-log: 5.0.0
semver: 7.7.2
semver: 7.7.3
tar: 7.5.2
tinyglobby: 0.2.15
which: 5.0.0
@@ -12878,7 +12906,7 @@ snapshots:
npm-install-checks@7.1.2:
dependencies:
semver: 7.7.2
semver: 7.7.3
npm-normalize-package-bin@4.0.0: {}
@@ -12886,7 +12914,7 @@ snapshots:
dependencies:
hosted-git-info: 8.1.0
proc-log: 5.0.0
semver: 7.7.2
semver: 7.7.3
validate-npm-package-name: 6.0.2
npm-package-arg@13.0.0:
@@ -12906,7 +12934,7 @@ snapshots:
npm-install-checks: 7.1.2
npm-normalize-package-bin: 4.0.0
npm-package-arg: 12.0.2
semver: 7.7.2
semver: 7.7.3
npm-registry-fetch@18.0.2:
dependencies:
@@ -13146,7 +13174,7 @@ snapshots:
cosmiconfig: 9.0.0(typescript@5.8.3)
jiti: 1.21.7
postcss: 8.5.3
semver: 7.7.2
semver: 7.7.3
optionalDependencies:
webpack: 5.99.8(esbuild@0.25.5)
transitivePeerDependencies:
@@ -13943,8 +13971,8 @@ snapshots:
tinyglobby@0.2.13:
dependencies:
fdir: 6.5.0(picomatch@4.0.2)
picomatch: 4.0.2
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinyglobby@0.2.14:
dependencies:
@@ -13999,7 +14027,7 @@ snapshots:
dependencies:
typescript: 5.8.3
ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
ts-jest@29.4.4(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@29.7.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -14017,10 +14045,10 @@ snapshots:
'@jest/transform': 30.2.0
'@jest/types': 30.2.0
babel-jest: 30.2.0(@babel/core@7.28.5)
esbuild: 0.27.2
esbuild: 0.27.0
jest-util: 29.7.0
ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(typescript@5.8.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -14038,7 +14066,7 @@ snapshots:
'@jest/transform': 30.2.0
'@jest/types': 30.2.0
babel-jest: 30.2.0(@babel/core@7.28.5)
esbuild: 0.27.2
esbuild: 0.27.0
jest-util: 30.2.0
ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3):
@@ -14216,9 +14244,9 @@ snapshots:
vite@6.3.5(@types/node@24.10.1)(jiti@1.21.7)(less@4.3.0)(sass@1.88.0)(terser@5.39.1)(yaml@2.7.0):
dependencies:
esbuild: 0.25.5
fdir: 6.5.0(picomatch@4.0.2)
picomatch: 4.0.2
esbuild: 0.25.9
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.52.3
tinyglobby: 0.2.15

View File

@@ -16,6 +16,7 @@ from pikepdf import Pdf
from documents.converters import convert_from_tiff_to_pdf
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentMetadataOverrides
from documents.models import Document
from documents.models import Tag
from documents.plugins.base import ConsumeTaskPlugin
from documents.plugins.base import StopConsumeTaskError
@@ -115,6 +116,24 @@ class BarcodePlugin(ConsumeTaskPlugin):
self._tiff_conversion_done = False
self.barcodes: list[Barcode] = []
def _apply_detected_asn(self, detected_asn: int) -> None:
"""
Apply a detected ASN to metadata if allowed.
"""
if (
self.metadata.skip_asn_if_exists
and Document.global_objects.filter(
archive_serial_number=detected_asn,
).exists()
):
logger.info(
f"Found ASN in barcode {detected_asn} but skipping because it already exists.",
)
return
logger.info(f"Found ASN in barcode: {detected_asn}")
self.metadata.asn = detected_asn
def run(self) -> None:
# Some operations may use PIL, override pixel setting if needed
maybe_override_pixel_limit()
@@ -186,13 +205,8 @@ class BarcodePlugin(ConsumeTaskPlugin):
# Update/overwrite an ASN if possible
# After splitting, as otherwise each split document gets the same ASN
if (
self.settings.barcode_enable_asn
and not self.metadata.skip_asn
and (located_asn := self.asn) is not None
):
logger.info(f"Found ASN in barcode: {located_asn}")
self.metadata.asn = located_asn
if self.settings.barcode_enable_asn and (located_asn := self.asn) is not None:
self._apply_detected_asn(located_asn)
def cleanup(self) -> None:
self.temp_dir.cleanup()

View File

@@ -7,7 +7,6 @@ from pathlib import Path
from typing import TYPE_CHECKING
from typing import Literal
from celery import chain
from celery import chord
from celery import group
from celery import shared_task
@@ -38,6 +37,42 @@ if TYPE_CHECKING:
logger: logging.Logger = logging.getLogger("paperless.bulk_edit")
@shared_task(bind=True)
def restore_archive_serial_numbers_task(
self,
backup: dict[int, int],
*args,
**kwargs,
) -> None:
restore_archive_serial_numbers(backup)
def release_archive_serial_numbers(doc_ids: list[int]) -> dict[int, int]:
"""
Clears ASNs on documents that are about to be replaced so new documents
can be assigned ASNs without uniqueness collisions. Returns a backup map
of doc_id -> previous ASN for potential restoration.
"""
qs = Document.objects.filter(
id__in=doc_ids,
archive_serial_number__isnull=False,
).only("pk", "archive_serial_number")
backup = dict(qs.values_list("pk", "archive_serial_number"))
qs.update(archive_serial_number=None)
logger.info(f"Released archive serial numbers for documents {list(backup.keys())}")
return backup
def restore_archive_serial_numbers(backup: dict[int, int]) -> None:
"""
Restores ASNs using the provided backup map, intended for
rollback when replacement consumption fails.
"""
for doc_id, asn in backup.items():
Document.objects.filter(pk=doc_id).update(archive_serial_number=asn)
logger.info(f"Restored archive serial numbers for documents {list(backup.keys())}")
def set_correspondent(
doc_ids: list[int],
correspondent: Correspondent,
@@ -386,6 +421,7 @@ def merge(
merged_pdf = pikepdf.new()
version: str = merged_pdf.pdf_version
handoff_asn: int | None = None
# use doc_ids to preserve order
for doc_id in doc_ids:
doc = qs.get(id=doc_id)
@@ -401,6 +437,8 @@ def merge(
version = max(version, pdf.pdf_version)
merged_pdf.pages.extend(pdf.pages)
affected_docs.append(doc.id)
if handoff_asn is None and doc.archive_serial_number is not None:
handoff_asn = doc.archive_serial_number
except Exception as e:
logger.exception(
f"Error merging document {doc.id}, it will not be included in the merge: {e}",
@@ -426,6 +464,8 @@ def merge(
DocumentMetadataOverrides.from_document(metadata_document)
)
overrides.title = metadata_document.title + " (merged)"
if metadata_document.archive_serial_number is not None:
handoff_asn = metadata_document.archive_serial_number
else:
overrides = DocumentMetadataOverrides()
else:
@@ -433,8 +473,11 @@ def merge(
if user is not None:
overrides.owner_id = user.id
# Avoid copying or detecting ASN from merged PDFs to prevent collision
overrides.skip_asn = True
if not delete_originals:
overrides.skip_asn_if_exists = True
if delete_originals and handoff_asn is not None:
overrides.asn = handoff_asn
logger.info("Adding merged document to the task queue.")
@@ -447,12 +490,20 @@ def merge(
)
if delete_originals:
backup = release_archive_serial_numbers(affected_docs)
logger.info(
"Queueing removal of original documents after consumption of merged document",
)
chain(consume_task, delete.si(affected_docs)).delay()
else:
consume_task.delay()
try:
consume_task.apply_async(
link=[delete.si(affected_docs)],
link_error=[restore_archive_serial_numbers_task.s(backup)],
)
except Exception:
restore_archive_serial_numbers(backup)
raise
else:
consume_task.delay()
return "OK"
@@ -494,6 +545,8 @@ def split(
overrides.title = f"{doc.title} (split {idx + 1})"
if user is not None:
overrides.owner_id = user.id
if not delete_originals:
overrides.skip_asn_if_exists = True
logger.info(
f"Adding split document with pages {split_doc} to the task queue.",
)
@@ -508,10 +561,20 @@ def split(
)
if delete_originals:
backup = release_archive_serial_numbers([doc.id])
logger.info(
"Queueing removal of original document after consumption of the split documents",
)
chord(header=consume_tasks, body=delete.si([doc.id])).delay()
try:
chord(
header=consume_tasks,
body=delete.si([doc.id]),
).apply_async(
link_error=[restore_archive_serial_numbers_task.s(backup)],
)
except Exception:
restore_archive_serial_numbers(backup)
raise
else:
group(consume_tasks).delay()
@@ -614,7 +677,10 @@ def edit_pdf(
)
if user is not None:
overrides.owner_id = user.id
if not delete_original:
overrides.skip_asn_if_exists = True
if delete_original and len(pdf_docs) == 1:
overrides.asn = doc.archive_serial_number
for idx, pdf in enumerate(pdf_docs, start=1):
filepath: Path = (
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
@@ -633,7 +699,17 @@ def edit_pdf(
)
if delete_original:
chord(header=consume_tasks, body=delete.si([doc.id])).delay()
backup = release_archive_serial_numbers([doc.id])
try:
chord(
header=consume_tasks,
body=delete.si([doc.id]),
).apply_async(
link_error=[restore_archive_serial_numbers_task.s(backup)],
)
except Exception:
restore_archive_serial_numbers(backup)
raise
else:
group(consume_tasks).delay()

View File

@@ -696,7 +696,7 @@ class ConsumerPlugin(
pk=self.metadata.storage_path_id,
)
if self.metadata.asn is not None and not self.metadata.skip_asn:
if self.metadata.asn is not None:
document.archive_serial_number = self.metadata.asn
if self.metadata.owner_id:
@@ -812,8 +812,8 @@ class ConsumerPreflightPlugin(
"""
Check that if override_asn is given, it is unique and within a valid range
"""
if self.metadata.skip_asn or self.metadata.asn is None:
# if skip is set or ASN is None
if self.metadata.asn is None:
# if ASN is None
return
# Validate the range is above zero and less than uint32_t max
# otherwise, Whoosh can't handle it in the index

View File

@@ -30,7 +30,7 @@ class DocumentMetadataOverrides:
change_users: list[int] | None = None
change_groups: list[int] | None = None
custom_fields: dict | None = None
skip_asn: bool = False
skip_asn_if_exists: bool = False
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
"""
@@ -50,8 +50,8 @@ class DocumentMetadataOverrides:
self.storage_path_id = other.storage_path_id
if other.owner_id is not None:
self.owner_id = other.owner_id
if other.skip_asn:
self.skip_asn = True
if other.skip_asn_if_exists:
self.skip_asn_if_exists = True
# merge
if self.tag_ids is None:

View File

@@ -602,23 +602,21 @@ class TestPDFActions(DirectoriesMixin, TestCase):
expected_filename,
)
self.assertEqual(consume_file_args[1].title, None)
self.assertTrue(consume_file_args[1].skip_asn)
# No metadata_document_id, delete_originals False, so ASN should be None
self.assertIsNone(consume_file_args[1].asn)
# With metadata_document_id overrides
result = bulk_edit.merge(doc_ids, metadata_document_id=metadata_document_id)
consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].title, "B (merged)")
self.assertEqual(consume_file_args[1].created, self.doc2.created)
self.assertTrue(consume_file_args[1].skip_asn)
self.assertEqual(result, "OK")
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.chain")
def test_merge_and_delete_originals(
self,
mock_chain,
mock_consume_file,
mock_delete_documents,
):
@@ -632,6 +630,12 @@ class TestPDFActions(DirectoriesMixin, TestCase):
- Document deletion task should be called
"""
doc_ids = [self.doc1.id, self.doc2.id, self.doc3.id]
self.doc1.archive_serial_number = 101
self.doc2.archive_serial_number = 102
self.doc3.archive_serial_number = 103
self.doc1.save()
self.doc2.save()
self.doc3.save()
result = bulk_edit.merge(doc_ids, delete_originals=True)
self.assertEqual(result, "OK")
@@ -642,7 +646,8 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mock_consume_file.assert_called()
mock_delete_documents.assert_called()
mock_chain.assert_called_once()
consume_sig = mock_consume_file.return_value
consume_sig.apply_async.assert_called_once()
consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(
@@ -650,7 +655,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
expected_filename,
)
self.assertEqual(consume_file_args[1].title, None)
self.assertTrue(consume_file_args[1].skip_asn)
self.assertEqual(consume_file_args[1].asn, 101)
delete_documents_args, _ = mock_delete_documents.call_args
self.assertEqual(
@@ -658,6 +663,92 @@ class TestPDFActions(DirectoriesMixin, TestCase):
doc_ids,
)
self.doc1.refresh_from_db()
self.doc2.refresh_from_db()
self.doc3.refresh_from_db()
self.assertIsNone(self.doc1.archive_serial_number)
self.assertIsNone(self.doc2.archive_serial_number)
self.assertIsNone(self.doc3.archive_serial_number)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
def test_merge_and_delete_originals_restore_on_failure(
self,
mock_consume_file,
mock_delete_documents,
):
"""
GIVEN:
- Existing documents
WHEN:
- Merge action with deleting documents is called with 1 document
- Error occurs when queuing consume file task
THEN:
- Archive serial numbers are restored
"""
doc_ids = [self.doc1.id]
self.doc1.archive_serial_number = 111
self.doc1.save()
sig = mock.Mock()
sig.apply_async.side_effect = Exception("boom")
mock_consume_file.return_value = sig
with self.assertRaises(Exception):
bulk_edit.merge(doc_ids, delete_originals=True)
self.doc1.refresh_from_db()
self.assertEqual(self.doc1.archive_serial_number, 111)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
def test_merge_and_delete_originals_metadata_handoff(
self,
mock_consume_file,
mock_delete_documents,
):
"""
GIVEN:
- Existing documents with ASNs
WHEN:
- Merge with delete_originals=True and metadata_document_id set
THEN:
- Handoff ASN uses metadata document ASN
"""
doc_ids = [self.doc1.id, self.doc2.id]
self.doc1.archive_serial_number = 101
self.doc2.archive_serial_number = 202
self.doc1.save()
self.doc2.save()
result = bulk_edit.merge(
doc_ids,
metadata_document_id=self.doc2.id,
delete_originals=True,
)
self.assertEqual(result, "OK")
consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].asn, 202)
def test_restore_archive_serial_numbers_task(self):
"""
GIVEN:
- Existing document with no archive serial number
WHEN:
- Restore archive serial number task is called with backup data
THEN:
- Document archive serial number is restored
"""
self.doc1.archive_serial_number = 444
self.doc1.save()
Document.objects.filter(pk=self.doc1.id).update(archive_serial_number=None)
backup = {self.doc1.id: 444}
bulk_edit.restore_archive_serial_numbers_task(backup)
self.doc1.refresh_from_db()
self.assertEqual(self.doc1.archive_serial_number, 444)
@mock.patch("documents.tasks.consume_file.s")
def test_merge_with_archive_fallback(self, mock_consume_file):
"""
@@ -726,6 +817,7 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(mock_consume_file.call_count, 2)
consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].title, "B (split 2)")
self.assertIsNone(consume_file_args[1].asn)
self.assertEqual(result, "OK")
@@ -750,6 +842,8 @@ class TestPDFActions(DirectoriesMixin, TestCase):
"""
doc_ids = [self.doc2.id]
pages = [[1, 2], [3]]
self.doc2.archive_serial_number = 200
self.doc2.save()
result = bulk_edit.split(doc_ids, pages, delete_originals=True)
self.assertEqual(result, "OK")
@@ -767,6 +861,42 @@ class TestPDFActions(DirectoriesMixin, TestCase):
doc_ids,
)
self.doc2.refresh_from_db()
self.assertIsNone(self.doc2.archive_serial_number)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.chord")
def test_split_restore_on_failure(
self,
mock_chord,
mock_consume_file,
mock_delete_documents,
):
"""
GIVEN:
- Existing documents
WHEN:
- Split action with deleting documents is called with 1 document and 2 page groups
- Error occurs when queuing chord task
THEN:
- Archive serial numbers are restored
"""
doc_ids = [self.doc2.id]
pages = [[1, 2]]
self.doc2.archive_serial_number = 222
self.doc2.save()
sig = mock.Mock()
sig.apply_async.side_effect = Exception("boom")
mock_chord.return_value = sig
result = bulk_edit.split(doc_ids, pages, delete_originals=True)
self.assertEqual(result, "OK")
self.doc2.refresh_from_db()
self.assertEqual(self.doc2.archive_serial_number, 222)
@mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.Pdf.save")
def test_split_with_errors(self, mock_save_pdf, mock_consume_file):
@@ -967,10 +1097,49 @@ class TestPDFActions(DirectoriesMixin, TestCase):
mock_chord.return_value.delay.return_value = None
doc_ids = [self.doc2.id]
operations = [{"page": 1}, {"page": 2}]
self.doc2.archive_serial_number = 250
self.doc2.save()
result = bulk_edit.edit_pdf(doc_ids, operations, delete_original=True)
self.assertEqual(result, "OK")
mock_chord.assert_called_once()
consume_file_args, _ = mock_consume_file.call_args
self.assertEqual(consume_file_args[1].asn, 250)
self.doc2.refresh_from_db()
self.assertIsNone(self.doc2.archive_serial_number)
@mock.patch("documents.bulk_edit.delete.si")
@mock.patch("documents.tasks.consume_file.s")
@mock.patch("documents.bulk_edit.chord")
def test_edit_pdf_restore_on_failure(
self,
mock_chord,
mock_consume_file,
mock_delete_documents,
):
"""
GIVEN:
- Existing document
WHEN:
- edit_pdf is called with delete_original=True
- Error occurs when queuing chord task
THEN:
- Archive serial numbers are restored
"""
doc_ids = [self.doc2.id]
operations = [{"page": 1}]
self.doc2.archive_serial_number = 333
self.doc2.save()
sig = mock.Mock()
sig.apply_async.side_effect = Exception("boom")
mock_chord.return_value = sig
with self.assertRaises(Exception):
bulk_edit.edit_pdf(doc_ids, operations, delete_original=True)
self.doc2.refresh_from_db()
self.assertEqual(self.doc2.archive_serial_number, 333)
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay")
def test_edit_pdf_with_update_document(self, mock_update_document):

View File

@@ -14,6 +14,7 @@ from django.test import override_settings
from django.utils import timezone
from guardian.core import ObjectPermissionChecker
from documents.barcodes import BarcodePlugin
from documents.consumer import ConsumerError
from documents.data_models import DocumentMetadataOverrides
from documents.data_models import DocumentSource
@@ -412,14 +413,6 @@ class TestConsumer(
self.assertEqual(document.archive_serial_number, 123)
self._assert_first_last_send_progress()
def testMetadataOverridesSkipAsnPropagation(self):
overrides = DocumentMetadataOverrides()
incoming = DocumentMetadataOverrides(skip_asn=True)
overrides.update(incoming)
self.assertTrue(overrides.skip_asn)
def testOverrideTitlePlaceholders(self):
c = Correspondent.objects.create(name="Correspondent Name")
dt = DocumentType.objects.create(name="DocType Name")
@@ -1240,3 +1233,46 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
r"sample\.pdf: Error while executing post-consume script: Command '\[.*\]' returned non-zero exit status \d+\.",
):
consumer.run_post_consume_script(doc)
class TestMetadataOverrides(TestCase):
def test_update_skip_asn_if_exists(self):
base = DocumentMetadataOverrides()
incoming = DocumentMetadataOverrides(skip_asn_if_exists=True)
base.update(incoming)
self.assertTrue(base.skip_asn_if_exists)
class TestBarcodeApplyDetectedASN(TestCase):
"""
GIVEN:
- Existing Documents with ASN 123
WHEN:
- A BarcodePlugin which detected an ASN
THEN:
- If skip_asn_if_exists is set, and ASN exists, do not set ASN
- If skip_asn_if_exists is set, and ASN does not exist, set ASN
"""
def test_apply_detected_asn_skips_existing_when_flag_set(self):
doc = Document.objects.create(
checksum="X1",
title="D1",
archive_serial_number=123,
)
metadata = DocumentMetadataOverrides(skip_asn_if_exists=True)
plugin = BarcodePlugin(
input_doc=mock.Mock(),
metadata=metadata,
status_mgr=mock.Mock(),
base_tmp_dir=tempfile.gettempdir(),
task_id="test-task",
)
plugin._apply_detected_asn(123)
self.assertIsNone(plugin.metadata.asn)
doc.hard_delete()
plugin._apply_detected_asn(123)
self.assertEqual(plugin.metadata.asn, 123)