From 049e4dc7d3ce90acb7aae4c66320823be336556d Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 7 Nov 2026 06:46:46 +1100 Subject: [PATCH 1/3] Bring over upload from the widgets so it can be reused Prompt: move the uploadFiles from solid-ui widgets to resource logic, remove ui, add resilient file failure, return structured messages for results. Co-authored-by: GPT-5.6 Luna --- package-lock.json | 319 ++++++++++++++++++---------------- package.json | 3 +- src/index.ts | 3 +- src/resource/resourceLogic.ts | 82 ++++++++- src/types.ts | 20 +++ test/resourceLogic.test.ts | 83 ++++++++- 6 files changed, 353 insertions(+), 157 deletions(-) diff --git a/package-lock.json b/package-lock.json index 075703a3..bb50b1a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "solid-logic", - "version": "6.0.1-5", + "version": "6.0.1-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "solid-logic", - "version": "6.0.1-5", + "version": "6.0.1-6", "license": "MIT", "dependencies": { "@dokieli/web-access-control": "^1.1.0", "@uvdsl/solid-oidc-client-browser": "^0.2.3", + "mime-types": "^3.0.2", "solid-namespace": "^0.5.4" }, "devDependencies": { @@ -79,21 +80,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.13.tgz", + "integrity": "sha512-j1nvOQCZYSgp7HLmisnCXFTpVvFODiijQ9QHkORfGKWH6YDtskTEeCJS7eV545GZ9UBzrq0e2xsOd5QQ5PjIqw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/generator": "^7.29.13", + "@babel/helper-compilation-targets": "^7.29.13", + "@babel/helper-module-transforms": "^7.29.13", + "@babel/helpers": "^7.29.13", + "@babel/parser": "^7.29.13", "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/traverse": "^7.29.13", + "@babel/types": "^7.29.11", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -110,9 +111,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -120,13 +121,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", - "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.13.tgz", + "integrity": "sha512-T5FRo+RzVHvGjWOuF1Ljc44b+NXAOazodHkGUHp4I3zCkQ4/sNVJm56h1S278LMR0bY5xBhztGziSbli8zObIw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.12", + "@babel/parser": "^7.29.13", "@babel/types": "^7.29.11", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", @@ -150,9 +151,9 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.13.tgz", + "integrity": "sha512-8WxJ4/Vmes3ghacj8VOJy3NW8eyHiFY/4thU/LSBuBE470+igjR/j1ZTNn8fV5VdNrLx6iXUwig526UdOvfP0w==", "dev": true, "license": "MIT", "dependencies": { @@ -177,9 +178,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -194,9 +195,9 @@ "license": "ISC" }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.12.tgz", - "integrity": "sha512-ACxGoWMV866q/QlZHeCOaQZAR5QclKsIvkAmlDemqjm6+6Bynijs1mH068BzH2N2e+wSuDmBWY87fqKfvhi1Lw==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.13.tgz", + "integrity": "sha512-fo8/Fr44/DMV71FQjk/HA0Npmu3bo5DFoWTCW1dhUR8Vx3NFfAVeEy0TbcBpt4e+hd2QhdmWgmspHo/9iWj8RA==", "dev": true, "license": "MIT", "dependencies": { @@ -204,8 +205,8 @@ "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.12", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.13", + "@babel/traverse": "^7.29.13", "semver": "^6.3.1" }, "engines": { @@ -216,9 +217,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -244,9 +245,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -271,14 +272,14 @@ } }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { - "version": "1.22.13", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", - "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "version": "1.22.14", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.14.tgz", + "integrity": "sha512-1ZjYvR9sbkiwC5+9QHetqW5HmA2pHRMaoupD01t2HlYnV/vydC2zPwLixhsMieSWVZt0JJRg5LVIk0Ow1D/TrA==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "is-core-module": "^2.17.0", + "is-core-module": "^2.18.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -331,15 +332,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.13.tgz", + "integrity": "sha512-/U96wOz+SCoZ7Da/8kjDGltcNY280JPQcRNPK0Hk3PTbdpaiBDQImtC6JsbVwb1HGMr3usZQU2WsWVjhYNmRzw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" + "@babel/traverse": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -408,23 +409,23 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", - "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.13.tgz", + "integrity": "sha512-Abm4rKIH0tmCayJLwcoeOkdzpYUphTbO52fHhyEBHn7S0R+ni4epNLI/8nq1L0DN2Bl0u5qmgWm2Og/z4fBb+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" + "@babel/traverse": "^7.29.13", + "@babel/types": "^7.29.11" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.29.11", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", - "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.13.tgz", + "integrity": "sha512-fWegr+WsEsiMVW4KUNdIIzzMTZSzNiqhy0//6+2MRi2d5NFNsL/AYwBQW8eBnRbTBVRnDVzjaJEUqKaCWjMMdQ==", "dev": true, "license": "MIT", "engines": { @@ -467,23 +468,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", - "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.13.tgz", + "integrity": "sha512-laoRpD/kO/iA4CDlgJbixHekg7oPvWALK0PQyK3Ob2I2njZRvVAD77RfWCIImtqWCq6KijoBoZK7bp/HlTWASQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8" + "@babel/types": "^7.29.11" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", - "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.13.tgz", + "integrity": "sha512-fJM50YYjlTiBhgdkGmC1tBpv+FMAyo4annf2X6Aks+0yRlaiVkYW/3f07xAfVVkYIoZ9fFZp+mEThUFnuybhkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -546,14 +547,14 @@ } }, "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", - "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.13.tgz", + "integrity": "sha512-7RpEgFZq9xNRHsWwPLFQpjPOi0ZECE2QZWMBes+vgK7E02krjOfPiYtnHTIUF2nCDyeCJPvSAj0AP4NTVf4hsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -794,9 +795,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.12.tgz", - "integrity": "sha512-nEreZBofjfd6FG621xe9Rk3+tMGBXf1nAuFrR6NUGF67fkQbSQf3NrbRuhgWRn+zb0qcy9XVok1GVFI/t9wWVQ==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.13.tgz", + "integrity": "sha512-q7f5zYLtDLqO11ydQK1w2RqZ1m5kmcAlmgZlfXssKgqVyL8+HgDvWXMxvuEz8TuJfq1UNsY9sB6ABjjtQkN6wA==", "dev": true, "license": "MIT", "dependencies": { @@ -882,14 +883,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", - "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.13.tgz", + "integrity": "sha512-X2meXB39NOPPmmshSRJzyd50hX9uCzbscNQwVCjLCtq363PZIJFFxJIeZPpgAewC570vpdeGIHTcltwd0QUlJg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", - "@babel/traverse": "^7.29.7" + "@babel/traverse": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -1014,14 +1015,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", - "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.13.tgz", + "integrity": "sha512-dFjiIw+xBYpgLhYDc1QFQOZmMX44se0YCmXUjPsm9SQEj9HaCJAjvVnK6/yK9iE4E1LaKU5fIAKVJPZWCbKwXw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.13" }, "engines": { "node": ">=6.9.0" @@ -1455,9 +1456,9 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -1733,9 +1734,9 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -1778,9 +1779,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", - "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.13.tgz", + "integrity": "sha512-v+oN26xP4YIlQsbQ/CbTH1WG5m/l/DjKKZNy/Kp3f/AFvbgt7Em/D0FeMM3O2hX0YzeN2M16W8lOpyhIkfCBcQ==", "dev": true, "license": "MIT", "engines": { @@ -1803,16 +1804,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.12", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", - "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", + "version": "7.29.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.13.tgz", + "integrity": "sha512-0ve0xMlFA73dgY1oyJRY6RoW+/bncdcOQqg6jjYcVfXBK90OdLXjm9R6lmRetdZVrPjXkKIoMVBqYFPjcAlZ4Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.12", + "@babel/generator": "^7.29.13", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.12", + "@babel/parser": "^7.29.13", "@babel/template": "^7.29.7", "@babel/types": "^7.29.11", "debug": "^4.3.1" @@ -3720,9 +3721,9 @@ } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -3787,9 +3788,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.34", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.34.tgz", - "integrity": "sha512-Aqr8mA1kIvyJ3UY5YAY7ib8LSo1DmDMbMnVqGPtsKW1LxaFQFwn9Eb3yvq2fUKJsRvaXjnAV7R5osqETapzNiA==", + "version": "2.11.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz", + "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3939,9 +3940,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001820", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", - "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", + "version": "1.0.30001823", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001823.tgz", + "integrity": "sha512-TQ5s/djDjNk1axreUW+4rULH+BmdCMLiJkhaXnO5wqivG7tITkSUj4ItIdzqdZHKaXcHAV8dhH3JxOM7/7bwqw==", "dev": true, "funding": [ { @@ -4331,16 +4332,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.472", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.472.tgz", - "integrity": "sha512-s4+nfT29b0FKpVKoaIxl2pWsoNcn7A/nMPSrxayw4T3BsmPeV2lF9Y06BsAQeMGhCTQ4nYJ5XST7jkqe4ZEqFw==", + "version": "1.5.478", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.478.tgz", + "integrity": "sha512-pftG+ma5Lm8O6/p45bhYT6StBN5ziQymaRzmCZwFl8DwWHKPPD93FdOmP1aqftcepolQYvrcyTLa17kE8Yf+jg==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", - "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "version": "5.26.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.1.tgz", + "integrity": "sha512-PPnnSJq3azKOIewXR/uppBV/6h98dprBJ/WaQ9Qgj10xX1lJLNfg+XShpbsrSqcDSy2yYYyhfXM1wWhftqgyzw==", "dev": true, "license": "MIT", "peer": true, @@ -4791,9 +4792,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -5151,6 +5152,29 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5682,9 +5706,9 @@ } }, "node_modules/is-core-module": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", - "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.18.0.tgz", + "integrity": "sha512-qyax9fgaKOEC4qK8WZn7Q4IcoZC9RC1O1fFxRcRTq/oMMkWT/uXpqpXa12kzWR409zAt1drYN9N9BO26UmBCmw==", "dev": true, "license": "MIT", "dependencies": { @@ -6708,26 +6732,28 @@ "peer": true }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/minimatch": { @@ -6927,9 +6953,9 @@ } }, "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -7546,16 +7572,16 @@ } }, "node_modules/regexpu-core": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.5.3.tgz", - "integrity": "sha512-E8VgUoSK+LMQrLboDxavm76MTBGrQhx9RiNuomRmYnZ10Uw12HrI8fkxOOncktb2hBpzIZ9SV/347euQHG5z+g==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.5.4.tgz", + "integrity": "sha512-VpdamrZ9bw4YmvMZfTA3Yg8IHU9Guvg21bonvgjC4l3KfBkOqCPJesHakz7cQeE4Vh4zz4r4Gy6igAcnJxZv9g==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.5.0", "regenerate-unicode-properties": "^10.3.0", "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", + "regjsparser": "^0.14.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.2.1" }, @@ -7571,9 +7597,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.4.tgz", - "integrity": "sha512-EWV80YgFaVgFrjGCAsV7oybp4GOxbls5RzBg8OB0nKEVXsBJR1JlV8Di1JhCROg3MzTfe1RI8yitIm1eOs68iA==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.14.1.tgz", + "integrity": "sha512-IeYboCLG1KBNRksrZQIZ1SV5KVWZiR0ikFokOcA0n63vLvlaFvCsfxN+fd4aXwL0KW3YD+ByRzIgo9ZAHdbldw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7772,9 +7798,9 @@ } }, "node_modules/schema-utils": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.8.2.tgz", - "integrity": "sha512-iKlak3OFHF0dgD1nZRvgWQzG+P1TiEcMnLSBtIVBg937z5Jd6WE1c3SyZkXIyCfPHowL35yMsVvIjotm1c1qiw==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.8.3.tgz", + "integrity": "sha512-qIfo3f+lRNqnMPgpXSKvUhsT3pY5U+ppmgGr4j8LanFP3W/Ze+xj8DY/dPb5qVu3m6qFIMyMWKVRLNKxrNhxWg==", "dev": true, "license": "MIT", "peer": true, @@ -7795,7 +7821,7 @@ "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.112.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { "webpack": { @@ -7844,9 +7870,9 @@ "peer": true }, "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -8809,16 +8835,16 @@ } }, "node_modules/vite": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", - "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz", + "integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.30", - "rolldown": "~1.2.14", + "postcss": "^8.5.32", + "rolldown": "~1.2.16", "tinyglobby": "^0.2.17" }, "bin": { @@ -9138,17 +9164,6 @@ "dev": true, "license": "MIT" }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", diff --git a/package.json b/package.json index 188b0ee6..49bd6556 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solid-logic", - "version": "6.0.1-5", + "version": "6.0.1-6", "description": "Core business logic of SolidOS", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", @@ -67,6 +67,7 @@ "dependencies": { "@dokieli/web-access-control": "^1.1.0", "@uvdsl/solid-oidc-client-browser": "^0.2.3", + "mime-types": "^3.0.2", "solid-namespace": "^0.5.4" }, "solidosDependencies": [ diff --git a/src/index.ts b/src/index.ts index 1a25c758..df1e01fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,8 @@ export { createTypeIndexLogic } from './typeIndex/typeIndexLogic' export { createDirectoryLogic, DEFAULT_DIRECTORY_CATALOG_URL, DEFAULT_DIRECTORY_SOURCES } from './directory/directoryLogic' export type { AccessControlSubjectKind, AccessControlSubject } from './acl/accessControlSubjects' export type { ACLContext, AccessMode, AccessSubject, Authorization, PatchPlan } from './types' -export type { AppDetails, SolidNamespace, AuthenticationContext, SolidLogic, ResourceLogic, ResourceContent, ResourceMetadata, ChatLogic, DirectoryLogic, DirectoryEntry, DirectorySearchOptions, DirectoryRelationshipLabel, DirectorySource } from './types' +export { ResourceUploadError } from './types' +export type { AppDetails, SolidNamespace, AuthenticationContext, SolidLogic, ResourceLogic, ResourceContent, ResourceMetadata, ResourceUploadResult, ChatLogic, DirectoryLogic, DirectoryEntry, DirectorySearchOptions, DirectoryRelationshipLabel, DirectorySource } from './types' export { UnauthorizedError, CrossOriginForbiddenError, SameOriginForbiddenError, NotFoundError, FetchError, NotEditableError, WebOperationError } from './logic/CustomError' export { diff --git a/src/resource/resourceLogic.ts b/src/resource/resourceLogic.ts index c2e7bc0d..c29475f2 100644 --- a/src/resource/resourceLogic.ts +++ b/src/resource/resourceLogic.ts @@ -3,10 +3,89 @@ import { ACL_LINK } from '../acl/aclLogic' import { ns } from '../util/ns' import { assertSuccessfulHttpResponse, isMissingError } from './resourceHttp' import { readWacAccessInfo } from './resourceMetadata' -import { type ResourceAccess, type ResourceAccessWithDelete, type ResourceContent, type ResourceDeleteOptions, type ResourceLogic, type ResourceMetadata, type ResourceMetadataWithDelete, type TypeIndexLogic } from '../types' +import { ResourceUploadError, type ResourceAccess, type ResourceAccessWithDelete, type ResourceContent, type ResourceDeleteOptions, type ResourceLogic, type ResourceMetadata, type ResourceMetadataWithDelete, type ResourceUploadResult, type TypeIndexLogic } from '../types' import type { AclLogic } from '../types' +import * as mime from 'mime-types' export function createResourceLogic(store, aclLogic: AclLogic, containerLogic, typeIndexLogic: TypeIndexLogic): ResourceLogic { + function uploadFiles ( + files: File[], + fileBase: string, + imageBase: string + ): Promise { + const getUploadError = (destUri: string | undefined, error: unknown) => { + const uploadError = typeof error === 'object' && error !== null + ? error as { response?: { status?: unknown }, status?: unknown, message?: unknown } + : undefined + const responseStatus = uploadError?.response?.status + const directStatus = uploadError?.status + const status = typeof responseStatus === 'number' + ? responseStatus + : typeof directStatus === 'number' ? directStatus : undefined + const message = error instanceof Error + ? error.message + : typeof uploadError?.message === 'string' + ? uploadError.message + : typeof error === 'string' ? error : 'Upload failed' + return new ResourceUploadError(message, destUri, status) + } + + const upload = (file: File): Promise => new Promise(resolve => { + const reader = new FileReader() + reader.onload = event => { + try { + const data = event.target?.result + if (!(data instanceof ArrayBuffer)) { + resolve({ success: false, file, error: getUploadError(undefined, new Error('Unable to read file as an ArrayBuffer')) }) + return + } + + let suffix = '' + let contentType = file.type + if (!contentType) { + contentType = mime.lookup(file.name) || '' + if (!contentType) { + resolve({ success: false, file, error: getUploadError(undefined, new Error('Filename has no recognized content type')) }) + return + } + } else { + const extension = mime.extension(file.type) + if (extension && extension !== 'false' && !file.name.endsWith(`.${extension}`) && file.type !== mime.lookup(file.name)) { + suffix = `_.${extension}` + } + } + + const folderName = file.type.startsWith('image/') ? imageBase || fileBase : fileBase + const destUri = `${folderName}${folderName.endsWith('/') ? '' : '/'}${encodeURIComponent(file.name)}${suffix}` + + store.fetcher.webOperation('PUT', destUri, { data, contentType }).then( + () => resolve({ success: true, file, uploadedUri: destUri }), + error => resolve({ success: false, file, error: getUploadError(destUri, error) }) + ) + } catch (error) { + resolve({ success: false, file, error: getUploadError(undefined, error) }) + } + } + reader.onerror = () => { + resolve({ success: false, file, error: getUploadError(undefined, new Error('Unable to read file')) }) + } + reader.onabort = () => { + resolve({ success: false, file, error: getUploadError(undefined, new Error('File reading was aborted')) }) + } + try { + reader.readAsArrayBuffer(file) + } catch (error) { + resolve({ success: false, file, error: getUploadError(undefined, error) }) + } + }).catch(error => ({ + success: false as const, + file, + error: getUploadError(undefined, error) + })) + + return Promise.all(files.map(upload)) + } + function createContainer(url: string) { return containerLogic.createContainer(url) } @@ -282,6 +361,7 @@ export function createResourceLogic(store, aclLogic: AclLogic, containerLogic, t } return { + uploadFiles, recursiveDelete, deleteResourceAndTypeIndexIfExists, fetchMetadata, diff --git a/src/types.ts b/src/types.ts index 56c1cfe4..61c617a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -154,12 +154,32 @@ export type ResourceMetadataWithDelete = ResourceMetadata & { access: ResourceAccessWithDelete } +export class ResourceUploadError extends Error { + constructor( + message: string, + public readonly destinationUri?: string, + public readonly status?: number + ) { + super(message) + this.name = 'ResourceUploadError' + } +} + +export type ResourceUploadResult = + | { success: true, file: File, uploadedUri: string } + | { success: false, file: File, error: ResourceUploadError } + export type ResourceContent = { content: string metadata: ResourceMetadata } export interface ResourceLogic { + uploadFiles: ( + files: File[], + fileBase: string, + imageBase: string + ) => Promise, recursiveDelete: (resource: NamedNode, options?: ResourceDeleteOptions) => Promise, deleteResourceAndTypeIndexIfExists: (resource: NamedNode, user?: NamedNode | null) => Promise, fetchMetadata: (subject: NamedNode) => Promise, diff --git a/test/resourceLogic.test.ts b/test/resourceLogic.test.ts index 38d38e01..39df1b7b 100644 --- a/test/resourceLogic.test.ts +++ b/test/resourceLogic.test.ts @@ -6,8 +6,13 @@ import { type AclLogic, type TypeIndexLogic } from '../src/types' const makeStore = () => { const fetcher = { _fetch: vi.fn(), - unload: vi.fn() - } as unknown as Fetcher & { _fetch: ReturnType; unload: ReturnType } + unload: vi.fn(), + webOperation: vi.fn() + } as unknown as Fetcher & { + _fetch: ReturnType + unload: ReturnType + webOperation: ReturnType + } return { fetcher, @@ -80,6 +85,7 @@ describe('resourceLogic', () => { getScopedAppsFromIndex: vi.fn() } store.fetcher._fetch.mockResolvedValue(new Response('deleted', { status: 200 })) + store.fetcher.webOperation.mockResolvedValue(new Response(null, { status: 201 })) }) it('deletes container members before deleting the container itself', async () => { @@ -147,4 +153,77 @@ describe('resourceLogic', () => { expect(resourceLogic.isWebId(sym('https://alice.example.com/profile/card#me'))).toBe(false) expect(resourceLogic.isWebId(sym('https://alice.example.com/profile/card.jsonld#me'))).toBe(false) }) + + it('uploads image files to the image folder and adds the MIME extension when needed', async () => { + const resourceLogic = createResourceLogic(store, aclLogic, containerLogic, typeIndexLogic) + const file = new File(['image'], 'photo', { type: 'image/jpeg' }) + + const [result] = await resourceLogic.uploadFiles( + [file], + 'https://example.com/files/', + 'https://example.com/images/' + ) + + expect(result).toEqual({ + success: true, + file, + uploadedUri: 'https://example.com/images/photo_.jpg' + }) + expect(store.fetcher.webOperation).toHaveBeenCalledWith( + 'PUT', + 'https://example.com/images/photo_.jpg', + expect.objectContaining({ contentType: 'image/jpeg' }) + ) + }) + + it('returns structured per-file failures without preventing other uploads', async () => { + const resourceLogic = createResourceLogic(store, aclLogic, containerLogic, typeIndexLogic) + const quotaFile = new File(['large'], 'quota.txt', { type: 'text/plain' }) + const successfulFile = new File(['ok'], 'ok.txt', { type: 'text/plain' }) + const unknownErrorFile = new File(['bad'], 'unknown-error.txt', { type: 'text/plain' }) + const unknownTypeFile = new File(['data'], 'file.unknownextension123', { type: '' }) + store.fetcher.webOperation.mockImplementation((_method, uri) => { + if (uri.endsWith('/quota.txt')) { + return Promise.reject({ response: { status: 413 }, message: 'Payload too large' }) + } + if (uri.endsWith('/ok.txt')) { + return Promise.resolve(new Response(null, { status: 201 })) + } + if (uri.endsWith('/unknown-error.txt')) return Promise.reject(null) + return Promise.reject(null) + }) + + const results = await resourceLogic.uploadFiles( + [quotaFile, successfulFile, unknownErrorFile, unknownTypeFile], + 'https://example.com/files', + 'https://example.com/images' + ) + + expect(results[0]).toMatchObject({ + success: false, + file: quotaFile, + error: { + name: 'ResourceUploadError', + message: 'Payload too large', + destinationUri: 'https://example.com/files/quota.txt', + status: 413 + } + }) + expect(results[1]).toEqual({ + success: true, + file: successfulFile, + uploadedUri: 'https://example.com/files/ok.txt' + }) + expect(results[2]).toMatchObject({ + success: false, + file: unknownErrorFile, + error: { name: 'ResourceUploadError', message: 'Upload failed' } + }) + expect(results[3]).toMatchObject({ + success: false, + file: unknownTypeFile, + error: { name: 'ResourceUploadError', status: undefined } + }) + expect(store.fetcher.webOperation).toHaveBeenCalledTimes(3) + }) }) From 130612f55825af946c38f74a4fec797836b8234d Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 7 Nov 2026 09:09:19 +1100 Subject: [PATCH 2/3] Fix variable name for folderName assignment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/resource/resourceLogic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resource/resourceLogic.ts b/src/resource/resourceLogic.ts index c29475f2..cfcf2929 100644 --- a/src/resource/resourceLogic.ts +++ b/src/resource/resourceLogic.ts @@ -55,7 +55,7 @@ export function createResourceLogic(store, aclLogic: AclLogic, containerLogic, t } } - const folderName = file.type.startsWith('image/') ? imageBase || fileBase : fileBase + const folderName = contentType.startsWith('image/') ? imageBase || fileBase : fileBase const destUri = `${folderName}${folderName.endsWith('/') ? '' : '/'}${encodeURIComponent(file.name)}${suffix}` store.fetcher.webOperation('PUT', destUri, { data, contentType }).then( From 706e0348ac5f6c65807824759af73c78450f1472 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Sat, 7 Nov 2026 09:09:46 +1100 Subject: [PATCH 3/3] Improve error handling for file upload response Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/resource/resourceLogic.ts | 10 +++++----- src/types.ts | 3 +-- test/resourceLogic.test.ts | 6 ++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/resource/resourceLogic.ts b/src/resource/resourceLogic.ts index cfcf2929..9adfe7fc 100644 --- a/src/resource/resourceLogic.ts +++ b/src/resource/resourceLogic.ts @@ -10,8 +10,7 @@ import * as mime from 'mime-types' export function createResourceLogic(store, aclLogic: AclLogic, containerLogic, typeIndexLogic: TypeIndexLogic): ResourceLogic { function uploadFiles ( files: File[], - fileBase: string, - imageBase: string + destination: string ): Promise { const getUploadError = (destUri: string | undefined, error: unknown) => { const uploadError = typeof error === 'object' && error !== null @@ -55,11 +54,12 @@ export function createResourceLogic(store, aclLogic: AclLogic, containerLogic, t } } - const folderName = contentType.startsWith('image/') ? imageBase || fileBase : fileBase - const destUri = `${folderName}${folderName.endsWith('/') ? '' : '/'}${encodeURIComponent(file.name)}${suffix}` + const destUri = `${destination}${destination.endsWith('/') ? '' : '/'}${encodeURIComponent(file.name)}${suffix}` store.fetcher.webOperation('PUT', destUri, { data, contentType }).then( - () => resolve({ success: true, file, uploadedUri: destUri }), + response => resolve(response.ok + ? { success: true, file, uploadedUri: destUri } + : { success: false, file, error: new ResourceUploadError(`HTTP error on PUT! Status: ${response.status}`, destUri, response.status) }), error => resolve({ success: false, file, error: getUploadError(destUri, error) }) ) } catch (error) { diff --git a/src/types.ts b/src/types.ts index 61c617a4..dcd38a95 100644 --- a/src/types.ts +++ b/src/types.ts @@ -177,8 +177,7 @@ export type ResourceContent = { export interface ResourceLogic { uploadFiles: ( files: File[], - fileBase: string, - imageBase: string + destination: string ) => Promise, recursiveDelete: (resource: NamedNode, options?: ResourceDeleteOptions) => Promise, deleteResourceAndTypeIndexIfExists: (resource: NamedNode, user?: NamedNode | null) => Promise, diff --git a/test/resourceLogic.test.ts b/test/resourceLogic.test.ts index 39df1b7b..ba9fba82 100644 --- a/test/resourceLogic.test.ts +++ b/test/resourceLogic.test.ts @@ -154,13 +154,12 @@ describe('resourceLogic', () => { expect(resourceLogic.isWebId(sym('https://alice.example.com/profile/card.jsonld#me'))).toBe(false) }) - it('uploads image files to the image folder and adds the MIME extension when needed', async () => { + it('uploads image files to the destination and adds the MIME extension when needed', async () => { const resourceLogic = createResourceLogic(store, aclLogic, containerLogic, typeIndexLogic) const file = new File(['image'], 'photo', { type: 'image/jpeg' }) const [result] = await resourceLogic.uploadFiles( [file], - 'https://example.com/files/', 'https://example.com/images/' ) @@ -195,8 +194,7 @@ describe('resourceLogic', () => { const results = await resourceLogic.uploadFiles( [quotaFile, successfulFile, unknownErrorFile, unknownTypeFile], - 'https://example.com/files', - 'https://example.com/images' + 'https://example.com/files' ) expect(results[0]).toMatchObject({