@@ -57,22 +57,21 @@ const app = application({ | |||||
.resource(Piano) | .resource(Piano) | ||||
.resource(User); | .resource(User); | ||||
app.create({ | |||||
const backend = app.createBackend({ | |||||
dataSource, | dataSource, | ||||
}).then((backend) => { | |||||
const server = backend.createServer({ | |||||
basePath: '/api' | |||||
}); | |||||
server.listen(3000); | |||||
}); | |||||
setTimeout(() => { | |||||
// Allow user operations after 5 seconds from startup | |||||
User | |||||
.canFetchItem() | |||||
.canFetchCollection() | |||||
.canCreate() | |||||
.canPatch(); | |||||
}, 5000); | |||||
const server = backend.createServer({ | |||||
basePath: '/api' | |||||
}); | }); | ||||
server.listen(3000); | |||||
setTimeout(() => { | |||||
// Allow user operations after 5 seconds from startup | |||||
User | |||||
.canFetchItem() | |||||
.canFetchCollection() | |||||
.canCreate() | |||||
.canPatch(); | |||||
}, 5000); |
@@ -14,11 +14,11 @@ | |||||
], | ], | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/negotiator": "^0.6.3", | "@types/negotiator": "^0.6.3", | ||||
"@types/node": "^20.11.0", | |||||
"@types/node": "^20.11.30", | |||||
"pridepack": "2.6.0", | "pridepack": "2.6.0", | ||||
"tslib": "^2.6.2", | "tslib": "^2.6.2", | ||||
"typescript": "^5.3.3", | |||||
"vitest": "^1.2.0" | |||||
"typescript": "^5.4.3", | |||||
"vitest": "^1.4.0" | |||||
}, | }, | ||||
"scripts": { | "scripts": { | ||||
"prepublishOnly": "pridepack clean && pridepack build", | "prepublishOnly": "pridepack clean && pridepack build", | ||||
@@ -45,7 +45,6 @@ | |||||
"access": "public" | "access": "public" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"inflection": "^3.0.0", | |||||
"negotiator": "^0.6.3", | "negotiator": "^0.6.3", | ||||
"tsx": "^4.7.1", | "tsx": "^4.7.1", | ||||
"valibot": "^0.30.0" | "valibot": "^0.30.0" | ||||
@@ -5,9 +5,6 @@ settings: | |||||
excludeLinksFromLockfile: false | excludeLinksFromLockfile: false | ||||
dependencies: | dependencies: | ||||
inflection: | |||||
specifier: ^3.0.0 | |||||
version: 3.0.0 | |||||
negotiator: | negotiator: | ||||
specifier: ^0.6.3 | specifier: ^0.6.3 | ||||
version: 0.6.3 | version: 0.6.3 | ||||
@@ -23,20 +20,20 @@ devDependencies: | |||||
specifier: ^0.6.3 | specifier: ^0.6.3 | ||||
version: 0.6.3 | version: 0.6.3 | ||||
'@types/node': | '@types/node': | ||||
specifier: ^20.11.0 | |||||
version: 20.11.0 | |||||
specifier: ^20.11.30 | |||||
version: 20.11.30 | |||||
pridepack: | pridepack: | ||||
specifier: 2.6.0 | specifier: 2.6.0 | ||||
version: 2.6.0(tslib@2.6.2)(typescript@5.3.3) | |||||
version: 2.6.0(tslib@2.6.2)(typescript@5.4.3) | |||||
tslib: | tslib: | ||||
specifier: ^2.6.2 | specifier: ^2.6.2 | ||||
version: 2.6.2 | version: 2.6.2 | ||||
typescript: | typescript: | ||||
specifier: ^5.3.3 | |||||
version: 5.3.3 | |||||
specifier: ^5.4.3 | |||||
version: 5.4.3 | |||||
vitest: | vitest: | ||||
specifier: ^1.2.0 | |||||
version: 1.2.0(@types/node@20.11.0) | |||||
specifier: ^1.4.0 | |||||
version: 1.4.0(@types/node@20.11.30) | |||||
packages: | packages: | ||||
@@ -356,44 +353,44 @@ packages: | |||||
resolution: {integrity: sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==} | resolution: {integrity: sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==} | ||||
dev: true | dev: true | ||||
/@types/node@20.11.0: | |||||
resolution: {integrity: sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==} | |||||
/@types/node@20.11.30: | |||||
resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} | |||||
dependencies: | dependencies: | ||||
undici-types: 5.26.5 | undici-types: 5.26.5 | ||||
dev: true | dev: true | ||||
/@vitest/expect@1.2.0: | |||||
resolution: {integrity: sha512-H+2bHzhyvgp32o7Pgj2h9RTHN0pgYaoi26Oo3mE+dCi1PAqV31kIIVfTbqMO3Bvshd5mIrJLc73EwSRrbol9Lw==} | |||||
/@vitest/expect@1.4.0: | |||||
resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} | |||||
dependencies: | dependencies: | ||||
'@vitest/spy': 1.2.0 | |||||
'@vitest/utils': 1.2.0 | |||||
'@vitest/spy': 1.4.0 | |||||
'@vitest/utils': 1.4.0 | |||||
chai: 4.4.1 | chai: 4.4.1 | ||||
dev: true | dev: true | ||||
/@vitest/runner@1.2.0: | |||||
resolution: {integrity: sha512-vaJkDoQaNUTroT70OhM0NPznP7H3WyRwt4LvGwCVYs/llLaqhoSLnlIhUClZpbF5RgAee29KRcNz0FEhYcgxqA==} | |||||
/@vitest/runner@1.4.0: | |||||
resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} | |||||
dependencies: | dependencies: | ||||
'@vitest/utils': 1.2.0 | |||||
'@vitest/utils': 1.4.0 | |||||
p-limit: 5.0.0 | p-limit: 5.0.0 | ||||
pathe: 1.1.2 | pathe: 1.1.2 | ||||
dev: true | dev: true | ||||
/@vitest/snapshot@1.2.0: | |||||
resolution: {integrity: sha512-P33EE7TrVgB3HDLllrjK/GG6WSnmUtWohbwcQqmm7TAk9AVHpdgf7M3F3qRHKm6vhr7x3eGIln7VH052Smo6Kw==} | |||||
/@vitest/snapshot@1.4.0: | |||||
resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} | |||||
dependencies: | dependencies: | ||||
magic-string: 0.30.8 | magic-string: 0.30.8 | ||||
pathe: 1.1.2 | pathe: 1.1.2 | ||||
pretty-format: 29.7.0 | pretty-format: 29.7.0 | ||||
dev: true | dev: true | ||||
/@vitest/spy@1.2.0: | |||||
resolution: {integrity: sha512-MNxSAfxUaCeowqyyGwC293yZgk7cECZU9wGb8N1pYQ0yOn/SIr8t0l9XnGRdQZvNV/ZHBYu6GO/W3tj5K3VN1Q==} | |||||
/@vitest/spy@1.4.0: | |||||
resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} | |||||
dependencies: | dependencies: | ||||
tinyspy: 2.2.1 | tinyspy: 2.2.1 | ||||
dev: true | dev: true | ||||
/@vitest/utils@1.2.0: | |||||
resolution: {integrity: sha512-FyD5bpugsXlwVpTcGLDf3wSPYy8g541fQt14qtzo8mJ4LdEpDKZ9mQy2+qdJm2TZRpjY5JLXihXCgIxiRJgi5g==} | |||||
/@vitest/utils@1.4.0: | |||||
resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} | |||||
dependencies: | dependencies: | ||||
diff-sequences: 29.6.3 | diff-sequences: 29.6.3 | ||||
estree-walker: 3.0.3 | estree-walker: 3.0.3 | ||||
@@ -739,11 +736,6 @@ packages: | |||||
engines: {node: '>=0.8.19'} | engines: {node: '>=0.8.19'} | ||||
dev: true | dev: true | ||||
/inflection@3.0.0: | |||||
resolution: {integrity: sha512-1zEJU1l19SgJlmwqsEyFTbScw/tkMHFenUo//Y0i+XEP83gDFdMvPizAD/WGcE+l1ku12PcTVHQhO6g5E0UCMw==} | |||||
engines: {node: '>=18.0.0'} | |||||
dev: false | |||||
/inherits@2.0.4: | /inherits@2.0.4: | ||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} | ||||
dev: true | dev: true | ||||
@@ -785,6 +777,10 @@ packages: | |||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} | ||||
dev: true | dev: true | ||||
/js-tokens@8.0.3: | |||||
resolution: {integrity: sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==} | |||||
dev: true | |||||
/jsonc-parser@3.2.1: | /jsonc-parser@3.2.1: | ||||
resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} | resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} | ||||
dev: true | dev: true | ||||
@@ -1022,7 +1018,7 @@ packages: | |||||
react-is: 18.2.0 | react-is: 18.2.0 | ||||
dev: true | dev: true | ||||
/pridepack@2.6.0(tslib@2.6.2)(typescript@5.3.3): | |||||
/pridepack@2.6.0(tslib@2.6.2)(typescript@5.4.3): | |||||
resolution: {integrity: sha512-K81TouT+M3zwzPvDi70/CFVtzADvGpn071zAMm419ULb29gZni21pJ24njDFm3O+lJn0txBl4x1dsFBLWqS4iQ==} | resolution: {integrity: sha512-K81TouT+M3zwzPvDi70/CFVtzADvGpn071zAMm419ULb29gZni21pJ24njDFm3O+lJn0txBl4x1dsFBLWqS4iQ==} | ||||
engines: {node: '>=16'} | engines: {node: '>=16'} | ||||
hasBin: true | hasBin: true | ||||
@@ -1041,7 +1037,7 @@ packages: | |||||
pretty-bytes: 6.1.1 | pretty-bytes: 6.1.1 | ||||
prompts: 2.4.2 | prompts: 2.4.2 | ||||
tslib: 2.6.2 | tslib: 2.6.2 | ||||
typescript: 5.3.3 | |||||
typescript: 5.4.3 | |||||
yargs: 17.7.2 | yargs: 17.7.2 | ||||
dev: true | dev: true | ||||
@@ -1215,10 +1211,10 @@ packages: | |||||
engines: {node: '>=12'} | engines: {node: '>=12'} | ||||
dev: true | dev: true | ||||
/strip-literal@1.3.0: | |||||
resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} | |||||
/strip-literal@2.0.0: | |||||
resolution: {integrity: sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==} | |||||
dependencies: | dependencies: | ||||
acorn: 8.11.3 | |||||
js-tokens: 8.0.3 | |||||
dev: true | dev: true | ||||
/tinybench@2.6.0: | /tinybench@2.6.0: | ||||
@@ -1261,8 +1257,8 @@ packages: | |||||
is-typedarray: 1.0.0 | is-typedarray: 1.0.0 | ||||
dev: true | dev: true | ||||
/typescript@5.3.3: | |||||
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} | |||||
/typescript@5.4.3: | |||||
resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} | |||||
engines: {node: '>=14.17'} | engines: {node: '>=14.17'} | ||||
hasBin: true | hasBin: true | ||||
dev: true | dev: true | ||||
@@ -1290,8 +1286,8 @@ packages: | |||||
resolution: {integrity: sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA==} | resolution: {integrity: sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA==} | ||||
dev: false | dev: false | ||||
/vite-node@1.2.0(@types/node@20.11.0): | |||||
resolution: {integrity: sha512-ETnQTHeAbbOxl7/pyBck9oAPZZZo+kYnFt1uQDD+hPReOc+wCjXw4r4jHriBRuVDB5isHmPXxrfc1yJnfBERqg==} | |||||
/vite-node@1.4.0(@types/node@20.11.30): | |||||
resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} | |||||
engines: {node: ^18.0.0 || >=20.0.0} | engines: {node: ^18.0.0 || >=20.0.0} | ||||
hasBin: true | hasBin: true | ||||
dependencies: | dependencies: | ||||
@@ -1299,7 +1295,7 @@ packages: | |||||
debug: 4.3.4 | debug: 4.3.4 | ||||
pathe: 1.1.2 | pathe: 1.1.2 | ||||
picocolors: 1.0.0 | picocolors: 1.0.0 | ||||
vite: 5.1.6(@types/node@20.11.0) | |||||
vite: 5.1.6(@types/node@20.11.30) | |||||
transitivePeerDependencies: | transitivePeerDependencies: | ||||
- '@types/node' | - '@types/node' | ||||
- less | - less | ||||
@@ -1311,7 +1307,7 @@ packages: | |||||
- terser | - terser | ||||
dev: true | dev: true | ||||
/vite@5.1.6(@types/node@20.11.0): | |||||
/vite@5.1.6(@types/node@20.11.30): | |||||
resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==} | resolution: {integrity: sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==} | ||||
engines: {node: ^18.0.0 || >=20.0.0} | engines: {node: ^18.0.0 || >=20.0.0} | ||||
hasBin: true | hasBin: true | ||||
@@ -1339,7 +1335,7 @@ packages: | |||||
terser: | terser: | ||||
optional: true | optional: true | ||||
dependencies: | dependencies: | ||||
'@types/node': 20.11.0 | |||||
'@types/node': 20.11.30 | |||||
esbuild: 0.19.12 | esbuild: 0.19.12 | ||||
postcss: 8.4.35 | postcss: 8.4.35 | ||||
rollup: 4.12.1 | rollup: 4.12.1 | ||||
@@ -1347,15 +1343,15 @@ packages: | |||||
fsevents: 2.3.3 | fsevents: 2.3.3 | ||||
dev: true | dev: true | ||||
/vitest@1.2.0(@types/node@20.11.0): | |||||
resolution: {integrity: sha512-Ixs5m7BjqvLHXcibkzKRQUvD/XLw0E3rvqaCMlrm/0LMsA0309ZqYvTlPzkhh81VlEyVZXFlwWnkhb6/UMtcaQ==} | |||||
/vitest@1.4.0(@types/node@20.11.30): | |||||
resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} | |||||
engines: {node: ^18.0.0 || >=20.0.0} | engines: {node: ^18.0.0 || >=20.0.0} | ||||
hasBin: true | hasBin: true | ||||
peerDependencies: | peerDependencies: | ||||
'@edge-runtime/vm': '*' | '@edge-runtime/vm': '*' | ||||
'@types/node': ^18.0.0 || >=20.0.0 | '@types/node': ^18.0.0 || >=20.0.0 | ||||
'@vitest/browser': ^1.0.0 | |||||
'@vitest/ui': ^1.0.0 | |||||
'@vitest/browser': 1.4.0 | |||||
'@vitest/ui': 1.4.0 | |||||
happy-dom: '*' | happy-dom: '*' | ||||
jsdom: '*' | jsdom: '*' | ||||
peerDependenciesMeta: | peerDependenciesMeta: | ||||
@@ -1372,14 +1368,13 @@ packages: | |||||
jsdom: | jsdom: | ||||
optional: true | optional: true | ||||
dependencies: | dependencies: | ||||
'@types/node': 20.11.0 | |||||
'@vitest/expect': 1.2.0 | |||||
'@vitest/runner': 1.2.0 | |||||
'@vitest/snapshot': 1.2.0 | |||||
'@vitest/spy': 1.2.0 | |||||
'@vitest/utils': 1.2.0 | |||||
'@types/node': 20.11.30 | |||||
'@vitest/expect': 1.4.0 | |||||
'@vitest/runner': 1.4.0 | |||||
'@vitest/snapshot': 1.4.0 | |||||
'@vitest/spy': 1.4.0 | |||||
'@vitest/utils': 1.4.0 | |||||
acorn-walk: 8.3.2 | acorn-walk: 8.3.2 | ||||
cac: 6.7.14 | |||||
chai: 4.4.1 | chai: 4.4.1 | ||||
debug: 4.3.4 | debug: 4.3.4 | ||||
execa: 8.0.1 | execa: 8.0.1 | ||||
@@ -1388,11 +1383,11 @@ packages: | |||||
pathe: 1.1.2 | pathe: 1.1.2 | ||||
picocolors: 1.0.0 | picocolors: 1.0.0 | ||||
std-env: 3.7.0 | std-env: 3.7.0 | ||||
strip-literal: 1.3.0 | |||||
strip-literal: 2.0.0 | |||||
tinybench: 2.6.0 | tinybench: 2.6.0 | ||||
tinypool: 0.8.2 | tinypool: 0.8.2 | ||||
vite: 5.1.6(@types/node@20.11.0) | |||||
vite-node: 1.2.0(@types/node@20.11.0) | |||||
vite: 5.1.6(@types/node@20.11.30) | |||||
vite-node: 1.4.0(@types/node@20.11.30) | |||||
why-is-node-running: 2.2.2 | why-is-node-running: 2.2.2 | ||||
transitivePeerDependencies: | transitivePeerDependencies: | ||||
- less | - less | ||||
@@ -9,11 +9,6 @@ export interface BackendState { | |||||
charset: Charset; | charset: Charset; | ||||
mediaType: MediaType; | mediaType: MediaType; | ||||
} | } | ||||
errorHeaders: { | |||||
language?: string; | |||||
charset?: string; | |||||
serializer?: string; | |||||
} | |||||
showTotalItemCountOnGetCollection: boolean; | showTotalItemCountOnGetCollection: boolean; | ||||
throws404OnDeletingNotFound: boolean; | throws404OnDeletingNotFound: boolean; | ||||
checksSerializersOnDelete: boolean; | checksSerializersOnDelete: boolean; | ||||
@@ -1,5 +1,5 @@ | |||||
import * as v from 'valibot'; | import * as v from 'valibot'; | ||||
import {ApplicationState, Language, LanguageStatusMessageMap, Resource} from '../common'; | |||||
import {ApplicationState, Resource} from '../common'; | |||||
import http from 'http'; | import http from 'http'; | ||||
import {createServer, CreateServerParams} from './server'; | import {createServer, CreateServerParams} from './server'; | ||||
import https from 'https'; | import https from 'https'; | ||||
@@ -22,8 +22,6 @@ export interface BackendResource< | |||||
dataSource: DataSourceType; | dataSource: DataSourceType; | ||||
} | } | ||||
export interface RequestContext extends http.IncomingMessage {} | |||||
export interface BackendBuilder<T extends BaseDataSource = BaseDataSource> { | export interface BackendBuilder<T extends BaseDataSource = BaseDataSource> { | ||||
showTotalItemCountOnGetCollection(b?: boolean): this; | showTotalItemCountOnGetCollection(b?: boolean): this; | ||||
showTotalItemCountOnCreateItem(b?: boolean): this; | showTotalItemCountOnCreateItem(b?: boolean): this; | ||||
@@ -33,84 +31,6 @@ export interface BackendBuilder<T extends BaseDataSource = BaseDataSource> { | |||||
dataSource?: (resource: Resource) => T; | dataSource?: (resource: Resource) => T; | ||||
} | } | ||||
export class MiddlewareError extends Error {} | |||||
interface ResponseParams { | |||||
statusCode: Response['statusCode']; | |||||
statusMessage?: Response['statusMessage']; | |||||
headers?: Response['headers']; | |||||
} | |||||
interface PlainResponseParams<T = unknown> extends ResponseParams { | |||||
body?: T; | |||||
} | |||||
interface StreamResponseParams extends ResponseParams { | |||||
stream: NodeJS.ReadableStream; | |||||
} | |||||
interface HttpMiddlewareErrorParams<T = unknown> extends Omit<PlainResponseParams<T>, 'statusMessage'> { | |||||
cause?: unknown | |||||
} | |||||
export interface Response { | |||||
statusCode: number; | |||||
statusMessage?: keyof LanguageStatusMessageMap; | |||||
headers?: Record<string, string>; | |||||
} | |||||
export class PlainResponse<T = unknown> implements Response { | |||||
readonly statusCode: Response['statusCode']; | |||||
readonly statusMessage?: keyof LanguageStatusMessageMap; | |||||
readonly headers: Response['headers']; | |||||
readonly body?: T; | |||||
constructor(args: PlainResponseParams<T>) { | |||||
this.statusCode = args.statusCode; | |||||
this.statusMessage = args.statusMessage; | |||||
this.headers = args.headers; | |||||
this.body = args.body; | |||||
} | |||||
} | |||||
export class StreamResponse implements Response { | |||||
readonly statusCode: Response['statusCode']; | |||||
readonly statusMessage?: keyof LanguageStatusMessageMap; | |||||
readonly headers: Response['headers']; | |||||
readonly stream: NodeJS.ReadableStream; | |||||
constructor(args: StreamResponseParams) { | |||||
this.statusCode = args.statusCode; | |||||
this.statusMessage = args.statusMessage; | |||||
this.headers = args.headers; | |||||
this.stream = args.stream; | |||||
} | |||||
} | |||||
export class HttpMiddlewareError extends MiddlewareError { | |||||
readonly response: PlainResponse; | |||||
constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams) { | |||||
super(statusMessage, { cause: params.cause }); | |||||
this.response = new PlainResponse({ | |||||
...params, | |||||
statusMessage, | |||||
}); | |||||
} | |||||
} | |||||
export interface ResponseContext<T extends http.IncomingMessage> extends http.ServerResponse<T> {} | |||||
export interface CreateBackendParams { | export interface CreateBackendParams { | ||||
app: ApplicationState; | app: ApplicationState; | ||||
dataSource: (resource: Resource) => BaseDataSource; | dataSource: (resource: Resource) => BaseDataSource; | ||||
@@ -125,13 +45,6 @@ export const createBackend = (params: CreateBackendParams) => { | |||||
charset: utf8, | charset: utf8, | ||||
mediaType: applicationJson | mediaType: applicationJson | ||||
}, | }, | ||||
errorHeaders: { | |||||
// undefined follows user accept headers strictly | |||||
// | |||||
language: undefined, | |||||
charset: undefined, | |||||
serializer: undefined, | |||||
}, | |||||
showTotalItemCountOnGetCollection: false, | showTotalItemCountOnGetCollection: false, | ||||
showTotalItemCountOnCreateItem: false, | showTotalItemCountOnCreateItem: false, | ||||
throws404OnDeletingNotFound: false, | throws404OnDeletingNotFound: false, | ||||
@@ -1,6 +1,6 @@ | |||||
import {constants} from 'http2'; | import {constants} from 'http2'; | ||||
import http from 'http'; | import http from 'http'; | ||||
import {HttpMiddlewareError} from '../index'; | |||||
import {HttpMiddlewareError} from '../server'; | |||||
interface RequestContext extends http.IncomingMessage { | interface RequestContext extends http.IncomingMessage { | ||||
method: string; | method: string; | ||||
@@ -1,6 +1,6 @@ | |||||
import {constants} from 'http2'; | import {constants} from 'http2'; | ||||
import http from 'http'; | import http from 'http'; | ||||
import {HttpMiddlewareError} from '..'; | |||||
import {HttpMiddlewareError} from '../server'; | |||||
interface RequestContext extends http.IncomingMessage { | interface RequestContext extends http.IncomingMessage { | ||||
basePath?: string; | basePath?: string; | ||||
@@ -1,7 +1,6 @@ | |||||
import { constants } from 'http2'; | import { constants } from 'http2'; | ||||
import * as v from 'valibot'; | import * as v from 'valibot'; | ||||
import {HttpMiddlewareError, PlainResponse} from './core'; | |||||
import {Middleware} from './server'; | |||||
import {HttpMiddlewareError, PlainResponse, Middleware} from './server'; | |||||
export const handleGetRoot: Middleware = (req) => { | export const handleGetRoot: Middleware = (req) => { | ||||
const { backend, basePath } = req; | const { backend, basePath } = req; | ||||
@@ -1,6 +1,6 @@ | |||||
import http from 'http'; | import http from 'http'; | ||||
import {BackendState} from './common'; | import {BackendState} from './common'; | ||||
import {Language, Resource, Charset, MediaType} from '../common'; | |||||
import {Language, Resource, Charset, MediaType, LanguageStatusMessageMap} from '../common'; | |||||
import * as applicationJson from '../common/media-types/application/json'; | import * as applicationJson from '../common/media-types/application/json'; | ||||
import * as utf8 from '../common/charsets/utf-8'; | import * as utf8 from '../common/charsets/utf-8'; | ||||
import * as en from '../common/languages/en'; | import * as en from '../common/languages/en'; | ||||
@@ -19,17 +19,65 @@ import { | |||||
handlePatchItem, | handlePatchItem, | ||||
} from './handlers'; | } from './handlers'; | ||||
import { | import { | ||||
HttpMiddlewareError, | |||||
PlainResponse, | |||||
ResponseContext, | |||||
StreamResponse, | |||||
Response, | |||||
BackendResource, | BackendResource, | ||||
} from './core'; | } from './core'; | ||||
import * as v from 'valibot'; | import * as v from 'valibot'; | ||||
import {getBody} from './utils'; | import {getBody} from './utils'; | ||||
import {DataSource} from './data-source'; | import {DataSource} from './data-source'; | ||||
export interface Response { | |||||
statusCode: number; | |||||
statusMessage?: keyof LanguageStatusMessageMap; | |||||
headers?: Record<string, string>; | |||||
} | |||||
interface ResponseParams { | |||||
statusCode: Response['statusCode']; | |||||
statusMessage?: Response['statusMessage']; | |||||
headers?: Response['headers']; | |||||
} | |||||
export class MiddlewareError extends Error {} | |||||
interface PlainResponseParams<T = unknown> extends ResponseParams { | |||||
body?: T; | |||||
} | |||||
interface HttpMiddlewareErrorParams<T = unknown> extends Omit<PlainResponseParams<T>, 'statusMessage'> { | |||||
cause?: unknown | |||||
} | |||||
export class PlainResponse<T = unknown> implements Response { | |||||
readonly statusCode: Response['statusCode']; | |||||
readonly statusMessage?: keyof LanguageStatusMessageMap; | |||||
readonly headers: Response['headers']; | |||||
readonly body?: T; | |||||
constructor(args: PlainResponseParams<T>) { | |||||
this.statusCode = args.statusCode; | |||||
this.statusMessage = args.statusMessage; | |||||
this.headers = args.headers; | |||||
this.body = args.body; | |||||
} | |||||
} | |||||
export class HttpMiddlewareError extends MiddlewareError { | |||||
readonly response: PlainResponse; | |||||
constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams) { | |||||
super(statusMessage, { cause: params.cause }); | |||||
this.response = new PlainResponse({ | |||||
...params, | |||||
statusMessage, | |||||
}); | |||||
} | |||||
} | |||||
export interface CreateServerParams { | export interface CreateServerParams { | ||||
basePath?: string; | basePath?: string; | ||||
host?: string; | host?: string; | ||||
@@ -74,6 +122,39 @@ export interface Middleware<Req extends RequestContext = RequestContext> { | |||||
(req: Req): undefined | Response | Promise<undefined | Response>; | (req: Req): undefined | Response | Promise<undefined | Response>; | ||||
} | } | ||||
class ServerYasumiRequest extends http.IncomingMessage implements RequestContext { | |||||
host = 'localhost'; | |||||
scheme = 'http'; | |||||
basePath = ''; | |||||
backend = {} as BackendState; | |||||
resource = undefined as unknown as BackendResource; | |||||
resourceId?: string; | |||||
query = new URLSearchParams(); | |||||
body?: unknown; | |||||
method = ''; | |||||
url = ''; | |||||
rawUrl = ''; | |||||
readonly cn: { | |||||
language: Language; | |||||
mediaType: MediaType; | |||||
charset: Charset; | |||||
} = { | |||||
language: en, | |||||
mediaType: applicationJson, | |||||
charset: utf8, | |||||
}; | |||||
} | |||||
const getAllowedMiddlewares = <T extends v.BaseSchema>(resource: Resource<T>, mainResourceId: string) => { | const getAllowedMiddlewares = <T extends v.BaseSchema>(resource: Resource<T>, mainResourceId: string) => { | ||||
const middlewares = [] as [string, Middleware, v.BaseSchema?][]; | const middlewares = [] as [string, Middleware, v.BaseSchema?][]; | ||||
if (mainResourceId === '') { | if (mainResourceId === '') { | ||||
@@ -127,46 +208,75 @@ const getAllowedMiddlewares = <T extends v.BaseSchema>(resource: Resource<T>, ma | |||||
return middlewares; | return middlewares; | ||||
}; | }; | ||||
export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { | |||||
const isHttps = 'key' in serverParams && 'cert' in serverParams; | |||||
class ServerYasumiRequest extends http.IncomingMessage implements RequestContext { | |||||
readonly host = serverParams.host ?? 'localhost'; | |||||
readonly scheme = isHttps ? 'https' : 'http'; | |||||
readonly basePath = serverParams.basePath ?? ''; | |||||
readonly backend = backendState; | |||||
resource = undefined as unknown as BackendResource; | |||||
resourceId?: string; | |||||
query = new URLSearchParams(); | |||||
body?: unknown; | |||||
method = ''; | |||||
url = ''; | |||||
const adjustRequestForContentNegotiation = (req: RequestContext, res: http.ServerResponse<RequestContext>) => { | |||||
const negotiator = new Negotiator(req); | |||||
const availableLanguages = Array.from(req.backend.app.languages); | |||||
const availableCharsets = Array.from(req.backend.app.charsets); | |||||
const availableMediaTypes = Array.from(req.backend.app.mediaTypes); | |||||
const languageCandidate = negotiator.language(availableLanguages.map((l) => l.name)) ?? req.backend.cn.language.name; | |||||
const charsetCandidate = negotiator.charset(availableCharsets.map((l) => l.name)) ?? req.backend.cn.charset.name; | |||||
const mediaTypeCandidate = negotiator.mediaType(availableMediaTypes.map((l) => l.name)) ?? req.backend.cn.mediaType.name; | |||||
// TODO refactor | |||||
const currentLanguage = availableLanguages.find((l) => l.name === languageCandidate); | |||||
if (typeof currentLanguage === 'undefined') { | |||||
const data = req.backend?.cn.language.bodies.languageNotAcceptable(); | |||||
const responseRaw = req.backend?.cn.mediaType.serialize(data); | |||||
const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; | |||||
res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { | |||||
'Content-Language': req.backend?.cn.language.name, | |||||
'Content-Type': [ | |||||
req.backend?.cn.mediaType.name, | |||||
`charset="${req.backend?.cn.charset.name}"` | |||||
].join('; '), | |||||
}); | |||||
res.statusMessage = req.backend?.cn.language.statusMessages.languageNotAcceptable() ?? ''; | |||||
res.end(response); | |||||
return; | |||||
} | |||||
rawUrl = ''; | |||||
const currentMediaType = availableMediaTypes.find((l) => l.name === mediaTypeCandidate); | |||||
if (typeof currentMediaType === 'undefined') { | |||||
const data = req.backend?.cn.language.bodies.mediaTypeNotAcceptable(); | |||||
const responseRaw = req.backend?.cn.mediaType.serialize(data); | |||||
const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; | |||||
res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { | |||||
'Content-Language': req.backend?.cn.language.name, | |||||
'Content-Type': [ | |||||
req.backend?.cn.mediaType.name, | |||||
`charset="${req.backend?.cn.charset.name}"` | |||||
].join('; '), | |||||
}); | |||||
res.statusMessage = req.backend?.cn.language.statusMessages.mediaTypeNotAcceptable() ?? ''; | |||||
res.end(response); | |||||
return; | |||||
} | |||||
readonly cn: { | |||||
language: Language; | |||||
mediaType: MediaType; | |||||
charset: Charset; | |||||
} = { | |||||
language: en, | |||||
mediaType: applicationJson, | |||||
charset: utf8, | |||||
}; | |||||
const responseBodyCharset = availableCharsets.find((l) => l.name === charsetCandidate); | |||||
if (typeof responseBodyCharset === 'undefined') { | |||||
const data = req.backend?.cn.language.bodies.encodingNotAcceptable(); | |||||
const responseRaw = req.backend?.cn.mediaType.serialize(data); | |||||
const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; | |||||
res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { | |||||
'Content-Language': req.backend?.cn.language.name, | |||||
'Content-Type': [ | |||||
req.backend?.cn.mediaType.name, | |||||
`charset="${req.backend?.cn.charset.name}"` | |||||
].join('; '), | |||||
}); | |||||
res.statusMessage = req.backend?.cn.language.statusMessages.encodingNotAcceptable() ?? ''; | |||||
res.end(response); | |||||
return; | |||||
} | } | ||||
class ServerYasumiResponse<T extends http.IncomingMessage> extends http.ServerResponse<T> { | |||||
req.cn.language = currentLanguage; | |||||
req.cn.mediaType = currentMediaType; | |||||
req.cn.charset = responseBodyCharset; | |||||
}; | |||||
} | |||||
export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { | |||||
const isHttps = 'key' in serverParams && 'cert' in serverParams; | |||||
const server = isHttps | const server = isHttps | ||||
? https.createServer({ | ? https.createServer({ | ||||
@@ -174,83 +284,17 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
cert: serverParams.cert, | cert: serverParams.cert, | ||||
requestTimeout: serverParams.requestTimeout, | requestTimeout: serverParams.requestTimeout, | ||||
IncomingMessage: ServerYasumiRequest, | IncomingMessage: ServerYasumiRequest, | ||||
ServerResponse: ServerYasumiResponse, | |||||
}) | }) | ||||
: http.createServer({ | : http.createServer({ | ||||
requestTimeout: serverParams.requestTimeout, | requestTimeout: serverParams.requestTimeout, | ||||
IncomingMessage: ServerYasumiRequest, | IncomingMessage: ServerYasumiRequest, | ||||
ServerResponse: ServerYasumiResponse, | |||||
}); | }); | ||||
const adjustRequestForContentNegotiation = (req: RequestContext, res: ResponseContext<RequestContext>) => { | |||||
const negotiator = new Negotiator(req); | |||||
const availableLanguages = Array.from(req.backend.app.languages); | |||||
const availableCharsets = Array.from(req.backend.app.charsets); | |||||
const availableMediaTypes = Array.from(req.backend.app.mediaTypes); | |||||
const languageCandidate = negotiator.language(availableLanguages.map((l) => l.name)) ?? backendState.cn.language.name; | |||||
const charsetCandidate = negotiator.charset(availableCharsets.map((l) => l.name)) ?? backendState.cn.charset.name; | |||||
const mediaTypeCandidate = negotiator.mediaType(availableMediaTypes.map((l) => l.name)) ?? backendState.cn.mediaType.name; | |||||
// TODO refactor | |||||
const currentLanguage = availableLanguages.find((l) => l.name === languageCandidate); | |||||
if (typeof currentLanguage === 'undefined') { | |||||
const data = req.backend?.cn.language.bodies.languageNotAcceptable(); | |||||
const responseRaw = req.backend?.cn.mediaType.serialize(data); | |||||
const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; | |||||
res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { | |||||
'Content-Language': req.backend?.cn.language.name, | |||||
'Content-Type': [ | |||||
req.backend?.cn.mediaType.name, | |||||
`charset="${req.backend?.cn.charset.name}"` | |||||
].join('; '), | |||||
}); | |||||
res.statusMessage = req.backend?.cn.language.statusMessages.languageNotAcceptable() ?? ''; | |||||
res.end(response); | |||||
return; | |||||
} | |||||
const currentMediaType = availableMediaTypes.find((l) => l.name === mediaTypeCandidate); | |||||
if (typeof currentMediaType === 'undefined') { | |||||
const data = req.backend?.cn.language.bodies.mediaTypeNotAcceptable(); | |||||
const responseRaw = req.backend?.cn.mediaType.serialize(data); | |||||
const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; | |||||
res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { | |||||
'Content-Language': req.backend?.cn.language.name, | |||||
'Content-Type': [ | |||||
req.backend?.cn.mediaType.name, | |||||
`charset="${req.backend?.cn.charset.name}"` | |||||
].join('; '), | |||||
}); | |||||
res.statusMessage = req.backend?.cn.language.statusMessages.mediaTypeNotAcceptable() ?? ''; | |||||
res.end(response); | |||||
return; | |||||
} | |||||
const responseBodyCharset = availableCharsets.find((l) => l.name === charsetCandidate); | |||||
if (typeof responseBodyCharset === 'undefined') { | |||||
const data = req.backend?.cn.language.bodies.encodingNotAcceptable(); | |||||
const responseRaw = req.backend?.cn.mediaType.serialize(data); | |||||
const response = typeof responseRaw !== 'undefined' ? req.backend?.cn.charset.encode(responseRaw) : undefined; | |||||
res.writeHead(constants.HTTP_STATUS_NOT_ACCEPTABLE, { | |||||
'Content-Language': req.backend?.cn.language.name, | |||||
'Content-Type': [ | |||||
req.backend?.cn.mediaType.name, | |||||
`charset="${req.backend?.cn.charset.name}"` | |||||
].join('; '), | |||||
}); | |||||
res.statusMessage = req.backend?.cn.language.statusMessages.encodingNotAcceptable() ?? ''; | |||||
res.end(response); | |||||
return; | |||||
} | |||||
req.cn.language = currentLanguage; | |||||
req.cn.mediaType = currentMediaType; | |||||
req.cn.charset = responseBodyCharset; | |||||
}; | |||||
server.on('request', async (req: RequestContext, res) => { | server.on('request', async (req: RequestContext, res) => { | ||||
req.backend = backendState; | |||||
req.basePath = serverParams.basePath ?? ''; | |||||
req.host = serverParams.host ?? 'localhost'; | |||||
req.scheme = isHttps ? 'https' : 'http'; | |||||
adjustRequestForContentNegotiation(req, res); | adjustRequestForContentNegotiation(req, res); | ||||
try { | try { | ||||
@@ -426,12 +470,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
'Content-Language': req.cn.language.name | 'Content-Language': req.cn.language.name | ||||
}; | }; | ||||
if (middlewareState instanceof StreamResponse) { | |||||
res.writeHead(constants.HTTP_STATUS_ACCEPTED, headers); | |||||
middlewareState.stream.pipe(res); | |||||
middlewareState.stream.on('end', () => { | |||||
res.end(); | |||||
}); | |||||
if (middlewareState instanceof http.ServerResponse) { | |||||
// TODO streaming responses | |||||
middlewareState.writeHead(constants.HTTP_STATUS_ACCEPTED, headers); | |||||
return; | return; | ||||
} | } | ||||
@@ -3,26 +3,26 @@ import {MediaType, Charset} from '../common'; | |||||
import {BaseSchema, parseAsync} from 'valibot'; | import {BaseSchema, parseAsync} from 'valibot'; | ||||
export const getBody = ( | export const getBody = ( | ||||
req: IncomingMessage, | |||||
schema: BaseSchema, | |||||
encodingPair?: Charset, | |||||
deserializer?: MediaType, | |||||
req: IncomingMessage, | |||||
schema: BaseSchema, | |||||
encodingPair?: Charset, | |||||
deserializer?: MediaType, | |||||
) => new Promise((resolve, reject) => { | ) => new Promise((resolve, reject) => { | ||||
let body = Buffer.from(''); | |||||
req.on('data', (chunk) => { | |||||
body = Buffer.concat([body, chunk]); | |||||
}); | |||||
req.on('end', async () => { | |||||
const bodyStr = encodingPair?.decode(body) ?? body.toString(); | |||||
try { | |||||
const bodyDeserialized = await parseAsync( | |||||
schema, | |||||
deserializer?.deserialize(bodyStr) ?? body, | |||||
{abortEarly: false}, | |||||
); | |||||
resolve(bodyDeserialized); | |||||
} catch (err) { | |||||
reject(err); | |||||
} | |||||
}); | |||||
let body = Buffer.from(''); | |||||
req.on('data', (chunk) => { | |||||
body = Buffer.concat([body, chunk]); | |||||
}); | |||||
req.on('end', async () => { | |||||
const bodyStr = encodingPair?.decode(body) ?? body.toString(); | |||||
try { | |||||
const bodyDeserialized = await parseAsync( | |||||
schema, | |||||
deserializer?.deserialize(bodyStr) ?? body, | |||||
{abortEarly: false}, | |||||
); | |||||
resolve(bodyDeserialized); | |||||
} catch (err) { | |||||
reject(err); | |||||
} | |||||
}); | |||||
}); | }); |