From 04b7493fb7aca8ca603d282bbbcb7f6428dca802 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Fri, 15 Dec 2023 10:37:57 -0800 Subject: [PATCH 01/19] Make failed expectations more clear in conformanceApiTests.ts. --- conformance/ts/test/conformanceApiTests.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/conformance/ts/test/conformanceApiTests.ts b/conformance/ts/test/conformanceApiTests.ts index 07fdef2..940e53f 100644 --- a/conformance/ts/test/conformanceApiTests.ts +++ b/conformance/ts/test/conformanceApiTests.ts @@ -21,8 +21,13 @@ describe('tests', () => { it(data.test, () => { return ((httpClient as any)[data.method](data.request)) .then((result: any) => { - expect(result.value).to.deep.equal(data.response); - expect(result.error).to.deep.equal(data.error); + expect({ + error: result.error ?? undefined, + value: result.value ?? undefined, + }).to.be.deep.equal({ + error: data.error ?? undefined, + value: data.response ?? undefined, + }); }); }); }); From b6f838e5672a6fcb20c1fe6798445981f0ca6085 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Fri, 15 Dec 2023 12:53:49 -0800 Subject: [PATCH 02/19] Clean up conformance test runner. Preparing to add fastify server impl. --- conformance/.gitignore | 2 + conformance/{ts => }/package-lock.json | 996 ++------------------- conformance/{ts => }/package.json | 4 +- conformance/ts/.gitignore | 4 - conformance/ts/test/conformanceApiTests.ts | 10 +- conformance/{ts => }/tsconfig.json | 10 +- 6 files changed, 91 insertions(+), 935 deletions(-) create mode 100644 conformance/.gitignore rename conformance/{ts => }/package-lock.json (51%) rename conformance/{ts => }/package.json (83%) delete mode 100644 conformance/ts/.gitignore rename conformance/{ts => }/tsconfig.json (58%) diff --git a/conformance/.gitignore b/conformance/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/conformance/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/conformance/ts/package-lock.json b/conformance/package-lock.json similarity index 51% rename from conformance/ts/package-lock.json rename to conformance/package-lock.json index da656ef..3f4ad86 100644 --- a/conformance/ts/package-lock.json +++ b/conformance/package-lock.json @@ -1,7 +1,7 @@ { "name": "conformance-tester", "version": "0.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "UNLICENSED", "dependencies": { - "facility-core": "file:../../ts" + "facility-core": "file:../ts" }, "devDependencies": { "@types/chai": "^4.2.18", @@ -23,6 +23,9 @@ } }, "../../ts": { + "extraneous": true + }, + "../ts": { "name": "facility-core", "version": "2.2.0", "license": "MIT", @@ -41,9 +44,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", - "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "dev": true }, "node_modules/@types/mocha": { @@ -59,21 +62,15 @@ "dev": true }, "node_modules/@types/node-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", - "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", "dev": true, "dependencies": { "@types/node": "*", - "form-data": "^3.0.0" + "form-data": "^4.0.0" } }, - "node_modules/@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -108,9 +105,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -196,18 +193,18 @@ } }, "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -242,10 +239,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -324,6 +324,29 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -337,15 +360,15 @@ } }, "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, "dependencies": { "type-detect": "^4.0.0" }, "engines": { - "node": ">=0.12" + "node": ">=6" } }, "node_modules/delayed-stream": { @@ -394,7 +417,7 @@ } }, "node_modules/facility-core": { - "resolved": "../../ts", + "resolved": "../ts", "link": true }, "node_modules/fill-range": { @@ -435,9 +458,9 @@ } }, "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dev": true, "dependencies": { "asynckit": "^0.4.0", @@ -455,9 +478,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -478,9 +501,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -690,12 +713,12 @@ } }, "node_modules/loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "node_modules/mime-db": { @@ -732,12 +755,11 @@ } }, "node_modules/mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, "dependencies": { - "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", @@ -772,30 +794,7 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mocha/node_modules/ms": { + "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", @@ -814,9 +813,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dev": true, "dependencies": { "whatwg-url": "^5.0.0" @@ -1180,846 +1179,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@types/chai": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.3.tgz", - "integrity": "sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==", - "dev": true - }, - "@types/mocha": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.3.tgz", - "integrity": "sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==", - "dev": true - }, - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, - "@types/node-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", - "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", - "dev": true, - "requires": { - "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "@ungap/promise-all-settled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", - "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", - "dev": true - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true - }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "facility-core": { - "version": "file:../../ts", - "requires": { - "@types/chai": "^4.2.18", - "@types/mocha": "^8.2.2", - "@typescript-eslint/eslint-plugin": "^4.26.1", - "@typescript-eslint/parser": "^4.26.1", - "chai": "^4.3.4", - "eslint": "^7.28.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-prettier": "^3.4.0", - "mocha": "^10.0.0", - "prettier": "^2.3.1", - "typescript": "~3.9.9" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "dev": true, - "requires": { - "get-func-name": "^2.0.0" - } - }, - "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 - }, - "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, - "requires": { - "mime-db": "1.52.0" - } - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "mocha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", - "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", - "dev": true, - "requires": { - "@ungap/promise-all-settled": "1.1.2", - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", - "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - } - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/conformance/ts/package.json b/conformance/package.json similarity index 83% rename from conformance/ts/package.json rename to conformance/package.json index d011dbc..9f3930d 100644 --- a/conformance/ts/package.json +++ b/conformance/package.json @@ -4,7 +4,7 @@ "description": "Tests the ConformanceApi client.", "scripts": { "build": "tsc", - "test": "npm run --silent build && mocha" + "test": "npm run --silent build && mocha dist/ts/test/conformanceApiTests.js" }, "repository": { "type": "git", @@ -23,6 +23,6 @@ "typescript": "~3.9.9" }, "dependencies": { - "facility-core": "file:../../ts" + "facility-core": "file:../ts" } } diff --git a/conformance/ts/.gitignore b/conformance/ts/.gitignore deleted file mode 100644 index b196ce0..0000000 --- a/conformance/ts/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -*.d.ts -*.js -*.js.map diff --git a/conformance/ts/test/conformanceApiTests.ts b/conformance/ts/test/conformanceApiTests.ts index 940e53f..95b0f8d 100644 --- a/conformance/ts/test/conformanceApiTests.ts +++ b/conformance/ts/test/conformanceApiTests.ts @@ -1,8 +1,7 @@ import { createHttpClient } from '../src/conformanceApi'; import { expect, should } from 'chai'; import fetch from 'node-fetch'; -import * as fs from 'fs'; -import * as path from 'path'; +import conformanceTestsJson from '../../ConformanceTests.json'; should(); @@ -12,13 +11,10 @@ const httpClient = createHttpClient({ } }); -const testData = JSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../conformanceTests.json'), 'utf8')); - describe('tests', () => { - testData.tests.forEach((data: any) => { - it(data.test, () => { + conformanceTestsJson.tests.forEach((data: any) => { + it(data.test, async () => { return ((httpClient as any)[data.method](data.request)) .then((result: any) => { expect({ diff --git a/conformance/ts/tsconfig.json b/conformance/tsconfig.json similarity index 58% rename from conformance/ts/tsconfig.json rename to conformance/tsconfig.json index 57266dd..b37f54f 100644 --- a/conformance/ts/tsconfig.json +++ b/conformance/tsconfig.json @@ -1,15 +1,19 @@ { "compilerOptions": { + "outDir": "dist", "module": "commonjs", "target": "ES2019", "strict": true, "newLine": "LF", "declaration": true, "sourceMap": true, - "lib": [ "ES2019" ] + "lib": [ + "ES2019" + ], + "resolveJsonModule": true, + "esModuleInterop": true, }, "include": [ - "src/*.ts", - "test/*.ts" + "ts/**/*.ts" ] } From c11db89a344ef80d47057137aa513874f4cb9fc7 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Fri, 15 Dec 2023 13:38:25 -0800 Subject: [PATCH 03/19] Add simple working fastify app for conformance tests. --- conformance/package-lock.json | 980 +++++++++++++++++++++++++++++- conformance/package.json | 9 +- conformance/ts/src/fastify/app.ts | 13 + conformance/tsconfig.json | 4 +- 4 files changed, 990 insertions(+), 16 deletions(-) create mode 100644 conformance/ts/src/fastify/app.ts diff --git a/conformance/package-lock.json b/conformance/package-lock.json index 3f4ad86..74552ef 100644 --- a/conformance/package-lock.json +++ b/conformance/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "license": "UNLICENSED", "dependencies": { - "facility-core": "file:../ts" + "facility-core": "file:../ts", + "fastify": "^4.25.1" }, "devDependencies": { "@types/chai": "^4.2.18", @@ -17,9 +18,10 @@ "@types/node": "^12.20.15", "@types/node-fetch": "^2.5.10", "chai": "^4.3.4", + "fastify-cli": "^5.9.0", "mocha": "^10.0.0", "node-fetch": "^2.6.7", - "typescript": "~3.9.9" + "typescript": "^5.2.2" } }, "../../ts": { @@ -43,6 +45,34 @@ "typescript": "~3.9.9" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", + "integrity": "sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==" + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, "node_modules/@types/chai": { "version": "4.3.11", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", @@ -71,6 +101,53 @@ "form-data": "^4.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -117,6 +194,11 @@ "node": ">= 8" } }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -138,12 +220,49 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.1.tgz", + "integrity": "sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==", + "dependencies": { + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.6.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -180,6 +299,29 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -288,6 +430,12 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/close-with-grace": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/close-with-grace/-/close-with-grace-1.2.0.tgz", + "integrity": "sha512-Xga0jyAb4fX98u5pZAgqlbqHP8cHuy5M3Wto0k0L/36aP2C25Cjp51XfPw3Hz7dNC2L2/hF/PK/KJhO275L+VA==", + "dev": true + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -306,6 +454,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -318,17 +472,39 @@ "node": ">= 0.8" } }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -344,8 +520,7 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/decamelize": { "version": "4.0.0", @@ -389,12 +564,33 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -416,10 +612,162 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/facility-core": { "resolved": "../ts", "link": true }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==", + "dev": true + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stringify": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.9.1.tgz", + "integrity": "sha512-NMrf+uU9UJnTzfxaumMDXK1NWqtPCfGoM9DYIE+ESlaTQqjlANFBy0VAbsm6FB88Mx0nceyi18zTo5kIEUlzxg==", + "dependencies": { + "@fastify/deepmerge": "^1.0.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, + "node_modules/fastify": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.25.1.tgz", + "integrity": "sha512-D8d0rv61TwqoAS7lom2tvIlgVMlx88lLsiwXyWNjA7CU/LC/mx/Gp2WAlC0S/ABq19U+y/aRvYFG5xLUu2aMrg==", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.2.1", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^7.7.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-cli": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/fastify-cli/-/fastify-cli-5.9.0.tgz", + "integrity": "sha512-CaIte5SwkLuvlzpdd1Al1VRVVwm2TQVV4bfVP4oz/Z54KVSo6pqNTgnxWOmyzdcNUbFnhJ3Z4vRjzvHoymP5cQ==", + "dev": true, + "dependencies": { + "@fastify/deepmerge": "^1.2.0", + "chalk": "^4.1.2", + "chokidar": "^3.5.2", + "close-with-grace": "^1.1.0", + "commist": "^3.0.0", + "dotenv": "^16.0.0", + "fastify": "^4.0.0", + "fastify-plugin": "^4.0.0", + "generify": "^4.0.0", + "help-me": "^4.0.1", + "is-docker": "^2.0.0", + "make-promises-safe": "^5.1.0", + "pino-pretty": "^10.1.0", + "pkg-up": "^3.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.3.5", + "yargs-parser": "^21.1.1" + }, + "bin": { + "fastify": "cli.js" + } + }, + "node_modules/fastify-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -432,6 +780,19 @@ "node": ">=8" } }, + "node_modules/find-my-way": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", + "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -471,6 +832,14 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -491,6 +860,44 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/generify": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/generify/-/generify-4.2.0.tgz", + "integrity": "sha512-b4cVhbPfbgbCZtK0dcUc1lASitXGEAIqukV5DDAyWm25fomWnV+C+a1yXvqikcRZXHN2j0pSDyj3cTfzq8pC7Q==", + "dev": true, + "dependencies": { + "isbinaryfile": "^4.0.2", + "pump": "^3.0.0", + "split2": "^3.0.0", + "walker": "^1.0.6" + }, + "bin": { + "generify": "generify.js" + } + }, + "node_modules/generify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/generify/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -581,6 +988,68 @@ "he": "bin/he" } }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dev": true, + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -597,6 +1066,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -609,6 +1086,21 @@ "node": ">=8" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -669,6 +1161,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -681,6 +1194,34 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/light-my-request": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", + "integrity": "sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==", + "dependencies": { + "cookie": "^0.5.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -721,6 +1262,32 @@ "get-func-name": "^2.0.1" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-promises-safe": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/make-promises-safe/-/make-promises-safe-5.1.0.tgz", + "integrity": "sha512-AfdZ49rtyhQR/6cqVKGoH7y4ql7XkS5HJI1lZm0/5N6CQosy1eYbBJ/qbhkKHzo17UH7M918Bysf6XB9f3kS1g==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -754,6 +1321,15 @@ "node": ">=10" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -841,6 +1417,14 @@ "node": ">=0.10.0" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -880,6 +1464,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -919,6 +1512,198 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.17.1.tgz", + "integrity": "sha512-YoN7/NJgnsJ+fkADZqjhRt96iepWBndQHeClmSBH0sQWCb8zGD74t00SK4eOtKFi/f8TUmQnfmgglEhd2kI1RQ==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.3.0.tgz", + "integrity": "sha512-JthvQW289q3454mhM3/38wFYGWPiBMR28T3CpDNABzoTQOje9UKS7XCJQSnjWF9LQGQkGd8D7h0oq+qwiM3jFA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -928,6 +1713,21 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -940,6 +1740,14 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -949,11 +1757,49 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -969,6 +1815,41 @@ } ] }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -978,6 +1859,35 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1031,6 +1941,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1043,6 +1967,14 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.4.1.tgz", + "integrity": "sha512-T0m3MxP3wcqW0LaV3dF1mHBU294sgYSm4FOpa5eEJaYO7PqJZBOjZEQI1y4YaKNnih1FXCEYTWDS9osCoTUefg==", + "engines": { + "node": ">=12" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -1059,16 +1991,39 @@ } }, "node_modules/typescript": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", - "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" } }, "node_modules/webidl-conversions": { @@ -1125,6 +2080,11 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/conformance/package.json b/conformance/package.json index 9f3930d..e33cf38 100644 --- a/conformance/package.json +++ b/conformance/package.json @@ -4,7 +4,8 @@ "description": "Tests the ConformanceApi client.", "scripts": { "build": "tsc", - "test": "npm run --silent build && mocha dist/ts/test/conformanceApiTests.js" + "test": "npm run --silent build && mocha dist/ts/test/conformanceApiTests.js", + "start:fastify": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/ts/src/fastify/app.js && mocha dist/ts/test/conformanceApiTests.js" }, "repository": { "type": "git", @@ -18,11 +19,13 @@ "@types/node": "^12.20.15", "@types/node-fetch": "^2.5.10", "chai": "^4.3.4", + "fastify-cli": "^5.9.0", "mocha": "^10.0.0", "node-fetch": "^2.6.7", - "typescript": "~3.9.9" + "typescript": "^5.2.2" }, "dependencies": { - "facility-core": "file:../ts" + "facility-core": "file:../ts", + "fastify": "^4.25.1" } } diff --git a/conformance/ts/src/fastify/app.ts b/conformance/ts/src/fastify/app.ts new file mode 100644 index 0000000..7ecf839 --- /dev/null +++ b/conformance/ts/src/fastify/app.ts @@ -0,0 +1,13 @@ +import { FastifyPluginAsync } from "fastify"; + +export type AppOptions = {}; +const options: AppOptions = {}; + +const app: FastifyPluginAsync = async (fastify, opts): Promise => { + fastify.get('/', () => { + return { hello: 'world' }; + }); +}; + +export default app; +export { app, options }; diff --git a/conformance/tsconfig.json b/conformance/tsconfig.json index b37f54f..a6a65b0 100644 --- a/conformance/tsconfig.json +++ b/conformance/tsconfig.json @@ -7,9 +7,7 @@ "newLine": "LF", "declaration": true, "sourceMap": true, - "lib": [ - "ES2019" - ], + "lib": [ "ES2019", "DOM" ], "resolveJsonModule": true, "esModuleInterop": true, }, From b4f1516a3c1fcf1da33a4756999d2556b178f077 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Fri, 15 Dec 2023 13:54:24 -0800 Subject: [PATCH 04/19] Add "hand rolled" ConformanceApi plugin. Codegen is incoming! --- conformance/package.json | 2 +- conformance/ts/src/conformanceApiService.ts | 108 ++++ conformance/ts/src/fastify/app.ts | 9 +- .../ts/src/fastify/handRolledPlugin.ts | 562 ++++++++++++++++++ 4 files changed, 677 insertions(+), 4 deletions(-) create mode 100644 conformance/ts/src/conformanceApiService.ts create mode 100644 conformance/ts/src/fastify/handRolledPlugin.ts diff --git a/conformance/package.json b/conformance/package.json index e33cf38..d3b1315 100644 --- a/conformance/package.json +++ b/conformance/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "tsc", "test": "npm run --silent build && mocha dist/ts/test/conformanceApiTests.js", - "start:fastify": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/ts/src/fastify/app.js && mocha dist/ts/test/conformanceApiTests.js" + "fastify": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/ts/src/fastify/app.js" }, "repository": { "type": "git", diff --git a/conformance/ts/src/conformanceApiService.ts b/conformance/ts/src/conformanceApiService.ts new file mode 100644 index 0000000..b844b78 --- /dev/null +++ b/conformance/ts/src/conformanceApiService.ts @@ -0,0 +1,108 @@ +import { IServiceError, IServiceResult } from "facility-core"; +import { IConformanceApi, IGetApiInfoRequest, IGetApiInfoResponse, IGetWidgetsRequest, IGetWidgetsResponse, ICreateWidgetRequest, ICreateWidgetResponse, IGetWidgetRequest, IGetWidgetResponse, IDeleteWidgetRequest, IDeleteWidgetResponse, IGetWidgetBatchRequest, IGetWidgetBatchResponse, IMirrorFieldsRequest, IMirrorFieldsResponse, ICheckQueryRequest, ICheckQueryResponse, ICheckPathRequest, ICheckPathResponse, IMirrorHeadersRequest, IMirrorHeadersResponse, IMixedRequest, IMixedResponse, IRequiredRequest, IRequiredResponse, IMirrorBytesRequest, IMirrorBytesResponse, IMirrorTextRequest, IMirrorTextResponse, IBodyTypesRequest, IBodyTypesResponse } from "./conformanceApiTypes"; + +export type ConformanceApiTest = { + test: string; + method: string; + request: unknown; + response?: unknown; + error?: IServiceError; +}; + +export class ConformanceApiService implements IConformanceApi { + constructor(tests: ConformanceApiTest[]) { + this._tests = tests; + } + + getApiInfo(request: IGetApiInfoRequest, context?: unknown): Promise> { + return this.execute("getApiInfo", request); + } + + getWidgets(request: IGetWidgetsRequest, context?: unknown): Promise> { + return this.execute("getWidgets", request); + } + + createWidget(request: ICreateWidgetRequest, context?: unknown): Promise> { + return this.execute("createWidget", request); + } + + getWidget(request: IGetWidgetRequest, context?: unknown): Promise> { + return this.execute("getWidget", request); + } + + deleteWidget(request: IDeleteWidgetRequest, context?: unknown): Promise> { + return this.execute("deleteWidget", request); + } + + getWidgetBatch(request: IGetWidgetBatchRequest, context?: unknown): Promise> { + return this.execute("getWidgetBatch", request); + } + + mirrorFields(request: IMirrorFieldsRequest, context?: unknown): Promise> { + return this.execute("mirrorFields", request); + } + + checkQuery(request: ICheckQueryRequest, context?: unknown): Promise> { + return this.execute("checkQuery", request); + } + + checkPath(request: ICheckPathRequest, context?: unknown): Promise> { + return this.execute("checkPath", request); + } + + mirrorHeaders(request: IMirrorHeadersRequest, context?: unknown): Promise> { + return this.execute("mirrorHeaders", request); + } + + mixed(request: IMixedRequest, context?: unknown): Promise> { + return this.execute("mixed", request); + } + + required(request: IRequiredRequest, context?: unknown): Promise> { + return this.execute("required", request); + } + + mirrorBytes(request: IMirrorBytesRequest, context?: unknown): Promise> { + return this.execute("mirrorBytes", request); + } + + mirrorText(request: IMirrorTextRequest, context?: unknown): Promise> { + return this.execute("mirrorText", request); + } + + bodyTypes(request: IBodyTypesRequest, context?: unknown): Promise> { + return this.execute("bodyTypes", request); + } + + private async execute(methodName: string, request: TRequest): Promise> { + const testsWithMethodName = this._tests.filter((x) => x.method === methodName); + + if (testsWithMethodName.length === 0) { + return this.failure({ + code: "InvalidRequest", + message: `No tests found for method ${methodName}.`, + }); + } + + const testsWithMatchingRequest = testsWithMethodName.filter((x) => { + return JSON.stringify(x.request) === JSON.stringify(request); + }); + + if (testsWithMatchingRequest.length !== 1) { + return this.failure({ + code: "InvalidRequest", + message: `${testsWithMatchingRequest.length} of ${testsWithMethodName.length} tests for method ${methodName} matched request:`, + }); + } + + var testInfo = testsWithMatchingRequest[0]; + return testInfo.error ? this.failure(testInfo.error) : this.success(testInfo.response as TResponse); + } + + private success(result: T): IServiceResult { return { value: result }; } + + private failure(error: IServiceError): IServiceResult { return { error}; } + + private readonly _tests: readonly ConformanceApiTest[]; + +} diff --git a/conformance/ts/src/fastify/app.ts b/conformance/ts/src/fastify/app.ts index 7ecf839..f6dc13e 100644 --- a/conformance/ts/src/fastify/app.ts +++ b/conformance/ts/src/fastify/app.ts @@ -1,12 +1,15 @@ +import fs from "fs"; +import path from "path"; import { FastifyPluginAsync } from "fastify"; +import { ConformanceApiService } from "../conformanceApiService.js"; +import { handRolledPlugin } from "./handRolledPlugin.js"; +import conformanceTestsJson from '../../../ConformanceTests.json'; export type AppOptions = {}; const options: AppOptions = {}; const app: FastifyPluginAsync = async (fastify, opts): Promise => { - fastify.get('/', () => { - return { hello: 'world' }; - }); + fastify.register(handRolledPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests) }); }; export default app; diff --git a/conformance/ts/src/fastify/handRolledPlugin.ts b/conformance/ts/src/fastify/handRolledPlugin.ts new file mode 100644 index 0000000..7120000 --- /dev/null +++ b/conformance/ts/src/fastify/handRolledPlugin.ts @@ -0,0 +1,562 @@ +import { FastifyPluginAsync } from "fastify"; +import type { IConformanceApi, IGetApiInfoRequest, IGetWidgetsRequest, IGetWidgetRequest, IGetWidgetsResponse, ICreateWidgetRequest, IWidget, IDeleteWidgetRequest, IGetWidgetBatchRequest, IMirrorFieldsRequest, ICheckQueryRequest, Answer, ICheckPathRequest, IMirrorHeadersRequest, IMixedRequest, IRequiredRequest } from "../conformanceApi"; + +const standardErrorCodes: { [code: string]: number } = { + NotModified: 304, + InvalidRequest: 400, + NotAuthenticated: 401, + NotAuthorized: 403, + NotFound: 404, + Conflict: 409, + RequestTooLarge: 413, + TooManyRequests: 429, + InternalError: 500, + ServiceUnavailable: 503, + NotAdmin: 403, +}; + +function parseBoolean(value: string | undefined) { + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + if (lowerValue === 'true') { + return true; + } + if (lowerValue === 'false') { + return false; + } + } + return undefined; +} + +export type ConformanceApiPluginOptions = { + api: IConformanceApi; +} + +export const handRolledPlugin: FastifyPluginAsync = async (fastify, opts) => { + const api = opts.api; + fastify.route({ + url: "/", + method: "GET", + handler: async function (req, reply) { + const request: IGetApiInfoRequest = {}; + + const result = await api.getApiInfo(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + reply.send(result.value); + } + }, + }); + + fastify.route({ + url: "/widgets", + method: "GET", + handler: async (req, reply) => { + const request: IGetWidgetsRequest = {}; + + const query = req.query as Record; + if (typeof query["q"] === "string") { + request.query = query["q"]; + } + + const result = await api.getWidgets(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + reply.status(200).send({ + widgets: result.value.widgets, + } satisfies IGetWidgetsResponse); + } + + throw new Error("Result must have an error or value."); + }, + }); + + fastify.route({ + url: "/widgets", + method: "POST", + handler: async (req, reply) => { + const request: ICreateWidgetRequest = {}; + request.widget = req.body as IWidget; + + const result = await api.createWidget(request); + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.url != null) { + reply.header("Location", result.value.url); + } + + if (result.value.eTag != null) { + reply.header("eTag", result.value.eTag); + } + + if (result.value.widget) { + reply.status(201).send(result.value.widget); + } + + return; + } + + throw new Error("Result must have an error or value."); + }, + }); + + fastify.route({ + url: "/widgets/:id", + method: "GET", + handler: async (req, reply) => { + const request: IGetWidgetRequest = {}; + + const params = req.params as Record; + if (typeof params.id === "string") { + request.id = parseInt(params.id); + } + request.ifNotETag = req.headers["if-none-match"] as string; + + const result = await api.getWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.eTag != null) { + reply.header("eTag", result.value.eTag); + } + + if (result.value.widget) { + reply.status(200).send(result.value.widget); + return; + } + + if (result.value.notModified) { + reply.status(304); + return; + } + } + + throw new Error("Result must have an error or value."); + }, + }); + + // create the delete route here + fastify.route({ + url: "/widgets/:id", + method: "DELETE", + handler: async (req, reply) => { + const request: IDeleteWidgetRequest = {}; + + const params = req.params as Record; + if (typeof params.id === "string") { + request.id = parseInt(params.id); + } + request.ifETag = req.headers['if-match'] as string; + + const result = await api.deleteWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.notFound) { + reply.status(404); + return; + } + + if (result.value.conflict) { + reply.status(409); + return; + } + + reply.status(204).send({}); + return; + } + + throw new Error("Result must have an error or value."); + }, + }); + + fastify.route({ + url: "/widgets/get", + method: "POST", + handler: async (req, reply) => { + const request: IGetWidgetBatchRequest = {}; + request.ids = req.body as number[]; + + const result = await api.getWidgetBatch(request); + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.results) { + reply.status(200).send(result.value.results); + return; + } + } + + throw new Error("Result must have an error or value."); + }, + }); + + fastify.route({ + url: "/mirrorFields", + method: "POST", + handler: async (req, reply) => { + const request: IMirrorFieldsRequest = {}; + request.field = (req.body as IMirrorFieldsRequest).field; + request.matrix = (req.body as IMirrorFieldsRequest).matrix; + + const result = await api.mirrorFields(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + reply.status(200).send(result.value); + return; + } + + throw new Error("Result must have an error or value."); + }, + }); + + fastify.route({ + url: "/checkQuery", + method: "GET", + handler: async (req, reply) => { + const request: ICheckQueryRequest = {}; + const query = req.query as Record; + + if (typeof query["string"] === "string") { + request.string = query["string"]; + } + if (typeof query["boolean"] === "string") { + request.boolean = parseBoolean(query["boolean"]); + } + if (typeof query["double"] === "string") { + request.double = parseFloat(query["double"]); + } + if (typeof query["int32"] === "string") { + request.int32 = parseInt(query["int32"]); + } + if (typeof query["int64"] === "string") { + request.int64 = parseInt(query["int64"]); + } + if (typeof query["decimal"] === "string") { + request.decimal = parseFloat(query["decimal"]); + } + if (typeof query["enum"] === "string") { + request.enum = query["enum"] as Answer; + } + if (typeof query["datetime"] === "string") { + request.datetime = query["datetime"]; + } + + const result = await api.checkQuery(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + reply.status(200).send(result.value); + } + + throw new Error("Result must have an error or value."); + }, + }); + + fastify.route({ + url: "/checkPath/:string/:boolean/:double/:int32/:int64/:decimal/:enum/:datetime", + method: "GET", + handler: async (req, reply) => { + const request: ICheckPathRequest = {}; + const params = req.params as Record; + + if (typeof params["string"] === "string") { + request.string = params["string"]; + } + if (typeof params["boolean"] === "string") { + request.boolean = parseBoolean(params["boolean"]); + } + if (typeof params["double"] === "string") { + request.double = parseFloat(params["double"]); + } + if (typeof params["int32"] === "string") { + request.int32 = parseInt(params["int32"]); + } + if (typeof params["int64"] === "string") { + request.int64 = parseInt(params["int64"]); + } + if (typeof params["decimal"] === "string") { + request.decimal = parseFloat(params["decimal"]); + } + if (typeof params["enum"] === "string") { + request.enum = params["enum"] as Answer; + } + if (typeof params["datetime"] === "string") { + request.datetime = params["datetime"]; + } + + const result = await api.checkPath(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + + if (result.value) { + reply.status(200).send(result.value); + } + + throw new Error("Result must have an error or value."); + } + }); + + fastify.route({ + url: "/mirrorHeaders", + method: "GET", + handler: async (req, reply) => { + const request: IMirrorHeadersRequest = {}; + const headers = req.headers as Record; + + if (typeof headers["string"] === "string") { + request.string = headers["string"]; + } + if (typeof headers["boolean"] === "string") { + request.boolean = parseBoolean(headers["boolean"]); + } + if (typeof headers["double"] === "string") { + request.double = parseFloat(headers["double"]); + } + if (typeof headers["int32"] === "string") { + request.int32 = parseInt(headers["int32"]); + } + if (typeof headers["int64"] === "string") { + request.int64 = parseInt(headers["int64"]); + } + if (typeof headers["decimal"] === "string") { + request.decimal = parseFloat(headers["decimal"]); + } + if (typeof headers["enum"] === "string") { + request.enum = headers["enum"] as Answer; + } + if (typeof headers["datetime"] === "string") { + request.datetime = headers["datetime"]; + } + + const result = await api.mirrorHeaders(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + return; + } + if (result.value) { + if (result.value.string != null) { + reply.header("string", result.value.string); + } + if (result.value.boolean != null) { + reply.header("boolean", result.value.boolean.toString()); + } + if (result.value.double != null) { + reply.header("double", result.value.double.toString()); + } + if (result.value.int32 != null) { + reply.header("int32", result.value.int32.toString()); + } + if (result.value.int64 != null) { + reply.header("int64", result.value.int64.toString()); + } + if (result.value.decimal != null) { + reply.header("decimal", result.value.decimal.toString()); + } + if (result.value.enum != null) { + reply.header("enum", result.value.enum); + } + if (result.value.datetime != null) { + reply.header("datetime", result.value.datetime); + } + + reply.status(200); + return; + } + + throw new Error("Result must have an error or value."); + } + }); + + // implement the mixed route here. + // pull path from params, query from query, header from headers, and normal from body + fastify.route({ + url: "/mixed/:path", + method: "POST", + handler: async (req, reply) => { + const request: IMixedRequest = {}; + const params = req.params as Record; + const query = req.query as Record; + const headers = req.headers as Record; + const body = req.body as Record; + + if (typeof params["path"] === "string") { + request.path = params["path"]; + } + if (typeof query["query"] === "string") { + request.query = query["query"]; + } + if (typeof headers["header"] === "string") { + request.header = headers["header"]; + } + if (typeof body["normal"] === "string") { + request.normal = body["normal"]; + } + + const result = await api.mixed(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + } + + if (result.value) { + if (result.value.header != null) { + reply.header("header", result.value.header); + } + + if (result.value.body != null) { + reply.status(202).send(result.value.body); + return; + } + + if (result.value.empty) { + reply.status(204); + return; + } + + + reply.status(200).send(result.value); + return; + } + + throw new Error("Result must have an error or value."); + } + }); + + fastify.route({ + url: "/required", + method: "POST", + handler: async (req, reply) => { + const request: IRequiredRequest = {}; + const body = req.body as Record; + const query = req.query as Record; + + if (typeof query["query"] === "string") { + request.query = query["query"]; + } + if (typeof body["normal"] === "string") { + request.normal = body["normal"]; + } + if (typeof body["widget"]) { + request.widget = body["widget"] as never; + } + if (typeof body["widgets"]) { + request.widgets = body["widgets"] as never; + } + if (typeof body["widgetMatrix"]) { + request.widgetMatrix = body["widgetMatrix"] as never; + } + if (typeof body["widgetResult"]) { + request.widgetResult = body["widgetResult"] as never; + } + if (typeof body["widgetResults"]) { + request.widgetResults = body["widgetResults"] as never; + } + if (typeof body["widgetMap"]) { + request.widgetMap = body["widgetMap"] as never; + } + if (typeof body["hasWidget"]) { + request.hasWidget = body["hasWidget"] as never; + } + if (typeof body["point"]) { + request.point = body["point"] as never; + } + + const result = await api.required(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + reply.status(status || 500).send(result.error); + } + + if (result.value) { + reply.status(200).send(result.value); + return; + } + + throw new Error("Result must have an error or value."); + }, + }); + + fastify.route({ + url: "/mirrorBytes", + method: "POST", + handler: async (req, reply) => { + reply.status(500).send({ + code: "NotImplemented", + message: "Javascript HttpClientUtility doesn't support non-json responses currently.", + }); + } + }); + + fastify.route({ + url: "/mirrorText", + method: "POST", + handler: async (req, reply) => { + reply.status(500).send({ + code: "NotImplemented", + message: "Javascript HttpClientUtility doesn't support non-json responses currently.", + }); + } + }); + + fastify.route({ + url: "/bodyTypes", + method: "POST", + handler: async (req, reply) => { + reply.status(500).send({ + code: "NotImplemented", + message: "Need to support FSD attributes like `[http(from: body, type: \"text/x-input\")]`", + }); + } + }); +} From 724ddcaf5c476b007a96d7b050ddfc936224b55a Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Fri, 15 Dec 2023 15:27:22 -0800 Subject: [PATCH 05/19] Generate fastify plugin shell. And generate conformance plugin. --- .../ts/src/fastify/conformanceApiPlugin.ts | 43 +++++++++ .../JavaScriptGenerator.cs | 91 +++++++++++++++++++ .../JavaScriptGeneratorSettings.cs | 5 + src/fsdgenjs/FsdGenJavaScriptApp.cs | 3 + tools/Build/Build.cs | 2 +- 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 conformance/ts/src/fastify/conformanceApiPlugin.ts diff --git a/conformance/ts/src/fastify/conformanceApiPlugin.ts b/conformance/ts/src/fastify/conformanceApiPlugin.ts new file mode 100644 index 0000000..5fa4d66 --- /dev/null +++ b/conformance/ts/src/fastify/conformanceApiPlugin.ts @@ -0,0 +1,43 @@ +// DO NOT EDIT: generated by fsdgenjs +/* eslint-disable */ + +import { FastifyPluginAsync } from 'fastify'; +import { IServiceResult } from 'facility-core'; +import { IConformanceApi, IGetApiInfoRequest, IGetApiInfoResponse, IGetWidgetsRequest, IGetWidgetsResponse, ICreateWidgetRequest, ICreateWidgetResponse, IGetWidgetRequest, IGetWidgetResponse, IDeleteWidgetRequest, IDeleteWidgetResponse, IGetWidgetBatchRequest, IGetWidgetBatchResponse, IMirrorFieldsRequest, IMirrorFieldsResponse, ICheckQueryRequest, ICheckQueryResponse, ICheckPathRequest, ICheckPathResponse, IMirrorHeadersRequest, IMirrorHeadersResponse, IMixedRequest, IMixedResponse, IRequiredRequest, IRequiredResponse, IMirrorBytesRequest, IMirrorBytesResponse, IMirrorTextRequest, IMirrorTextResponse, IBodyTypesRequest, IBodyTypesResponse, IWidget, IAny, IAnyArray, IAnyMap, IAnyResult, IAnyNullable, IHasWidget, Answer, ApiErrors } from '../conformanceApiTypes'; +export * from '../conformanceApiTypes'; + +const standardErrorCodes: { [code: string]: number } = { + 'NotModified': 304, + 'InvalidRequest': 400, + 'NotAuthenticated': 401, + 'NotAuthorized': 403, + 'NotFound': 404, + 'Conflict': 409, + 'RequestTooLarge': 413, + 'TooManyRequests': 429, + 'InternalError': 500, + 'ServiceUnavailable': 503, + 'NotAdmin': 403, + 'TooHappy': 500, +}; + +function parseBoolean(value: string | undefined) { + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + if (lowerValue === 'true') { + return true; + } + if (lowerValue === 'false') { + return false; + } + } + return undefined; +} + +export type ConformanceApiPluginOptions = { + api: IConformanceApi; +} + +export const conformanceApiPlugin: FastifyPluginAsync = async (fastify, opts) => { + throw new Error('Not implemented'); +} diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index f8c4e58..2c1d4e2 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -33,6 +33,11 @@ public static int GenerateJavaScript(JavaScriptGeneratorSettings settings) => /// public bool Express { get; set; } + /// + /// True to generate Fastify plugin. + /// + public bool Fastify { get; set; } + /// /// True to disable ESLint via code comment. /// @@ -46,6 +51,7 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) var httpServiceInfo = HttpServiceInfo.Create(service); var moduleName = ModuleName ?? service.Name; + var camelCaseModuleName = CodeGenUtility.ToCamelCase(moduleName); var capModuleName = CodeGenUtility.Capitalize(moduleName); var typesFileName = CodeGenUtility.Uncapitalize(moduleName) + "Types" + (TypeScript ? ".ts" : ".js"); var clientFileName = CodeGenUtility.Uncapitalize(moduleName) + (TypeScript ? ".ts" : ".js"); @@ -606,6 +612,90 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) })); } + if (Fastify) + { + var pluginFileName = CodeGenUtility.Uncapitalize(moduleName) + "Plugin" + (TypeScript ? ".ts" : ".js"); + namedTexts.Add(CreateFile("fastify/" + pluginFileName, code => + { + WriteFileHeader(code); + + if (!TypeScript) + code.WriteLine("'use strict';"); + + code.WriteLine(); + + var fastifyImports = new List(); + if (TypeScript) + fastifyImports.Add("FastifyPluginAsync"); + WriteImports(code, fastifyImports, "fastify"); + + var facilityImports = new List(); + if (TypeScript) + facilityImports.Add("IServiceResult"); + WriteImports(code, facilityImports, "facility-core"); + if (TypeScript) + { + WriteImports(code, typeNames, $"../{CodeGenUtility.Uncapitalize(moduleName)}Types"); + code.WriteLine($"export * from '../{CodeGenUtility.Uncapitalize(moduleName)}Types';"); + } + + // TODO: export this from facility-core + code.WriteLine(); + using (code.Block("const standardErrorCodes" + IfTypeScript(": { [code: string]: number }") + " = {", "};")) + { + code.WriteLine("'NotModified': 304,"); + code.WriteLine("'InvalidRequest': 400,"); + code.WriteLine("'NotAuthenticated': 401,"); + code.WriteLine("'NotAuthorized': 403,"); + code.WriteLine("'NotFound': 404,"); + code.WriteLine("'Conflict': 409,"); + code.WriteLine("'RequestTooLarge': 413,"); + code.WriteLine("'TooManyRequests': 429,"); + code.WriteLine("'InternalError': 500,"); + code.WriteLine("'ServiceUnavailable': 503,"); + + foreach (var errorSetInfo in httpServiceInfo.ErrorSets) + { + foreach (var error in errorSetInfo.Errors) + { + code.WriteLine($"'{error.ServiceError.Name}': {(int) error.StatusCode},"); + } + } + } + + // TODO: export this from facility-core? + code.WriteLine(); + using (code.Block("function parseBoolean(value" + IfTypeScript(": string | undefined") + ") {", "}")) + { + using (code.Block("if (typeof value === 'string') {", "}")) + { + code.WriteLine("const lowerValue = value.toLowerCase();"); + using (code.Block("if (lowerValue === 'true') {", "}")) + code.WriteLine("return true;"); + using (code.Block("if (lowerValue === 'false') {", "}")) + code.WriteLine("return false;"); + } + code.WriteLine("return undefined;"); + } + + if (TypeScript) + { + code.WriteLine(); + /* All code below this is meant to create the code that is in handRollingPlugin.ts */ + using (code.Block($"export type {capModuleName}PluginOptions = {{", "}")) + { + code.WriteLine($"api: I{capModuleName};"); + } + } + + code.WriteLine(); + using (code.Block($"export const {camelCaseModuleName}Plugin: FastifyPluginAsync<{capModuleName}PluginOptions> = async (fastify, opts) => {{", "}")) + { + code.WriteLine("throw new Error('Not implemented');"); + } + })); + } + return new CodeGenOutput(namedTexts, new List()); } @@ -618,6 +708,7 @@ public override void ApplySettings(FileGeneratorSettings settings) ModuleName = ourSettings.ModuleName; TypeScript = ourSettings.TypeScript; Express = ourSettings.Express; + Fastify = ourSettings.Fastify; DisableESLint = ourSettings.DisableESLint; } diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs index ce66619..45ffb04 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs @@ -22,6 +22,11 @@ public sealed class JavaScriptGeneratorSettings : FileGeneratorSettings /// public bool Express { get; set; } + /// + /// True to generate Fastify plugin. + /// + public bool Fastify { get; set; } + /// /// True to disable ESLint via code comment. /// diff --git a/src/fsdgenjs/FsdGenJavaScriptApp.cs b/src/fsdgenjs/FsdGenJavaScriptApp.cs index bd1dfdf..9425fc9 100644 --- a/src/fsdgenjs/FsdGenJavaScriptApp.cs +++ b/src/fsdgenjs/FsdGenJavaScriptApp.cs @@ -22,6 +22,8 @@ public sealed class FsdGenJavaScriptApp : CodeGeneratorApp " Generates TypeScript.", " --express", " Generates Express service.", + " --fastify", + " Generates Fastify plugin.", " --disable-eslint", " Disables ESLint via code comment.", }; @@ -34,6 +36,7 @@ protected override FileGeneratorSettings CreateSettings(ArgsReader args) => ModuleName = args.ReadOption("module"), TypeScript = args.ReadFlag("typescript"), Express = args.ReadFlag("express"), + Fastify = args.ReadFlag("fastify"), DisableESLint = args.ReadFlag("disable-eslint"), }; } diff --git a/tools/Build/Build.cs b/tools/Build/Build.cs index a947606..851d233 100644 --- a/tools/Build/Build.cs +++ b/tools/Build/Build.cs @@ -46,7 +46,7 @@ void CodeGen(bool verify) RunDotNet("FacilityConformance", "fsd", "--output", "conformance/ConformanceApi.fsd", verifyOption); RunDotNet("FacilityConformance", "json", "--output", "conformance/ConformanceTests.json", verifyOption); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/ts/src/", "--typescript", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/ts/src/", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); void RunCodeGen(params string?[] args) => RunDotNet(new[] { "run", "--no-build", "--project", $"src/{codegen}", "-f", "net6.0", "-c", configuration, "--", "--newline", "lf", verifyOption }.Concat(args)); From ae6f3b9a33d7e3e036b12ce351fd180f4e0b2eea Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 16 Dec 2023 07:25:32 -0800 Subject: [PATCH 06/19] Fill out codegen for fastify plugin. - all but three conformance tests are passing (mirrorBytes, mirrorText, and bodyTypes) which will require a bit of work to configurare the fastify server, as well as potential changes to the generated client, which assumes that fetch requests come back with JSON. --- conformance/ts/src/fastify/app.ts | 6 +- .../ts/src/fastify/conformanceApiPlugin.ts | 495 +++++++++++++++++- .../JavaScriptGenerator.cs | 139 ++++- 3 files changed, 632 insertions(+), 8 deletions(-) diff --git a/conformance/ts/src/fastify/app.ts b/conformance/ts/src/fastify/app.ts index f6dc13e..2447dff 100644 --- a/conformance/ts/src/fastify/app.ts +++ b/conformance/ts/src/fastify/app.ts @@ -1,15 +1,13 @@ -import fs from "fs"; -import path from "path"; import { FastifyPluginAsync } from "fastify"; import { ConformanceApiService } from "../conformanceApiService.js"; -import { handRolledPlugin } from "./handRolledPlugin.js"; import conformanceTestsJson from '../../../ConformanceTests.json'; +import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; export type AppOptions = {}; const options: AppOptions = {}; const app: FastifyPluginAsync = async (fastify, opts): Promise => { - fastify.register(handRolledPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests) }); + fastify.register(conformanceApiPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests) }); }; export default app; diff --git a/conformance/ts/src/fastify/conformanceApiPlugin.ts b/conformance/ts/src/fastify/conformanceApiPlugin.ts index 5fa4d66..3381741 100644 --- a/conformance/ts/src/fastify/conformanceApiPlugin.ts +++ b/conformance/ts/src/fastify/conformanceApiPlugin.ts @@ -39,5 +39,498 @@ export type ConformanceApiPluginOptions = { } export const conformanceApiPlugin: FastifyPluginAsync = async (fastify, opts) => { - throw new Error('Not implemented'); + const { api } = opts; + + fastify.route({ + url: '/', + method: 'GET', + handler: async function (req, res) { + const request: IGetApiInfoRequest = {}; + + const result = await api.getApiInfo(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets', + method: 'GET', + handler: async function (req, res) { + const request: IGetWidgetsRequest = {}; + + const query = req.query as Record; + if (typeof query['q'] === 'string') request.query = query['q']; + + const result = await api.getWidgets(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets', + method: 'POST', + handler: async function (req, res) { + const request: ICreateWidgetRequest = {}; + + request.widget = req.body as never; + + const result = await api.createWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.url != null) res.header('Location', result.value.url); + if (result.value.eTag != null) res.header('eTag', result.value.eTag); + + if (result.value.widget) { + res.status(201).send(result.value.widget); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets/:id', + method: 'GET', + handler: async function (req, res) { + const request: IGetWidgetRequest = {}; + + const params = req.params as Record; + if (typeof params['id'] === 'string') request.id = parseInt(params['id'], 10); + + const headers = req.headers as Record; + if (typeof headers['if-none-match'] === 'string') request.ifNotETag = headers['if-none-match']; + + const result = await api.getWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.eTag != null) res.header('eTag', result.value.eTag); + + if (result.value.widget) { + res.status(200).send(result.value.widget); + return; + } + + if (result.value.notModified) { + res.status(304); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets/:id', + method: 'DELETE', + handler: async function (req, res) { + const request: IDeleteWidgetRequest = {}; + + const params = req.params as Record; + if (typeof params['id'] === 'string') request.id = parseInt(params['id'], 10); + + const headers = req.headers as Record; + if (typeof headers['if-match'] === 'string') request.ifETag = headers['if-match']; + + const result = await api.deleteWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.notFound) { + res.status(404); + return; + } + + if (result.value.conflict) { + res.status(409); + return; + } + + res.status(204); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets/get', + method: 'POST', + handler: async function (req, res) { + const request: IGetWidgetBatchRequest = {}; + + request.ids = req.body as never; + + const result = await api.getWidgetBatch(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.results) { + res.status(200).send(result.value.results); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorFields', + method: 'POST', + handler: async function (req, res) { + const request: IMirrorFieldsRequest = {}; + + const body = req.body as Record; + request.field = body.field; + request.matrix = body.matrix; + + const result = await api.mirrorFields(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/checkQuery', + method: 'GET', + handler: async function (req, res) { + const request: ICheckQueryRequest = {}; + + const query = req.query as Record; + if (typeof query['string'] === 'string') request.string = query['string']; + if (typeof query['boolean'] === 'string') request.boolean = parseBoolean(query['boolean']); + if (typeof query['double'] === 'string') request.double = parseFloat(query['double']); + if (typeof query['int32'] === 'string') request.int32 = parseInt(query['int32'], 10); + if (typeof query['int64'] === 'string') request.int64 = parseInt(query['int64'], 10); + if (typeof query['decimal'] === 'string') request.decimal = parseFloat(query['decimal']); + if (typeof query['enum'] === 'string') request.enum = query['enum'] as Answer; + if (typeof query['datetime'] === 'string') request.datetime = query['datetime']; + + const result = await api.checkQuery(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/checkPath/:string/:boolean/:double/:int32/:int64/:decimal/:enum/:datetime', + method: 'GET', + handler: async function (req, res) { + const request: ICheckPathRequest = {}; + + const params = req.params as Record; + if (typeof params['string'] === 'string') request.string = params['string']; + if (typeof params['boolean'] === 'string') request.boolean = parseBoolean(params['boolean']); + if (typeof params['double'] === 'string') request.double = parseFloat(params['double']); + if (typeof params['int32'] === 'string') request.int32 = parseInt(params['int32'], 10); + if (typeof params['int64'] === 'string') request.int64 = parseInt(params['int64'], 10); + if (typeof params['decimal'] === 'string') request.decimal = parseFloat(params['decimal']); + if (typeof params['enum'] === 'string') request.enum = params['enum'] as Answer; + if (typeof params['datetime'] === 'string') request.datetime = params['datetime']; + + const result = await api.checkPath(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorHeaders', + method: 'GET', + handler: async function (req, res) { + const request: IMirrorHeadersRequest = {}; + + const headers = req.headers as Record; + if (typeof headers['string'] === 'string') request.string = headers['string']; + if (typeof headers['boolean'] === 'string') request.boolean = parseBoolean(headers['boolean']); + if (typeof headers['double'] === 'string') request.double = parseFloat(headers['double']); + if (typeof headers['int32'] === 'string') request.int32 = parseInt(headers['int32'], 10); + if (typeof headers['int64'] === 'string') request.int64 = parseInt(headers['int64'], 10); + if (typeof headers['decimal'] === 'string') request.decimal = parseFloat(headers['decimal']); + if (typeof headers['enum'] === 'string') request.enum = headers['enum'] as Answer; + if (typeof headers['datetime'] === 'string') request.datetime = headers['datetime']; + + const result = await api.mirrorHeaders(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.string != null) res.header('string', result.value.string); + if (result.value.boolean != null) res.header('boolean', result.value.boolean); + if (result.value.double != null) res.header('double', result.value.double); + if (result.value.int32 != null) res.header('int32', result.value.int32); + if (result.value.int64 != null) res.header('int64', result.value.int64); + if (result.value.decimal != null) res.header('decimal', result.value.decimal); + if (result.value.enum != null) res.header('enum', result.value.enum); + if (result.value.datetime != null) res.header('datetime', result.value.datetime); + + res.status(200); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mixed/:path', + method: 'POST', + handler: async function (req, res) { + const request: IMixedRequest = {}; + + const params = req.params as Record; + if (typeof params['path'] === 'string') request.path = params['path']; + + const query = req.query as Record; + if (typeof query['query'] === 'string') request.query = query['query']; + + const headers = req.headers as Record; + if (typeof headers['header'] === 'string') request.header = headers['header']; + + const body = req.body as Record; + request.normal = body.normal; + + const result = await api.mixed(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.header != null) res.header('header', result.value.header); + + if (result.value.body) { + res.status(202).send(result.value.body); + return; + } + + if (result.value.empty) { + res.status(204); + return; + } + + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/required', + method: 'POST', + handler: async function (req, res) { + const request: IRequiredRequest = {}; + + const query = req.query as Record; + if (typeof query['query'] === 'string') request.query = query['query']; + + const body = req.body as Record; + request.normal = body.normal; + request.widget = body.widget; + request.widgets = body.widgets; + request.widgetMatrix = body.widgetMatrix; + request.widgetResult = body.widgetResult; + request.widgetResults = body.widgetResults; + request.widgetMap = body.widgetMap; + request.hasWidget = body.hasWidget; + request.point = body.point; + + const result = await api.required(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorBytes', + method: 'POST', + handler: async function (req, res) { + const request: IMirrorBytesRequest = {}; + + const headers = req.headers as Record; + if (typeof headers['content-type'] === 'string') request.type = headers['content-type']; + + request.content = req.body as never; + + const result = await api.mirrorBytes(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.type != null) res.header('Content-Type', result.value.type); + + if (result.value.content) { + res.status(200).send(result.value.content); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorText', + method: 'POST', + handler: async function (req, res) { + const request: IMirrorTextRequest = {}; + + const headers = req.headers as Record; + if (typeof headers['content-type'] === 'string') request.type = headers['content-type']; + + request.content = req.body as never; + + const result = await api.mirrorText(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.type != null) res.header('Content-Type', result.value.type); + + if (result.value.content) { + res.status(200).send(result.value.content); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/bodyTypes', + method: 'POST', + handler: async function (req, res) { + const request: IBodyTypesRequest = {}; + + request.content = req.body as never; + + const result = await api.bodyTypes(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.content) { + res.status(200).send(result.value.content); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); } diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 2c1d4e2..620b763 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -681,7 +681,6 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) if (TypeScript) { code.WriteLine(); - /* All code below this is meant to create the code that is in handRollingPlugin.ts */ using (code.Block($"export type {capModuleName}PluginOptions = {{", "}")) { code.WriteLine($"api: I{capModuleName};"); @@ -689,9 +688,143 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) } code.WriteLine(); - using (code.Block($"export const {camelCaseModuleName}Plugin: FastifyPluginAsync<{capModuleName}PluginOptions> = async (fastify, opts) => {{", "}")) + using (code.Block($"export const {camelCaseModuleName}Plugin" + IfTypeScript($": FastifyPluginAsync<{capModuleName}PluginOptions>") + " = async (fastify, opts) => {", "}")) { - code.WriteLine("throw new Error('Not implemented');"); + code.WriteLine("const { api } = opts;"); + + foreach (var httpMethodInfo in httpServiceInfo.Methods) + { + var methodName = httpMethodInfo.ServiceMethod.Name; + var capMethodName = CodeGenUtility.Capitalize(methodName); + var fastifyPath = httpMethodInfo.Path; + foreach (var httpPathField in httpMethodInfo.PathFields) + fastifyPath = ReplaceOrdinal(fastifyPath, "{" + httpPathField.Name + "}", $":{httpPathField.Name}"); + + code.WriteLine(); + using (code.Block("fastify.route({", "});")) + { + code.WriteLine($"url: '{fastifyPath}',"); + code.WriteLine($"method: '{httpMethodInfo.Method.ToUpperInvariant()}',"); + using (code.Block("handler: async function (req, res) {", "}")) + { + code.WriteLine("const request" + IfTypeScript($": I{capMethodName}Request") + " = {};"); + if (httpMethodInfo.PathFields.Count != 0) + { + code.WriteLine(); + code.WriteLine($"const params = req.params{IfTypeScript(" as Record")};"); + foreach (var pathParam in httpMethodInfo.PathFields) + { + code.WriteLine($"if (typeof params['{pathParam.Name}'] === 'string') request.{pathParam.ServiceField.Name} = {ParseFieldValue(pathParam.ServiceField, service, $"params['{pathParam.Name}']")};"); + } + } + + if (httpMethodInfo.QueryFields.Count != 0) + { + code.WriteLine(); + code.WriteLine($"const query = req.query{IfTypeScript(" as Record")};"); + foreach (var queryParam in httpMethodInfo.QueryFields) + { + code.WriteLine($"if (typeof query['{queryParam.Name}'] === 'string') request.{queryParam.ServiceField.Name} = {ParseFieldValue(queryParam.ServiceField, service, $"query['{queryParam.Name}']")};"); + } + } + + if (httpMethodInfo.RequestHeaderFields.Count != 0) + { + code.WriteLine(); + code.WriteLine($"const headers = req.headers{IfTypeScript(" as Record")};"); + foreach (var header in httpMethodInfo.RequestHeaderFields) + { + string lowerHeaderName = header.Name.ToLowerInvariant(); + code.WriteLine($"if (typeof headers['{lowerHeaderName}'] === 'string') request.{header.ServiceField.Name} = {ParseFieldValue(header.ServiceField, service, $"headers['{lowerHeaderName}']")};"); + } + } + + if (httpMethodInfo.RequestBodyField != null) + { + code.WriteLine(); + code.WriteLine($"request.{httpMethodInfo.RequestBodyField.ServiceField.Name} = req.body{IfTypeScript(" as never")};"); + } + else if (httpMethodInfo.RequestNormalFields.Count != 0) + { + code.WriteLine(); + code.WriteLine($"const body = req.body{IfTypeScript(" as Record")};"); + foreach (var field in httpMethodInfo.RequestNormalFields) + code.WriteLine($"request.{field.ServiceField.Name} = body.{field.ServiceField.Name};"); + } + + code.WriteLine(); + code.WriteLine($"const result = await api.{methodName}(request);"); + + code.WriteLine(); + using (code.Block("if (result.error) {", "}")) + { + code.WriteLine("const status = result.error.code && standardErrorCodes[result.error.code];"); + code.WriteLine("res.status(status || 500).send(result.error);"); + code.WriteLine("return;"); + } + + code.WriteLine(); + using (code.Block("if (result.value) {", "}")) + { + if (httpMethodInfo.ResponseHeaderFields.Count != 0) + { + code.WriteLineSkipOnce(); + foreach (var field in httpMethodInfo.ResponseHeaderFields) + code.WriteLine($"if (result.value.{field.ServiceField.Name} != null) res.header('{field.Name}', result.value.{field.ServiceField.Name});"); + } + + var handledResponses = httpMethodInfo.ValidResponses.ToList(); + foreach (var validResponse in httpMethodInfo.ValidResponses.Where(x => x.BodyField is not null)) + { + handledResponses.Remove(validResponse); + var bodyField = validResponse.BodyField!; + var statusCode = (int) validResponse.StatusCode; + + code.WriteLineSkipOnce(); + using (code.Block($"if (result.value.{bodyField.ServiceField.Name}) {{", "}")) + { + var bodyFieldType = service.GetFieldType(bodyField.ServiceField)!; + if (bodyFieldType.Kind == ServiceTypeKind.Boolean) + { + code.WriteLine($"res.status({statusCode});"); + code.WriteLine("return;"); + } + else + { + code.WriteLine($"res.status({statusCode}).send(result.value.{bodyField.ServiceField.Name});"); + code.WriteLine("return;"); + } + } + } + + if (handledResponses.Count == 1) + { + var lastValidResponse = handledResponses[0]; + var statusCode = (int) lastValidResponse.StatusCode; + + if (lastValidResponse.NormalFields?.Count > 0) + { + code.WriteLineSkipOnce(); + code.WriteLine($"res.status({statusCode}).send(result.value);"); + } + else + { + code.WriteLineSkipOnce(); + code.WriteLine($"res.status({statusCode});"); + } + code.WriteLine("return;"); + } + else if (handledResponses.Count > 1) + { + throw new InvalidOperationException("More than one response is left."); + } + } + + code.WriteLine(); + code.WriteLine("throw new Error('Result must have an error or value.');"); + } + } + } } })); } From 0901688a7cfc5746169561d08ed6e039896e54f2 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 16 Dec 2023 14:25:25 -0800 Subject: [PATCH 07/19] Generate separate copy of types for fastify plugin. - This allows the plugin to live anywhere without needing to know where the client type file is. I debated creating a new type file instead of inlining them in the plugin, but didn't really see a point since it is just for the server to use. Perhaps I'm missing something though... --- .../ts/src/fastify/conformanceApiPlugin.ts | 525 +++++++++++++++++- .../JavaScriptGenerator.cs | 161 +++--- 2 files changed, 608 insertions(+), 78 deletions(-) diff --git a/conformance/ts/src/fastify/conformanceApiPlugin.ts b/conformance/ts/src/fastify/conformanceApiPlugin.ts index 3381741..8dc9442 100644 --- a/conformance/ts/src/fastify/conformanceApiPlugin.ts +++ b/conformance/ts/src/fastify/conformanceApiPlugin.ts @@ -2,9 +2,7 @@ /* eslint-disable */ import { FastifyPluginAsync } from 'fastify'; -import { IServiceResult } from 'facility-core'; -import { IConformanceApi, IGetApiInfoRequest, IGetApiInfoResponse, IGetWidgetsRequest, IGetWidgetsResponse, ICreateWidgetRequest, ICreateWidgetResponse, IGetWidgetRequest, IGetWidgetResponse, IDeleteWidgetRequest, IDeleteWidgetResponse, IGetWidgetBatchRequest, IGetWidgetBatchResponse, IMirrorFieldsRequest, IMirrorFieldsResponse, ICheckQueryRequest, ICheckQueryResponse, ICheckPathRequest, ICheckPathResponse, IMirrorHeadersRequest, IMirrorHeadersResponse, IMixedRequest, IMixedResponse, IRequiredRequest, IRequiredResponse, IMirrorBytesRequest, IMirrorBytesResponse, IMirrorTextRequest, IMirrorTextResponse, IBodyTypesRequest, IBodyTypesResponse, IWidget, IAny, IAnyArray, IAnyMap, IAnyResult, IAnyNullable, IHasWidget, Answer, ApiErrors } from '../conformanceApiTypes'; -export * from '../conformanceApiTypes'; +import { IServiceResult, IServiceError } from 'facility-core'; const standardErrorCodes: { [code: string]: number } = { 'NotModified': 304, @@ -534,3 +532,524 @@ export const conformanceApiPlugin: FastifyPluginAsync>; + + /** Gets widgets. */ + getWidgets(request: IGetWidgetsRequest, context?: unknown): Promise>; + + /** Creates a new widget. */ + createWidget(request: ICreateWidgetRequest, context?: unknown): Promise>; + + /** Gets the specified widget. */ + getWidget(request: IGetWidgetRequest, context?: unknown): Promise>; + + /** Deletes the specified widget. */ + deleteWidget(request: IDeleteWidgetRequest, context?: unknown): Promise>; + + /** Gets the specified widgets. */ + getWidgetBatch(request: IGetWidgetBatchRequest, context?: unknown): Promise>; + + mirrorFields(request: IMirrorFieldsRequest, context?: unknown): Promise>; + + checkQuery(request: ICheckQueryRequest, context?: unknown): Promise>; + + checkPath(request: ICheckPathRequest, context?: unknown): Promise>; + + mirrorHeaders(request: IMirrorHeadersRequest, context?: unknown): Promise>; + + mixed(request: IMixedRequest, context?: unknown): Promise>; + + required(request: IRequiredRequest, context?: unknown): Promise>; + + mirrorBytes(request: IMirrorBytesRequest, context?: unknown): Promise>; + + mirrorText(request: IMirrorTextRequest, context?: unknown): Promise>; + + bodyTypes(request: IBodyTypesRequest, context?: unknown): Promise>; +} + +/** Request for GetApiInfo. */ +export interface IGetApiInfoRequest { +} + +/** Response for GetApiInfo. */ +export interface IGetApiInfoResponse { + /** The name of the service. */ + service?: string; + + /** The version of the service. */ + version?: string; +} + +/** Request for GetWidgets. */ +export interface IGetWidgetsRequest { + /** The query. */ + query?: string; +} + +/** Response for GetWidgets. */ +export interface IGetWidgetsResponse { + /** The widgets. */ + widgets?: IWidget[]; +} + +/** Request for CreateWidget. */ +export interface ICreateWidgetRequest { + /** The widget to create. */ + widget?: IWidget; +} + +/** Response for CreateWidget. */ +export interface ICreateWidgetResponse { + /** The created widget. */ + widget?: IWidget; + + /** The URL of the created widget. */ + url?: string; + + /** The ETag of the created widget. */ + eTag?: string; +} + +/** Request for GetWidget. */ +export interface IGetWidgetRequest { + /** The widget ID. */ + id?: number; + + /** Don't get the widget if it has this ETag. */ + ifNotETag?: string; +} + +/** Response for GetWidget. */ +export interface IGetWidgetResponse { + /** The requested widget. */ + widget?: IWidget; + + /** The ETag of the widget. */ + eTag?: string; + + /** The widget still has the specified ETag. */ + notModified?: boolean; +} + +/** Request for DeleteWidget. */ +export interface IDeleteWidgetRequest { + /** The widget ID. */ + id?: number; + + /** Don't delete the widget unless it has this ETag. */ + ifETag?: string; +} + +/** Response for DeleteWidget. */ +export interface IDeleteWidgetResponse { + /** The widget was not found. */ + notFound?: boolean; + + /** The widget no longer has the specified ETag. */ + conflict?: boolean; +} + +/** Request for GetWidgetBatch. */ +export interface IGetWidgetBatchRequest { + /** The IDs of the widgets to return. */ + ids?: number[]; +} + +/** Response for GetWidgetBatch. */ +export interface IGetWidgetBatchResponse { + /** The widget results. */ + results?: IServiceResult[]; +} + +/** Request for MirrorFields. */ +export interface IMirrorFieldsRequest { + field?: IAny; + + matrix?: number[][][]; +} + +/** Response for MirrorFields. */ +export interface IMirrorFieldsResponse { + field?: IAny; + + matrix?: number[][][]; +} + +/** Request for CheckQuery. */ +export interface ICheckQueryRequest { + string?: string; + + boolean?: boolean; + + double?: number; + + int32?: number; + + int64?: number; + + decimal?: number; + + enum?: Answer; + + datetime?: string; +} + +/** Response for CheckQuery. */ +export interface ICheckQueryResponse { +} + +/** Request for CheckPath. */ +export interface ICheckPathRequest { + string?: string; + + boolean?: boolean; + + double?: number; + + int32?: number; + + int64?: number; + + decimal?: number; + + enum?: Answer; + + datetime?: string; +} + +/** Response for CheckPath. */ +export interface ICheckPathResponse { +} + +/** Request for MirrorHeaders. */ +export interface IMirrorHeadersRequest { + string?: string; + + boolean?: boolean; + + double?: number; + + int32?: number; + + int64?: number; + + decimal?: number; + + enum?: Answer; + + datetime?: string; +} + +/** Response for MirrorHeaders. */ +export interface IMirrorHeadersResponse { + string?: string; + + boolean?: boolean; + + double?: number; + + int32?: number; + + int64?: number; + + decimal?: number; + + enum?: Answer; + + datetime?: string; +} + +/** Request for Mixed. */ +export interface IMixedRequest { + path?: string; + + query?: string; + + header?: string; + + normal?: string; +} + +/** Response for Mixed. */ +export interface IMixedResponse { + header?: string; + + normal?: string; + + body?: { [name: string]: any }; + + empty?: boolean; +} + +/** Request for Required. */ +export interface IRequiredRequest { + query?: string; + + normal?: string; + + widget?: IWidget; + + widgets?: IWidget[]; + + widgetMatrix?: IWidget[][]; + + widgetResult?: IServiceResult; + + widgetResults?: IServiceResult[]; + + widgetMap?: { [name: string]: IWidget }; + + hasWidget?: IHasWidget; + + point?: number[]; +} + +/** Response for Required. */ +export interface IRequiredResponse { + normal?: string; +} + +/** Request for MirrorBytes. */ +export interface IMirrorBytesRequest { + content?: string; + + type?: string; +} + +/** Response for MirrorBytes. */ +export interface IMirrorBytesResponse { + content?: string; + + type?: string; +} + +/** Request for MirrorText. */ +export interface IMirrorTextRequest { + content?: string; + + type?: string; +} + +/** Response for MirrorText. */ +export interface IMirrorTextResponse { + content?: string; + + type?: string; +} + +/** Request for BodyTypes. */ +export interface IBodyTypesRequest { + content?: string; +} + +/** Response for BodyTypes. */ +export interface IBodyTypesResponse { + content?: string; +} + +/** A widget. */ +export interface IWidget { + /** A unique identifier for the widget. */ + id?: number; + + /** The name of the widget. */ + name?: string; +} + +export interface IAny { + string?: string; + + boolean?: boolean; + + double?: number; + + int32?: number; + + int64?: number; + + decimal?: number; + + datetime?: string; + + bytes?: string; + + object?: { [name: string]: any }; + + error?: IServiceError; + + data?: IAny; + + enum?: Answer; + + array?: IAnyArray; + + map?: IAnyMap; + + result?: IAnyResult; + + nullable?: IAnyNullable; +} + +export interface IAnyArray { + string?: string[]; + + boolean?: boolean[]; + + double?: number[]; + + int32?: number[]; + + int64?: number[]; + + decimal?: number[]; + + datetime?: string[]; + + bytes?: string[]; + + object?: { [name: string]: any }[]; + + error?: IServiceError[]; + + data?: IAny[]; + + enum?: Answer[]; + + array?: number[][]; + + map?: { [name: string]: number }[]; + + result?: IServiceResult[]; + + nullable?: (number | null)[]; +} + +export interface IAnyMap { + string?: { [name: string]: string }; + + boolean?: { [name: string]: boolean }; + + double?: { [name: string]: number }; + + int32?: { [name: string]: number }; + + int64?: { [name: string]: number }; + + decimal?: { [name: string]: number }; + + datetime?: { [name: string]: string }; + + bytes?: { [name: string]: string }; + + object?: { [name: string]: { [name: string]: any } }; + + error?: { [name: string]: IServiceError }; + + data?: { [name: string]: IAny }; + + enum?: { [name: string]: Answer }; + + array?: { [name: string]: number[] }; + + map?: { [name: string]: { [name: string]: number } }; + + result?: { [name: string]: IServiceResult }; + + nullable?: { [name: string]: (number | null) }; +} + +export interface IAnyResult { + string?: IServiceResult; + + boolean?: IServiceResult; + + double?: IServiceResult; + + int32?: IServiceResult; + + int64?: IServiceResult; + + decimal?: IServiceResult; + + datetime?: IServiceResult; + + bytes?: IServiceResult; + + object?: IServiceResult<{ [name: string]: any }>; + + error?: IServiceResult; + + data?: IServiceResult; + + enum?: IServiceResult; + + array?: IServiceResult; + + map?: IServiceResult<{ [name: string]: number }>; + + result?: IServiceResult>; + + nullable?: IServiceResult<(number | null)>; +} + +export interface IAnyNullable { + string?: (string | null); + + boolean?: (boolean | null); + + double?: (number | null); + + int32?: (number | null); + + int64?: (number | null); + + decimal?: (number | null); + + datetime?: (string | null); + + bytes?: (string | null); + + object?: ({ [name: string]: any } | null); + + error?: (IServiceError | null); + + data?: (IAny | null); + + enum?: (Answer | null); + + array?: (number[] | null); + + map?: ({ [name: string]: number } | null); + + result?: (IServiceResult | null); +} + +export interface IHasWidget { + widget?: IWidget; +} + +/** One of three answers. */ +export enum Answer { + /** Affirmative. */ + yes = 'yes', + + /** Negative. */ + no = 'no', + + /** Unknown. */ + maybe = 'maybe', +} + +/** Custom errors. */ +export enum ApiErrors { + /** The user is not an administrator. */ + NotAdmin = 'NotAdmin', + + /** I'm "too" 😄! */ + TooHappy = 'TooHappy', +} diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 620b763..2d873cd 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -110,76 +110,7 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) foreach (var import in externImports.GroupBy(x => x.Module)) WriteImports(code, import.Select(x => $"{x.Name}{(x.Name != x.Alias ? $" as {x.Alias}" : "")}").ToArray(), import.Key); - code.WriteLine(); - WriteJsDoc(code, service); - typeNames.Add($"I{capModuleName}"); - using (code.Block($"export interface I{capModuleName} {{", "}")) - { - foreach (var httpMethodInfo in httpServiceInfo.Methods) - { - var methodName = httpMethodInfo.ServiceMethod.Name; - var capMethodName = CodeGenUtility.Capitalize(methodName); - code.WriteLineSkipOnce(); - WriteJsDoc(code, httpMethodInfo.ServiceMethod); - code.WriteLine($"{methodName}(request: I{capMethodName}Request, context?: unknown): Promise>;"); - } - } - - foreach (var methodInfo in service.Methods) - { - var requestDtoName = $"{CodeGenUtility.Capitalize(methodInfo.Name)}Request"; - typeNames.Add($"I{requestDtoName}"); - WriteDto(code, new ServiceDtoInfo( - name: requestDtoName, - fields: methodInfo.RequestFields, - summary: $"Request for {CodeGenUtility.Capitalize(methodInfo.Name)}."), service); - - var responseDtoName = $"{CodeGenUtility.Capitalize(methodInfo.Name)}Response"; - typeNames.Add($"I{responseDtoName}"); - WriteDto(code, new ServiceDtoInfo( - name: responseDtoName, - fields: methodInfo.ResponseFields, - summary: $"Response for {CodeGenUtility.Capitalize(methodInfo.Name)}."), service); - } - - foreach (var dtoInfo in service.Dtos) - { - typeNames.Add($"I{dtoInfo.Name}"); - WriteDto(code, dtoInfo, service); - } - - foreach (var enumInfo in service.Enums) - { - typeNames.Add(enumInfo.Name); - code.WriteLine(); - WriteJsDoc(code, enumInfo); - using (code.Block($"export enum {enumInfo.Name} {{", "}")) - { - foreach (var value in enumInfo.Values) - { - code.WriteLineSkipOnce(); - WriteJsDoc(code, value); - code.WriteLine($"{value.Name} = '{value.Name}',"); - } - } - } - - foreach (var errorSetInfo in service.ErrorSets) - { - typeNames.Add(errorSetInfo.Name); - code.WriteLine(); - WriteJsDoc(code, errorSetInfo); - using (code.Block($"export enum {errorSetInfo.Name} {{", "}")) - { - foreach (var error in errorSetInfo.Errors) - { - code.WriteLineSkipOnce(); - WriteJsDoc(code, error); - code.WriteLine($"{error.Name} = '{error.Name}',"); - } - } - } - + typeNames.AddRange(WriteTypes(code, httpServiceInfo)); code.WriteLine(); })); } @@ -630,14 +561,12 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) WriteImports(code, fastifyImports, "fastify"); var facilityImports = new List(); - if (TypeScript) - facilityImports.Add("IServiceResult"); - WriteImports(code, facilityImports, "facility-core"); if (TypeScript) { - WriteImports(code, typeNames, $"../{CodeGenUtility.Uncapitalize(moduleName)}Types"); - code.WriteLine($"export * from '../{CodeGenUtility.Uncapitalize(moduleName)}Types';"); + facilityImports.Add("IServiceResult"); + facilityImports.Add("IServiceError"); } + WriteImports(code, facilityImports, "facility-core"); // TODO: export this from facility-core code.WriteLine(); @@ -826,6 +755,9 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) } } } + + if (TypeScript) + WriteTypes(code, httpServiceInfo); })); } @@ -1032,6 +964,85 @@ private static void WriteImports(CodeWriter code, IReadOnlyList imports, code.WriteLine($"import {{ {string.Join(", ", imports)} }} from '{from}';"); } + private List WriteTypes(CodeWriter code, HttpServiceInfo httpServiceInfo) + { + var typeNames = new List(); + var service = httpServiceInfo.Service; + code.WriteLine(); + WriteJsDoc(code, service); + + var capModuleName = CodeGenUtility.Capitalize(ModuleName ?? service.Name); + typeNames.Add($"I{capModuleName}"); + using (code.Block($"export interface I{capModuleName} {{", "}")) + { + foreach (var httpMethodInfo in httpServiceInfo.Methods) + { + var methodName = httpMethodInfo.ServiceMethod.Name; + var capMethodName = CodeGenUtility.Capitalize(methodName); + code.WriteLineSkipOnce(); + WriteJsDoc(code, httpMethodInfo.ServiceMethod); + code.WriteLine($"{methodName}(request: I{capMethodName}Request, context?: unknown): Promise>;"); + } + } + + foreach (var methodInfo in service.Methods) + { + var requestDtoName = $"{CodeGenUtility.Capitalize(methodInfo.Name)}Request"; + typeNames.Add($"I{requestDtoName}"); + WriteDto(code, new ServiceDtoInfo( + name: requestDtoName, + fields: methodInfo.RequestFields, + summary: $"Request for {CodeGenUtility.Capitalize(methodInfo.Name)}."), service); + + var responseDtoName = $"{CodeGenUtility.Capitalize(methodInfo.Name)}Response"; + typeNames.Add($"I{responseDtoName}"); + WriteDto(code, new ServiceDtoInfo( + name: responseDtoName, + fields: methodInfo.ResponseFields, + summary: $"Response for {CodeGenUtility.Capitalize(methodInfo.Name)}."), service); + } + + foreach (var dtoInfo in service.Dtos) + { + typeNames.Add($"I{dtoInfo.Name}"); + WriteDto(code, dtoInfo, service); + } + + foreach (var enumInfo in service.Enums) + { + typeNames.Add(enumInfo.Name); + code.WriteLine(); + WriteJsDoc(code, enumInfo); + using (code.Block($"export enum {enumInfo.Name} {{", "}")) + { + foreach (var value in enumInfo.Values) + { + code.WriteLineSkipOnce(); + WriteJsDoc(code, value); + code.WriteLine($"{value.Name} = '{value.Name}',"); + } + } + } + + foreach (var errorSetInfo in service.ErrorSets) + { + typeNames.Add(errorSetInfo.Name); + code.WriteLine(); + WriteJsDoc(code, errorSetInfo); + using (code.Block($"export enum {errorSetInfo.Name} {{", "}")) + { + foreach (var error in errorSetInfo.Errors) + { + code.WriteLineSkipOnce(); + WriteJsDoc(code, error); + code.WriteLine($"{error.Name} = '{error.Name}',"); + } + } + } + + return typeNames; + } + private static bool FieldUsesKind(ServiceInfo service, ServiceFieldInfo field, ServiceTypeKind kind) { var type = service.GetFieldType(field)!; From 862cc03ff43ab1b2ddcf0d2fd1297899399cbb14 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 16 Dec 2023 15:03:07 -0800 Subject: [PATCH 08/19] Consolidate duplicate code into local functions. --- .../JavaScriptGenerator.cs | 120 ++++++++---------- 1 file changed, 50 insertions(+), 70 deletions(-) diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 2d873cd..8ca97df 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -379,44 +379,11 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) code.WriteLine($"export * from './{CodeGenUtility.Uncapitalize(moduleName)}Types';"); } - // TODO: export this from facility-core code.WriteLine(); - using (code.Block("const standardErrorCodes" + IfTypeScript(": { [code: string]: number }") + " = {", "};")) - { - code.WriteLine("'NotModified': 304,"); - code.WriteLine("'InvalidRequest': 400,"); - code.WriteLine("'NotAuthenticated': 401,"); - code.WriteLine("'NotAuthorized': 403,"); - code.WriteLine("'NotFound': 404,"); - code.WriteLine("'Conflict': 409,"); - code.WriteLine("'RequestTooLarge': 413,"); - code.WriteLine("'TooManyRequests': 429,"); - code.WriteLine("'InternalError': 500,"); - code.WriteLine("'ServiceUnavailable': 503,"); - - foreach (var errorSetInfo in httpServiceInfo.ErrorSets) - { - foreach (var error in errorSetInfo.Errors) - { - code.WriteLine($"'{error.ServiceError.Name}': {(int) error.StatusCode},"); - } - } - } + WriteStandardErrorCodesVariable("standardErrorCodes", code, httpServiceInfo.ErrorSets); - // TODO: export this from facility-core? code.WriteLine(); - using (code.Block("function parseBoolean(value" + IfTypeScript(": string | undefined") + ") {", "}")) - { - using (code.Block("if (typeof value === 'string') {", "}")) - { - code.WriteLine("const lowerValue = value.toLowerCase();"); - using (code.Block("if (lowerValue === 'true') {", "}")) - code.WriteLine("return true;"); - using (code.Block("if (lowerValue === 'false') {", "}")) - code.WriteLine("return false;"); - } - code.WriteLine("return undefined;"); - } + WriteParseBooleanFunction("parseBoolean", code); code.WriteLine(); using (code.Block("export function createApp(service" + IfTypeScript($": I{capModuleName}") + ")" + IfTypeScript(": express.Application") + " {", "}")) @@ -568,44 +535,11 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) } WriteImports(code, facilityImports, "facility-core"); - // TODO: export this from facility-core code.WriteLine(); - using (code.Block("const standardErrorCodes" + IfTypeScript(": { [code: string]: number }") + " = {", "};")) - { - code.WriteLine("'NotModified': 304,"); - code.WriteLine("'InvalidRequest': 400,"); - code.WriteLine("'NotAuthenticated': 401,"); - code.WriteLine("'NotAuthorized': 403,"); - code.WriteLine("'NotFound': 404,"); - code.WriteLine("'Conflict': 409,"); - code.WriteLine("'RequestTooLarge': 413,"); - code.WriteLine("'TooManyRequests': 429,"); - code.WriteLine("'InternalError': 500,"); - code.WriteLine("'ServiceUnavailable': 503,"); - - foreach (var errorSetInfo in httpServiceInfo.ErrorSets) - { - foreach (var error in errorSetInfo.Errors) - { - code.WriteLine($"'{error.ServiceError.Name}': {(int) error.StatusCode},"); - } - } - } + WriteStandardErrorCodesVariable("standardErrorCodes", code, httpServiceInfo.ErrorSets); - // TODO: export this from facility-core? code.WriteLine(); - using (code.Block("function parseBoolean(value" + IfTypeScript(": string | undefined") + ") {", "}")) - { - using (code.Block("if (typeof value === 'string') {", "}")) - { - code.WriteLine("const lowerValue = value.toLowerCase();"); - using (code.Block("if (lowerValue === 'true') {", "}")) - code.WriteLine("return true;"); - using (code.Block("if (lowerValue === 'false') {", "}")) - code.WriteLine("return false;"); - } - code.WriteLine("return undefined;"); - } + WriteParseBooleanFunction("parseBoolean", code); if (TypeScript) { @@ -964,6 +898,52 @@ private static void WriteImports(CodeWriter code, IReadOnlyList imports, code.WriteLine($"import {{ {string.Join(", ", imports)} }} from '{from}';"); } + private void WriteStandardErrorCodesVariable(string name, CodeWriter code, IEnumerable? errorSets) + { + // TODO: export this from facility-core + using (code.Block($"const {name}" + IfTypeScript(": { [code: string]: number }") + " = {", "};")) + { + code.WriteLine("'NotModified': 304,"); + code.WriteLine("'InvalidRequest': 400,"); + code.WriteLine("'NotAuthenticated': 401,"); + code.WriteLine("'NotAuthorized': 403,"); + code.WriteLine("'NotFound': 404,"); + code.WriteLine("'Conflict': 409,"); + code.WriteLine("'RequestTooLarge': 413,"); + code.WriteLine("'TooManyRequests': 429,"); + code.WriteLine("'InternalError': 500,"); + code.WriteLine("'ServiceUnavailable': 503,"); + + if (errorSets is not null) + { + foreach (var errorSetInfo in errorSets) + { + foreach (var error in errorSetInfo.Errors) + { + code.WriteLine($"'{error.ServiceError.Name}': {(int) error.StatusCode},"); + } + } + } + } + } + + private void WriteParseBooleanFunction(string name, CodeWriter code) + { + // TODO: export this from facility-core + using (code.Block($"function {name}(value" + IfTypeScript(": string | undefined") + ") {", "}")) + { + using (code.Block("if (typeof value === 'string') {", "}")) + { + code.WriteLine("const lowerValue = value.toLowerCase();"); + using (code.Block("if (lowerValue === 'true') {", "}")) + code.WriteLine("return true;"); + using (code.Block("if (lowerValue === 'false') {", "}")) + code.WriteLine("return false;"); + } + code.WriteLine("return undefined;"); + } + } + private List WriteTypes(CodeWriter code, HttpServiceInfo httpServiceInfo) { var typeNames = new List(); From 9175554cbc812c2685acbf1de925f4477b31365c Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 16 Dec 2023 15:34:21 -0800 Subject: [PATCH 09/19] Delete handRolledPlugin.ts No longer needed! --- .../ts/src/fastify/handRolledPlugin.ts | 562 ------------------ 1 file changed, 562 deletions(-) delete mode 100644 conformance/ts/src/fastify/handRolledPlugin.ts diff --git a/conformance/ts/src/fastify/handRolledPlugin.ts b/conformance/ts/src/fastify/handRolledPlugin.ts deleted file mode 100644 index 7120000..0000000 --- a/conformance/ts/src/fastify/handRolledPlugin.ts +++ /dev/null @@ -1,562 +0,0 @@ -import { FastifyPluginAsync } from "fastify"; -import type { IConformanceApi, IGetApiInfoRequest, IGetWidgetsRequest, IGetWidgetRequest, IGetWidgetsResponse, ICreateWidgetRequest, IWidget, IDeleteWidgetRequest, IGetWidgetBatchRequest, IMirrorFieldsRequest, ICheckQueryRequest, Answer, ICheckPathRequest, IMirrorHeadersRequest, IMixedRequest, IRequiredRequest } from "../conformanceApi"; - -const standardErrorCodes: { [code: string]: number } = { - NotModified: 304, - InvalidRequest: 400, - NotAuthenticated: 401, - NotAuthorized: 403, - NotFound: 404, - Conflict: 409, - RequestTooLarge: 413, - TooManyRequests: 429, - InternalError: 500, - ServiceUnavailable: 503, - NotAdmin: 403, -}; - -function parseBoolean(value: string | undefined) { - if (typeof value === 'string') { - const lowerValue = value.toLowerCase(); - if (lowerValue === 'true') { - return true; - } - if (lowerValue === 'false') { - return false; - } - } - return undefined; -} - -export type ConformanceApiPluginOptions = { - api: IConformanceApi; -} - -export const handRolledPlugin: FastifyPluginAsync = async (fastify, opts) => { - const api = opts.api; - fastify.route({ - url: "/", - method: "GET", - handler: async function (req, reply) { - const request: IGetApiInfoRequest = {}; - - const result = await api.getApiInfo(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - reply.send(result.value); - } - }, - }); - - fastify.route({ - url: "/widgets", - method: "GET", - handler: async (req, reply) => { - const request: IGetWidgetsRequest = {}; - - const query = req.query as Record; - if (typeof query["q"] === "string") { - request.query = query["q"]; - } - - const result = await api.getWidgets(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - reply.status(200).send({ - widgets: result.value.widgets, - } satisfies IGetWidgetsResponse); - } - - throw new Error("Result must have an error or value."); - }, - }); - - fastify.route({ - url: "/widgets", - method: "POST", - handler: async (req, reply) => { - const request: ICreateWidgetRequest = {}; - request.widget = req.body as IWidget; - - const result = await api.createWidget(request); - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - if (result.value.url != null) { - reply.header("Location", result.value.url); - } - - if (result.value.eTag != null) { - reply.header("eTag", result.value.eTag); - } - - if (result.value.widget) { - reply.status(201).send(result.value.widget); - } - - return; - } - - throw new Error("Result must have an error or value."); - }, - }); - - fastify.route({ - url: "/widgets/:id", - method: "GET", - handler: async (req, reply) => { - const request: IGetWidgetRequest = {}; - - const params = req.params as Record; - if (typeof params.id === "string") { - request.id = parseInt(params.id); - } - request.ifNotETag = req.headers["if-none-match"] as string; - - const result = await api.getWidget(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - if (result.value.eTag != null) { - reply.header("eTag", result.value.eTag); - } - - if (result.value.widget) { - reply.status(200).send(result.value.widget); - return; - } - - if (result.value.notModified) { - reply.status(304); - return; - } - } - - throw new Error("Result must have an error or value."); - }, - }); - - // create the delete route here - fastify.route({ - url: "/widgets/:id", - method: "DELETE", - handler: async (req, reply) => { - const request: IDeleteWidgetRequest = {}; - - const params = req.params as Record; - if (typeof params.id === "string") { - request.id = parseInt(params.id); - } - request.ifETag = req.headers['if-match'] as string; - - const result = await api.deleteWidget(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - if (result.value.notFound) { - reply.status(404); - return; - } - - if (result.value.conflict) { - reply.status(409); - return; - } - - reply.status(204).send({}); - return; - } - - throw new Error("Result must have an error or value."); - }, - }); - - fastify.route({ - url: "/widgets/get", - method: "POST", - handler: async (req, reply) => { - const request: IGetWidgetBatchRequest = {}; - request.ids = req.body as number[]; - - const result = await api.getWidgetBatch(request); - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - if (result.value.results) { - reply.status(200).send(result.value.results); - return; - } - } - - throw new Error("Result must have an error or value."); - }, - }); - - fastify.route({ - url: "/mirrorFields", - method: "POST", - handler: async (req, reply) => { - const request: IMirrorFieldsRequest = {}; - request.field = (req.body as IMirrorFieldsRequest).field; - request.matrix = (req.body as IMirrorFieldsRequest).matrix; - - const result = await api.mirrorFields(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - reply.status(200).send(result.value); - return; - } - - throw new Error("Result must have an error or value."); - }, - }); - - fastify.route({ - url: "/checkQuery", - method: "GET", - handler: async (req, reply) => { - const request: ICheckQueryRequest = {}; - const query = req.query as Record; - - if (typeof query["string"] === "string") { - request.string = query["string"]; - } - if (typeof query["boolean"] === "string") { - request.boolean = parseBoolean(query["boolean"]); - } - if (typeof query["double"] === "string") { - request.double = parseFloat(query["double"]); - } - if (typeof query["int32"] === "string") { - request.int32 = parseInt(query["int32"]); - } - if (typeof query["int64"] === "string") { - request.int64 = parseInt(query["int64"]); - } - if (typeof query["decimal"] === "string") { - request.decimal = parseFloat(query["decimal"]); - } - if (typeof query["enum"] === "string") { - request.enum = query["enum"] as Answer; - } - if (typeof query["datetime"] === "string") { - request.datetime = query["datetime"]; - } - - const result = await api.checkQuery(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - reply.status(200).send(result.value); - } - - throw new Error("Result must have an error or value."); - }, - }); - - fastify.route({ - url: "/checkPath/:string/:boolean/:double/:int32/:int64/:decimal/:enum/:datetime", - method: "GET", - handler: async (req, reply) => { - const request: ICheckPathRequest = {}; - const params = req.params as Record; - - if (typeof params["string"] === "string") { - request.string = params["string"]; - } - if (typeof params["boolean"] === "string") { - request.boolean = parseBoolean(params["boolean"]); - } - if (typeof params["double"] === "string") { - request.double = parseFloat(params["double"]); - } - if (typeof params["int32"] === "string") { - request.int32 = parseInt(params["int32"]); - } - if (typeof params["int64"] === "string") { - request.int64 = parseInt(params["int64"]); - } - if (typeof params["decimal"] === "string") { - request.decimal = parseFloat(params["decimal"]); - } - if (typeof params["enum"] === "string") { - request.enum = params["enum"] as Answer; - } - if (typeof params["datetime"] === "string") { - request.datetime = params["datetime"]; - } - - const result = await api.checkPath(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - - if (result.value) { - reply.status(200).send(result.value); - } - - throw new Error("Result must have an error or value."); - } - }); - - fastify.route({ - url: "/mirrorHeaders", - method: "GET", - handler: async (req, reply) => { - const request: IMirrorHeadersRequest = {}; - const headers = req.headers as Record; - - if (typeof headers["string"] === "string") { - request.string = headers["string"]; - } - if (typeof headers["boolean"] === "string") { - request.boolean = parseBoolean(headers["boolean"]); - } - if (typeof headers["double"] === "string") { - request.double = parseFloat(headers["double"]); - } - if (typeof headers["int32"] === "string") { - request.int32 = parseInt(headers["int32"]); - } - if (typeof headers["int64"] === "string") { - request.int64 = parseInt(headers["int64"]); - } - if (typeof headers["decimal"] === "string") { - request.decimal = parseFloat(headers["decimal"]); - } - if (typeof headers["enum"] === "string") { - request.enum = headers["enum"] as Answer; - } - if (typeof headers["datetime"] === "string") { - request.datetime = headers["datetime"]; - } - - const result = await api.mirrorHeaders(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - return; - } - if (result.value) { - if (result.value.string != null) { - reply.header("string", result.value.string); - } - if (result.value.boolean != null) { - reply.header("boolean", result.value.boolean.toString()); - } - if (result.value.double != null) { - reply.header("double", result.value.double.toString()); - } - if (result.value.int32 != null) { - reply.header("int32", result.value.int32.toString()); - } - if (result.value.int64 != null) { - reply.header("int64", result.value.int64.toString()); - } - if (result.value.decimal != null) { - reply.header("decimal", result.value.decimal.toString()); - } - if (result.value.enum != null) { - reply.header("enum", result.value.enum); - } - if (result.value.datetime != null) { - reply.header("datetime", result.value.datetime); - } - - reply.status(200); - return; - } - - throw new Error("Result must have an error or value."); - } - }); - - // implement the mixed route here. - // pull path from params, query from query, header from headers, and normal from body - fastify.route({ - url: "/mixed/:path", - method: "POST", - handler: async (req, reply) => { - const request: IMixedRequest = {}; - const params = req.params as Record; - const query = req.query as Record; - const headers = req.headers as Record; - const body = req.body as Record; - - if (typeof params["path"] === "string") { - request.path = params["path"]; - } - if (typeof query["query"] === "string") { - request.query = query["query"]; - } - if (typeof headers["header"] === "string") { - request.header = headers["header"]; - } - if (typeof body["normal"] === "string") { - request.normal = body["normal"]; - } - - const result = await api.mixed(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - } - - if (result.value) { - if (result.value.header != null) { - reply.header("header", result.value.header); - } - - if (result.value.body != null) { - reply.status(202).send(result.value.body); - return; - } - - if (result.value.empty) { - reply.status(204); - return; - } - - - reply.status(200).send(result.value); - return; - } - - throw new Error("Result must have an error or value."); - } - }); - - fastify.route({ - url: "/required", - method: "POST", - handler: async (req, reply) => { - const request: IRequiredRequest = {}; - const body = req.body as Record; - const query = req.query as Record; - - if (typeof query["query"] === "string") { - request.query = query["query"]; - } - if (typeof body["normal"] === "string") { - request.normal = body["normal"]; - } - if (typeof body["widget"]) { - request.widget = body["widget"] as never; - } - if (typeof body["widgets"]) { - request.widgets = body["widgets"] as never; - } - if (typeof body["widgetMatrix"]) { - request.widgetMatrix = body["widgetMatrix"] as never; - } - if (typeof body["widgetResult"]) { - request.widgetResult = body["widgetResult"] as never; - } - if (typeof body["widgetResults"]) { - request.widgetResults = body["widgetResults"] as never; - } - if (typeof body["widgetMap"]) { - request.widgetMap = body["widgetMap"] as never; - } - if (typeof body["hasWidget"]) { - request.hasWidget = body["hasWidget"] as never; - } - if (typeof body["point"]) { - request.point = body["point"] as never; - } - - const result = await api.required(request); - - if (result.error) { - const status = result.error.code && standardErrorCodes[result.error.code]; - reply.status(status || 500).send(result.error); - } - - if (result.value) { - reply.status(200).send(result.value); - return; - } - - throw new Error("Result must have an error or value."); - }, - }); - - fastify.route({ - url: "/mirrorBytes", - method: "POST", - handler: async (req, reply) => { - reply.status(500).send({ - code: "NotImplemented", - message: "Javascript HttpClientUtility doesn't support non-json responses currently.", - }); - } - }); - - fastify.route({ - url: "/mirrorText", - method: "POST", - handler: async (req, reply) => { - reply.status(500).send({ - code: "NotImplemented", - message: "Javascript HttpClientUtility doesn't support non-json responses currently.", - }); - } - }); - - fastify.route({ - url: "/bodyTypes", - method: "POST", - handler: async (req, reply) => { - reply.status(500).send({ - code: "NotImplemented", - message: "Need to support FSD attributes like `[http(from: body, type: \"text/x-input\")]`", - }); - } - }); -} From a7a77ef5cfad818b93f3fe21e7724ed2b36b78b1 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 16 Dec 2023 15:59:10 -0800 Subject: [PATCH 10/19] Clean up plain js support. --- conformance/js/src/conformanceApi.js | 589 ++++++++++++++++++ conformance/js/src/fastify/app.js | 14 + .../js/src/fastify/conformanceApiPlugin.js | 529 ++++++++++++++++ conformance/package.json | 3 +- conformance/tsconfig.json | 4 +- example/js/exampleApiServer.js | 2 +- .../JavaScriptGenerator.cs | 4 +- tools/Build/Build.cs | 1 + 8 files changed, 1141 insertions(+), 5 deletions(-) create mode 100644 conformance/js/src/conformanceApi.js create mode 100644 conformance/js/src/fastify/app.js create mode 100644 conformance/js/src/fastify/conformanceApiPlugin.js diff --git a/conformance/js/src/conformanceApi.js b/conformance/js/src/conformanceApi.js new file mode 100644 index 0000000..c42987f --- /dev/null +++ b/conformance/js/src/conformanceApi.js @@ -0,0 +1,589 @@ +// DO NOT EDIT: generated by fsdgenjs +/* eslint-disable */ +'use strict'; + +import { HttpClientUtility } from 'facility-core'; + +/** Provides access to ConformanceApi over HTTP via fetch. */ +export function createHttpClient({ fetch, baseUri }) { + return new ConformanceApiHttpClient(fetch, baseUri); +} + +const { fetchResponse, createResponseError, createRequiredRequestFieldError } = HttpClientUtility; + +function parseBoolean(value) { + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + if (lowerValue === 'true') { + return true; + } + if (lowerValue === 'false') { + return false; + } + } + return undefined; +} + +class ConformanceApiHttpClient { + constructor(fetch, baseUri) { + if (typeof fetch !== 'function') { + throw new TypeError('fetch must be a function.'); + } + if (typeof baseUri === 'undefined') { + baseUri = ''; + } + if (/[^\/]$/.test(baseUri)) { + baseUri += '/'; + } + this._fetch = fetch; + this._baseUri = baseUri; + } + + /** Gets API information. */ + getApiInfo(request, context) { + const uri = ''; + const fetchRequest = { + method: 'GET', + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = result.json; + } + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + /** Gets widgets. */ + getWidgets(request, context) { + let uri = 'widgets'; + const query = []; + request.query == null || query.push('q=' + encodeURIComponent(request.query)); + if (query.length) { + uri = uri + '?' + query.join('&'); + } + const fetchRequest = { + method: 'GET', + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = result.json; + } + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + /** Creates a new widget. */ + createWidget(request, context) { + const uri = 'widgets'; + if (!request.widget) { + return Promise.resolve(createRequiredRequestFieldError('widget')); + } + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request.widget) + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 201) { + if (result.json) { + value = { widget: result.json }; + } + } + if (!value) { + return createResponseError(status, result.json); + } + let headerValue; + headerValue = result.response.headers.get('Location'); + if (headerValue != null) { + value.url = headerValue; + } + headerValue = result.response.headers.get('eTag'); + if (headerValue != null) { + value.eTag = headerValue; + } + return { value: value }; + }); + } + + /** Gets the specified widget. */ + getWidget(request, context) { + const uriPartId = request.id != null && request.id.toString(); + if (!uriPartId) { + return Promise.resolve(createRequiredRequestFieldError('id')); + } + const uri = `widgets/${uriPartId}`; + const fetchRequest = { + method: 'GET', + headers: {}, + }; + if (request.ifNotETag != null) { + fetchRequest.headers['If-None-Match'] = request.ifNotETag; + } + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = { widget: result.json }; + } + } + else if (status === 304) { + value = { notModified: true }; + } + if (!value) { + return createResponseError(status, result.json); + } + let headerValue; + headerValue = result.response.headers.get('eTag'); + if (headerValue != null) { + value.eTag = headerValue; + } + return { value: value }; + }); + } + + /** Deletes the specified widget. */ + deleteWidget(request, context) { + const uriPartId = request.id != null && request.id.toString(); + if (!uriPartId) { + return Promise.resolve(createRequiredRequestFieldError('id')); + } + const uri = `widgets/${uriPartId}`; + const fetchRequest = { + method: 'DELETE', + headers: {}, + }; + if (request.ifETag != null) { + fetchRequest.headers['If-Match'] = request.ifETag; + } + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 204) { + value = {}; + } + else if (status === 404) { + value = { notFound: true }; + } + else if (status === 409) { + value = { conflict: true }; + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + /** Gets the specified widgets. */ + getWidgetBatch(request, context) { + const uri = 'widgets/get'; + if (!request.ids) { + return Promise.resolve(createRequiredRequestFieldError('ids')); + } + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request.ids) + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = { results: result.json }; + } + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + mirrorFields(request, context) { + const uri = 'mirrorFields'; + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request) + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = result.json; + } + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + checkQuery(request, context) { + let uri = 'checkQuery'; + const query = []; + request.string == null || query.push('string=' + encodeURIComponent(request.string)); + request.boolean == null || query.push('boolean=' + request.boolean.toString()); + request.double == null || query.push('double=' + encodeURIComponent(request.double.toString())); + request.int32 == null || query.push('int32=' + request.int32.toString()); + request.int64 == null || query.push('int64=' + request.int64.toString()); + request.decimal == null || query.push('decimal=' + request.decimal.toString()); + request.enum == null || query.push('enum=' + request.enum); + request.datetime == null || query.push('datetime=' + encodeURIComponent(request.datetime)); + if (query.length) { + uri = uri + '?' + query.join('&'); + } + const fetchRequest = { + method: 'GET', + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + value = {}; + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + checkPath(request, context) { + const uriPartString = request.string != null && encodeURIComponent(request.string); + if (!uriPartString) { + return Promise.resolve(createRequiredRequestFieldError('string')); + } + const uriPartBoolean = request.boolean != null && request.boolean.toString(); + if (!uriPartBoolean) { + return Promise.resolve(createRequiredRequestFieldError('boolean')); + } + const uriPartDouble = request.double != null && encodeURIComponent(request.double.toString()); + if (!uriPartDouble) { + return Promise.resolve(createRequiredRequestFieldError('double')); + } + const uriPartInt32 = request.int32 != null && request.int32.toString(); + if (!uriPartInt32) { + return Promise.resolve(createRequiredRequestFieldError('int32')); + } + const uriPartInt64 = request.int64 != null && request.int64.toString(); + if (!uriPartInt64) { + return Promise.resolve(createRequiredRequestFieldError('int64')); + } + const uriPartDecimal = request.decimal != null && request.decimal.toString(); + if (!uriPartDecimal) { + return Promise.resolve(createRequiredRequestFieldError('decimal')); + } + const uriPartEnum = request.enum != null && request.enum; + if (!uriPartEnum) { + return Promise.resolve(createRequiredRequestFieldError('enum')); + } + const uriPartDatetime = request.datetime != null && encodeURIComponent(request.datetime); + if (!uriPartDatetime) { + return Promise.resolve(createRequiredRequestFieldError('datetime')); + } + const uri = `checkPath/${uriPartString}/${uriPartBoolean}/${uriPartDouble}/${uriPartInt32}/${uriPartInt64}/${uriPartDecimal}/${uriPartEnum}/${uriPartDatetime}`; + const fetchRequest = { + method: 'GET', + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + value = {}; + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + mirrorHeaders(request, context) { + const uri = 'mirrorHeaders'; + const fetchRequest = { + method: 'GET', + headers: {}, + }; + if (request.string != null) { + fetchRequest.headers['string'] = request.string; + } + if (request.boolean != null) { + fetchRequest.headers['boolean'] = request.boolean.toString(); + } + if (request.double != null) { + fetchRequest.headers['double'] = request.double.toString(); + } + if (request.int32 != null) { + fetchRequest.headers['int32'] = request.int32.toString(); + } + if (request.int64 != null) { + fetchRequest.headers['int64'] = request.int64.toString(); + } + if (request.decimal != null) { + fetchRequest.headers['decimal'] = request.decimal.toString(); + } + if (request.enum != null) { + fetchRequest.headers['enum'] = request.enum; + } + if (request.datetime != null) { + fetchRequest.headers['datetime'] = request.datetime; + } + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + value = {}; + } + if (!value) { + return createResponseError(status, result.json); + } + let headerValue; + headerValue = result.response.headers.get('string'); + if (headerValue != null) { + value.string = headerValue; + } + headerValue = result.response.headers.get('boolean'); + if (headerValue != null) { + value.boolean = parseBoolean(headerValue); + } + headerValue = result.response.headers.get('double'); + if (headerValue != null) { + value.double = parseFloat(headerValue); + } + headerValue = result.response.headers.get('int32'); + if (headerValue != null) { + value.int32 = parseInt(headerValue, 10); + } + headerValue = result.response.headers.get('int64'); + if (headerValue != null) { + value.int64 = parseInt(headerValue, 10); + } + headerValue = result.response.headers.get('decimal'); + if (headerValue != null) { + value.decimal = parseFloat(headerValue); + } + headerValue = result.response.headers.get('enum'); + if (headerValue != null) { + value.enum = headerValue; + } + headerValue = result.response.headers.get('datetime'); + if (headerValue != null) { + value.datetime = headerValue; + } + return { value: value }; + }); + } + + mixed(request, context) { + const uriPartPath = request.path != null && encodeURIComponent(request.path); + if (!uriPartPath) { + return Promise.resolve(createRequiredRequestFieldError('path')); + } + let uri = `mixed/${uriPartPath}`; + const query = []; + request.query == null || query.push('query=' + encodeURIComponent(request.query)); + if (query.length) { + uri = uri + '?' + query.join('&'); + } + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + normal: request.normal + }) + }; + if (request.header != null) { + fetchRequest.headers['header'] = request.header; + } + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = result.json; + } + } + else if (status === 202) { + if (result.json) { + value = { body: result.json }; + } + } + else if (status === 204) { + value = { empty: true }; + } + if (!value) { + return createResponseError(status, result.json); + } + let headerValue; + headerValue = result.response.headers.get('header'); + if (headerValue != null) { + value.header = headerValue; + } + return { value: value }; + }); + } + + required(request, context) { + let uri = 'required'; + const query = []; + request.query == null || query.push('query=' + encodeURIComponent(request.query)); + if (query.length) { + uri = uri + '?' + query.join('&'); + } + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + normal: request.normal, + widget: request.widget, + widgets: request.widgets, + widgetMatrix: request.widgetMatrix, + widgetResult: request.widgetResult, + widgetResults: request.widgetResults, + widgetMap: request.widgetMap, + hasWidget: request.hasWidget, + point: request.point + }) + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = result.json; + } + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } + + mirrorBytes(request, context) { + const uri = 'mirrorBytes'; + if (!request.content) { + return Promise.resolve(createRequiredRequestFieldError('content')); + } + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request.content) + }; + if (request.type != null) { + fetchRequest.headers['Content-Type'] = request.type; + } + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = { content: result.json }; + } + } + if (!value) { + return createResponseError(status, result.json); + } + let headerValue; + headerValue = result.response.headers.get('Content-Type'); + if (headerValue != null) { + value.type = headerValue; + } + return { value: value }; + }); + } + + mirrorText(request, context) { + const uri = 'mirrorText'; + if (!request.content) { + return Promise.resolve(createRequiredRequestFieldError('content')); + } + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request.content) + }; + if (request.type != null) { + fetchRequest.headers['Content-Type'] = request.type; + } + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = { content: result.json }; + } + } + if (!value) { + return createResponseError(status, result.json); + } + let headerValue; + headerValue = result.response.headers.get('Content-Type'); + if (headerValue != null) { + value.type = headerValue; + } + return { value: value }; + }); + } + + bodyTypes(request, context) { + const uri = 'bodyTypes'; + if (!request.content) { + return Promise.resolve(createRequiredRequestFieldError('content')); + } + const fetchRequest = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request.content) + }; + return fetchResponse(this._fetch, this._baseUri + uri, fetchRequest, context) + .then(result => { + const status = result.response.status; + let value = null; + if (status === 200) { + if (result.json) { + value = { content: result.json }; + } + } + if (!value) { + return createResponseError(status, result.json); + } + return { value: value }; + }); + } +} diff --git a/conformance/js/src/fastify/app.js b/conformance/js/src/fastify/app.js new file mode 100644 index 0000000..c6b4f93 --- /dev/null +++ b/conformance/js/src/fastify/app.js @@ -0,0 +1,14 @@ +import { ConformanceApiService } from "../../../ts/src/conformanceApiService.js"; +import conformanceTestsJson from "../../../ConformanceTests.json"; +import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; + +const options = {}; + +const app = async (fastify, opts) => { + fastify.register(conformanceApiPlugin, { + api: new ConformanceApiService(conformanceTestsJson.tests), + }); +}; + +export default app; +export { app, options }; diff --git a/conformance/js/src/fastify/conformanceApiPlugin.js b/conformance/js/src/fastify/conformanceApiPlugin.js new file mode 100644 index 0000000..0efc9b5 --- /dev/null +++ b/conformance/js/src/fastify/conformanceApiPlugin.js @@ -0,0 +1,529 @@ +// DO NOT EDIT: generated by fsdgenjs +/* eslint-disable */ +'use strict'; + + +const standardErrorCodes = { + 'NotModified': 304, + 'InvalidRequest': 400, + 'NotAuthenticated': 401, + 'NotAuthorized': 403, + 'NotFound': 404, + 'Conflict': 409, + 'RequestTooLarge': 413, + 'TooManyRequests': 429, + 'InternalError': 500, + 'ServiceUnavailable': 503, + 'NotAdmin': 403, + 'TooHappy': 500, +}; + +function parseBoolean(value) { + if (typeof value === 'string') { + const lowerValue = value.toLowerCase(); + if (lowerValue === 'true') { + return true; + } + if (lowerValue === 'false') { + return false; + } + } + return undefined; +} + +export const conformanceApiPlugin = async (fastify, opts) => { + const { api } = opts; + + fastify.route({ + url: '/', + method: 'GET', + handler: async function (req, res) { + const request = {}; + + const result = await api.getApiInfo(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets', + method: 'GET', + handler: async function (req, res) { + const request = {}; + + const query = req.query; + if (typeof query['q'] === 'string') request.query = query['q']; + + const result = await api.getWidgets(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + request.widget = req.body; + + const result = await api.createWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.url != null) res.header('Location', result.value.url); + if (result.value.eTag != null) res.header('eTag', result.value.eTag); + + if (result.value.widget) { + res.status(201).send(result.value.widget); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets/:id', + method: 'GET', + handler: async function (req, res) { + const request = {}; + + const params = req.params; + if (typeof params['id'] === 'string') request.id = parseInt(params['id'], 10); + + const headers = req.headers; + if (typeof headers['if-none-match'] === 'string') request.ifNotETag = headers['if-none-match']; + + const result = await api.getWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.eTag != null) res.header('eTag', result.value.eTag); + + if (result.value.widget) { + res.status(200).send(result.value.widget); + return; + } + + if (result.value.notModified) { + res.status(304); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets/:id', + method: 'DELETE', + handler: async function (req, res) { + const request = {}; + + const params = req.params; + if (typeof params['id'] === 'string') request.id = parseInt(params['id'], 10); + + const headers = req.headers; + if (typeof headers['if-match'] === 'string') request.ifETag = headers['if-match']; + + const result = await api.deleteWidget(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.notFound) { + res.status(404); + return; + } + + if (result.value.conflict) { + res.status(409); + return; + } + + res.status(204); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/widgets/get', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + request.ids = req.body; + + const result = await api.getWidgetBatch(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.results) { + res.status(200).send(result.value.results); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorFields', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + const body = req.body; + request.field = body.field; + request.matrix = body.matrix; + + const result = await api.mirrorFields(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/checkQuery', + method: 'GET', + handler: async function (req, res) { + const request = {}; + + const query = req.query; + if (typeof query['string'] === 'string') request.string = query['string']; + if (typeof query['boolean'] === 'string') request.boolean = parseBoolean(query['boolean']); + if (typeof query['double'] === 'string') request.double = parseFloat(query['double']); + if (typeof query['int32'] === 'string') request.int32 = parseInt(query['int32'], 10); + if (typeof query['int64'] === 'string') request.int64 = parseInt(query['int64'], 10); + if (typeof query['decimal'] === 'string') request.decimal = parseFloat(query['decimal']); + if (typeof query['enum'] === 'string') request.enum = query['enum']; + if (typeof query['datetime'] === 'string') request.datetime = query['datetime']; + + const result = await api.checkQuery(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/checkPath/:string/:boolean/:double/:int32/:int64/:decimal/:enum/:datetime', + method: 'GET', + handler: async function (req, res) { + const request = {}; + + const params = req.params; + if (typeof params['string'] === 'string') request.string = params['string']; + if (typeof params['boolean'] === 'string') request.boolean = parseBoolean(params['boolean']); + if (typeof params['double'] === 'string') request.double = parseFloat(params['double']); + if (typeof params['int32'] === 'string') request.int32 = parseInt(params['int32'], 10); + if (typeof params['int64'] === 'string') request.int64 = parseInt(params['int64'], 10); + if (typeof params['decimal'] === 'string') request.decimal = parseFloat(params['decimal']); + if (typeof params['enum'] === 'string') request.enum = params['enum']; + if (typeof params['datetime'] === 'string') request.datetime = params['datetime']; + + const result = await api.checkPath(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorHeaders', + method: 'GET', + handler: async function (req, res) { + const request = {}; + + const headers = req.headers; + if (typeof headers['string'] === 'string') request.string = headers['string']; + if (typeof headers['boolean'] === 'string') request.boolean = parseBoolean(headers['boolean']); + if (typeof headers['double'] === 'string') request.double = parseFloat(headers['double']); + if (typeof headers['int32'] === 'string') request.int32 = parseInt(headers['int32'], 10); + if (typeof headers['int64'] === 'string') request.int64 = parseInt(headers['int64'], 10); + if (typeof headers['decimal'] === 'string') request.decimal = parseFloat(headers['decimal']); + if (typeof headers['enum'] === 'string') request.enum = headers['enum']; + if (typeof headers['datetime'] === 'string') request.datetime = headers['datetime']; + + const result = await api.mirrorHeaders(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.string != null) res.header('string', result.value.string); + if (result.value.boolean != null) res.header('boolean', result.value.boolean); + if (result.value.double != null) res.header('double', result.value.double); + if (result.value.int32 != null) res.header('int32', result.value.int32); + if (result.value.int64 != null) res.header('int64', result.value.int64); + if (result.value.decimal != null) res.header('decimal', result.value.decimal); + if (result.value.enum != null) res.header('enum', result.value.enum); + if (result.value.datetime != null) res.header('datetime', result.value.datetime); + + res.status(200); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mixed/:path', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + const params = req.params; + if (typeof params['path'] === 'string') request.path = params['path']; + + const query = req.query; + if (typeof query['query'] === 'string') request.query = query['query']; + + const headers = req.headers; + if (typeof headers['header'] === 'string') request.header = headers['header']; + + const body = req.body; + request.normal = body.normal; + + const result = await api.mixed(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.header != null) res.header('header', result.value.header); + + if (result.value.body) { + res.status(202).send(result.value.body); + return; + } + + if (result.value.empty) { + res.status(204); + return; + } + + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/required', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + const query = req.query; + if (typeof query['query'] === 'string') request.query = query['query']; + + const body = req.body; + request.normal = body.normal; + request.widget = body.widget; + request.widgets = body.widgets; + request.widgetMatrix = body.widgetMatrix; + request.widgetResult = body.widgetResult; + request.widgetResults = body.widgetResults; + request.widgetMap = body.widgetMap; + request.hasWidget = body.hasWidget; + request.point = body.point; + + const result = await api.required(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + res.status(200).send(result.value); + return; + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorBytes', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + const headers = req.headers; + if (typeof headers['content-type'] === 'string') request.type = headers['content-type']; + + request.content = req.body; + + const result = await api.mirrorBytes(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.type != null) res.header('Content-Type', result.value.type); + + if (result.value.content) { + res.status(200).send(result.value.content); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/mirrorText', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + const headers = req.headers; + if (typeof headers['content-type'] === 'string') request.type = headers['content-type']; + + request.content = req.body; + + const result = await api.mirrorText(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.type != null) res.header('Content-Type', result.value.type); + + if (result.value.content) { + res.status(200).send(result.value.content); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); + + fastify.route({ + url: '/bodyTypes', + method: 'POST', + handler: async function (req, res) { + const request = {}; + + request.content = req.body; + + const result = await api.bodyTypes(request); + + if (result.error) { + const status = result.error.code && standardErrorCodes[result.error.code]; + res.status(status || 500).send(result.error); + return; + } + + if (result.value) { + if (result.value.content) { + res.status(200).send(result.value.content); + return; + } + } + + throw new Error('Result must have an error or value.'); + } + }); +} diff --git a/conformance/package.json b/conformance/package.json index d3b1315..b6023ce 100644 --- a/conformance/package.json +++ b/conformance/package.json @@ -5,7 +5,8 @@ "scripts": { "build": "tsc", "test": "npm run --silent build && mocha dist/ts/test/conformanceApiTests.js", - "fastify": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/ts/src/fastify/app.js" + "fastify:ts": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/ts/src/fastify/app.js", + "fastify:js": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/js/src/fastify/app.js" }, "repository": { "type": "git", diff --git a/conformance/tsconfig.json b/conformance/tsconfig.json index a6a65b0..9495247 100644 --- a/conformance/tsconfig.json +++ b/conformance/tsconfig.json @@ -10,8 +10,10 @@ "lib": [ "ES2019", "DOM" ], "resolveJsonModule": true, "esModuleInterop": true, + "allowJs": true }, "include": [ - "ts/**/*.ts" + "ts/**/*.ts", + "js/**/*.js" ] } diff --git a/example/js/exampleApiServer.js b/example/js/exampleApiServer.js index 88aed80..2a13ea9 100644 --- a/example/js/exampleApiServer.js +++ b/example/js/exampleApiServer.js @@ -47,7 +47,7 @@ export function createApp(service) { request.limit = parseInt(req.query['limit'], 10); } if (typeof req.query['sort'] === 'string') { - request.sort = req.query['sort'] as WidgetField; + request.sort = req.query['sort']; } if (typeof req.query['desc'] === 'string') { request.desc = parseBoolean(req.query['desc']); diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 8ca97df..22fb211 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -809,14 +809,14 @@ private static string RenderUriComponent(ServiceFieldInfo field, ServiceInfo ser } } - private static string ParseFieldValue(ServiceFieldInfo field, ServiceInfo service, string value) + private string ParseFieldValue(ServiceFieldInfo field, ServiceInfo service, string value) { var fieldTypeKind = service.GetFieldType(field)!.Kind; switch (fieldTypeKind) { case ServiceTypeKind.Enum: - return $"{value} as {field.TypeName}"; + return $"{value}{IfTypeScript($" as {field.TypeName}")}"; case ServiceTypeKind.String: case ServiceTypeKind.Bytes: case ServiceTypeKind.DateTime: diff --git a/tools/Build/Build.cs b/tools/Build/Build.cs index 851d233..f7809c8 100644 --- a/tools/Build/Build.cs +++ b/tools/Build/Build.cs @@ -46,6 +46,7 @@ void CodeGen(bool verify) RunDotNet("FacilityConformance", "fsd", "--output", "conformance/ConformanceApi.fsd", verifyOption); RunDotNet("FacilityConformance", "json", "--output", "conformance/ConformanceTests.json", verifyOption); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/js/src/", "--fastify", "--indent", "2", "--disable-eslint"); RunCodeGen("conformance/ConformanceApi.fsd", "conformance/ts/src/", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); void RunCodeGen(params string?[] args) => From dd18f5a8635b8a3713c4f569944dcaab7b095ae1 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 16 Dec 2023 16:09:02 -0800 Subject: [PATCH 11/19] Restructure conformance folder structure. --- conformance/package.json | 6 +++--- .../{js/src => src/js}/conformanceApi.js | 0 conformance/{js/src => src/js}/fastify/app.js | 2 +- .../js}/fastify/conformanceApiPlugin.js | 0 .../{ts/src => src/ts}/conformanceApi.ts | 0 .../src => src/ts}/conformanceApiService.ts | 0 .../{ts/src => src/ts}/conformanceApiTypes.ts | 0 conformance/{ts/src => src/ts}/fastify/app.ts | 11 ++++++++--- .../ts}/fastify/conformanceApiPlugin.ts | 0 .../{ts => }/test/conformanceApiTests.ts | 19 +++++++++---------- conformance/tsconfig.json | 7 ++++--- tools/Build/Build.cs | 4 ++-- 12 files changed, 27 insertions(+), 22 deletions(-) rename conformance/{js/src => src/js}/conformanceApi.js (100%) rename conformance/{js/src => src/js}/fastify/app.js (81%) rename conformance/{js/src => src/js}/fastify/conformanceApiPlugin.js (100%) rename conformance/{ts/src => src/ts}/conformanceApi.ts (100%) rename conformance/{ts/src => src/ts}/conformanceApiService.ts (100%) rename conformance/{ts/src => src/ts}/conformanceApiTypes.ts (100%) rename conformance/{ts/src => src/ts}/fastify/app.ts (56%) rename conformance/{ts/src => src/ts}/fastify/conformanceApiPlugin.ts (100%) rename conformance/{ts => }/test/conformanceApiTests.ts (55%) diff --git a/conformance/package.json b/conformance/package.json index b6023ce..a2f35f1 100644 --- a/conformance/package.json +++ b/conformance/package.json @@ -4,9 +4,9 @@ "description": "Tests the ConformanceApi client.", "scripts": { "build": "tsc", - "test": "npm run --silent build && mocha dist/ts/test/conformanceApiTests.js", - "fastify:ts": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/ts/src/fastify/app.js", - "fastify:js": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/js/src/fastify/app.js" + "test": "npm run --silent build && mocha dist/test/conformanceApiTests.js", + "fastify:ts": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/src/ts/fastify/app.js", + "fastify:js": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/src/js/fastify/app.js" }, "repository": { "type": "git", diff --git a/conformance/js/src/conformanceApi.js b/conformance/src/js/conformanceApi.js similarity index 100% rename from conformance/js/src/conformanceApi.js rename to conformance/src/js/conformanceApi.js diff --git a/conformance/js/src/fastify/app.js b/conformance/src/js/fastify/app.js similarity index 81% rename from conformance/js/src/fastify/app.js rename to conformance/src/js/fastify/app.js index c6b4f93..7760f90 100644 --- a/conformance/js/src/fastify/app.js +++ b/conformance/src/js/fastify/app.js @@ -1,4 +1,4 @@ -import { ConformanceApiService } from "../../../ts/src/conformanceApiService.js"; +import { ConformanceApiService } from "../../ts/conformanceApiService.js"; import conformanceTestsJson from "../../../ConformanceTests.json"; import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; diff --git a/conformance/js/src/fastify/conformanceApiPlugin.js b/conformance/src/js/fastify/conformanceApiPlugin.js similarity index 100% rename from conformance/js/src/fastify/conformanceApiPlugin.js rename to conformance/src/js/fastify/conformanceApiPlugin.js diff --git a/conformance/ts/src/conformanceApi.ts b/conformance/src/ts/conformanceApi.ts similarity index 100% rename from conformance/ts/src/conformanceApi.ts rename to conformance/src/ts/conformanceApi.ts diff --git a/conformance/ts/src/conformanceApiService.ts b/conformance/src/ts/conformanceApiService.ts similarity index 100% rename from conformance/ts/src/conformanceApiService.ts rename to conformance/src/ts/conformanceApiService.ts diff --git a/conformance/ts/src/conformanceApiTypes.ts b/conformance/src/ts/conformanceApiTypes.ts similarity index 100% rename from conformance/ts/src/conformanceApiTypes.ts rename to conformance/src/ts/conformanceApiTypes.ts diff --git a/conformance/ts/src/fastify/app.ts b/conformance/src/ts/fastify/app.ts similarity index 56% rename from conformance/ts/src/fastify/app.ts rename to conformance/src/ts/fastify/app.ts index 2447dff..ad91fde 100644 --- a/conformance/ts/src/fastify/app.ts +++ b/conformance/src/ts/fastify/app.ts @@ -1,13 +1,18 @@ import { FastifyPluginAsync } from "fastify"; import { ConformanceApiService } from "../conformanceApiService.js"; -import conformanceTestsJson from '../../../ConformanceTests.json'; +import conformanceTestsJson from "../../../ConformanceTests.json"; import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; export type AppOptions = {}; const options: AppOptions = {}; -const app: FastifyPluginAsync = async (fastify, opts): Promise => { - fastify.register(conformanceApiPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests) }); +const app: FastifyPluginAsync = async ( + fastify, + opts +): Promise => { + fastify.register(conformanceApiPlugin, { + api: new ConformanceApiService(conformanceTestsJson.tests), + }); }; export default app; diff --git a/conformance/ts/src/fastify/conformanceApiPlugin.ts b/conformance/src/ts/fastify/conformanceApiPlugin.ts similarity index 100% rename from conformance/ts/src/fastify/conformanceApiPlugin.ts rename to conformance/src/ts/fastify/conformanceApiPlugin.ts diff --git a/conformance/ts/test/conformanceApiTests.ts b/conformance/test/conformanceApiTests.ts similarity index 55% rename from conformance/ts/test/conformanceApiTests.ts rename to conformance/test/conformanceApiTests.ts index 95b0f8d..88d8dd5 100644 --- a/conformance/ts/test/conformanceApiTests.ts +++ b/conformance/test/conformanceApiTests.ts @@ -1,21 +1,21 @@ -import { createHttpClient } from '../src/conformanceApi'; -import { expect, should } from 'chai'; -import fetch from 'node-fetch'; -import conformanceTestsJson from '../../ConformanceTests.json'; +import { createHttpClient } from "../src/ts/conformanceApi"; +import { expect, should } from "chai"; +import fetch from "node-fetch"; +import conformanceTestsJson from "../ConformanceTests.json"; should(); const httpClient = createHttpClient({ fetch: (uri, request) => { - return fetch('http://localhost:4117/' + uri, request); - } + return fetch("http://localhost:4117/" + uri, request); + }, }); -describe('tests', () => { - +describe("tests", () => { conformanceTestsJson.tests.forEach((data: any) => { it(data.test, async () => { - return ((httpClient as any)[data.method](data.request)) + return (httpClient as any) + [data.method](data.request) .then((result: any) => { expect({ error: result.error ?? undefined, @@ -27,5 +27,4 @@ describe('tests', () => { }); }); }); - }); diff --git a/conformance/tsconfig.json b/conformance/tsconfig.json index 9495247..ad750c0 100644 --- a/conformance/tsconfig.json +++ b/conformance/tsconfig.json @@ -13,7 +13,8 @@ "allowJs": true }, "include": [ - "ts/**/*.ts", - "js/**/*.js" - ] + "src/**/*.ts", + "src/**/*.js", + "test/**/*.ts" + ], } diff --git a/tools/Build/Build.cs b/tools/Build/Build.cs index f7809c8..c952d98 100644 --- a/tools/Build/Build.cs +++ b/tools/Build/Build.cs @@ -46,8 +46,8 @@ void CodeGen(bool verify) RunDotNet("FacilityConformance", "fsd", "--output", "conformance/ConformanceApi.fsd", verifyOption); RunDotNet("FacilityConformance", "json", "--output", "conformance/ConformanceTests.json", verifyOption); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/js/src/", "--fastify", "--indent", "2", "--disable-eslint"); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/ts/src/", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/js/", "--fastify", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/ts/", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); void RunCodeGen(params string?[] args) => RunDotNet(new[] { "run", "--no-build", "--project", $"src/{codegen}", "-f", "net6.0", "-c", configuration, "--", "--newline", "lf", verifyOption }.Concat(args)); From 6470f1db36cda44256825c71e2fb197c65775cd7 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Sat, 16 Dec 2023 16:41:01 -0800 Subject: [PATCH 12/19] Only generate fastify plugin when option is supplied. - This allows the plugin to live wherever the user wants it to live. - Ideally I would think the express server should be generated the same way but that would be a breaking change, so maybe not needed? --- .../JavaScriptGenerator.cs | 323 +++++++++--------- tools/Build/Build.cs | 8 +- 2 files changed, 172 insertions(+), 159 deletions(-) diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 22fb211..4a4ed46 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -48,10 +48,12 @@ public static int GenerateJavaScript(JavaScriptGeneratorSettings settings) => /// public override CodeGenOutput GenerateOutput(ServiceInfo service) { + if (Fastify) + return GenerateFastifyPluginOutput(service); + var httpServiceInfo = HttpServiceInfo.Create(service); var moduleName = ModuleName ?? service.Name; - var camelCaseModuleName = CodeGenUtility.ToCamelCase(moduleName); var capModuleName = CodeGenUtility.Capitalize(moduleName); var typesFileName = CodeGenUtility.Uncapitalize(moduleName) + "Types" + (TypeScript ? ".ts" : ".js"); var clientFileName = CodeGenUtility.Uncapitalize(moduleName) + (TypeScript ? ".ts" : ".js"); @@ -510,212 +512,219 @@ public override CodeGenOutput GenerateOutput(ServiceInfo service) })); } - if (Fastify) + return new CodeGenOutput(namedTexts, new List()); + } + + /// + /// Applies generator-specific settings. + /// + public override void ApplySettings(FileGeneratorSettings settings) + { + var ourSettings = (JavaScriptGeneratorSettings) settings; + ModuleName = ourSettings.ModuleName; + TypeScript = ourSettings.TypeScript; + Express = ourSettings.Express; + Fastify = ourSettings.Fastify; + DisableESLint = ourSettings.DisableESLint; + } + + /// + /// Supports writing output to a single file. + /// + public override bool SupportsSingleOutput => true; + + private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) + { + var httpServiceInfo = HttpServiceInfo.Create(service); + var moduleName = ModuleName ?? service.Name; + var capModuleName = CodeGenUtility.Capitalize(moduleName); + var camelCaseModuleName = CodeGenUtility.ToCamelCase(moduleName); + var pluginFileName = CodeGenUtility.Uncapitalize(moduleName) + "Plugin" + (TypeScript ? ".ts" : ".js"); + + var file = CreateFile(pluginFileName, code => { - var pluginFileName = CodeGenUtility.Uncapitalize(moduleName) + "Plugin" + (TypeScript ? ".ts" : ".js"); - namedTexts.Add(CreateFile("fastify/" + pluginFileName, code => - { - WriteFileHeader(code); + WriteFileHeader(code); - if (!TypeScript) - code.WriteLine("'use strict';"); + if (!TypeScript) + code.WriteLine("'use strict';"); - code.WriteLine(); + code.WriteLine(); - var fastifyImports = new List(); - if (TypeScript) - fastifyImports.Add("FastifyPluginAsync"); - WriteImports(code, fastifyImports, "fastify"); + var fastifyImports = new List(); + if (TypeScript) + fastifyImports.Add("FastifyPluginAsync"); + WriteImports(code, fastifyImports, "fastify"); - var facilityImports = new List(); - if (TypeScript) - { - facilityImports.Add("IServiceResult"); - facilityImports.Add("IServiceError"); - } - WriteImports(code, facilityImports, "facility-core"); + var facilityImports = new List(); + if (TypeScript) + { + facilityImports.Add("IServiceResult"); + facilityImports.Add("IServiceError"); + } + WriteImports(code, facilityImports, "facility-core"); - code.WriteLine(); - WriteStandardErrorCodesVariable("standardErrorCodes", code, httpServiceInfo.ErrorSets); + code.WriteLine(); + WriteStandardErrorCodesVariable("standardErrorCodes", code, httpServiceInfo.ErrorSets); - code.WriteLine(); - WriteParseBooleanFunction("parseBoolean", code); + code.WriteLine(); + WriteParseBooleanFunction("parseBoolean", code); - if (TypeScript) + if (TypeScript) + { + code.WriteLine(); + using (code.Block($"export type {capModuleName}PluginOptions = {{", "}")) { - code.WriteLine(); - using (code.Block($"export type {capModuleName}PluginOptions = {{", "}")) - { - code.WriteLine($"api: I{capModuleName};"); - } + code.WriteLine($"api: I{capModuleName};"); } + } - code.WriteLine(); - using (code.Block($"export const {camelCaseModuleName}Plugin" + IfTypeScript($": FastifyPluginAsync<{capModuleName}PluginOptions>") + " = async (fastify, opts) => {", "}")) + code.WriteLine(); + using (code.Block($"export const {camelCaseModuleName}Plugin" + IfTypeScript($": FastifyPluginAsync<{capModuleName}PluginOptions>") + " = async (fastify, opts) => {", "}")) + { + code.WriteLine("const { api } = opts;"); + + foreach (var httpMethodInfo in httpServiceInfo.Methods) { - code.WriteLine("const { api } = opts;"); + var methodName = httpMethodInfo.ServiceMethod.Name; + var capMethodName = CodeGenUtility.Capitalize(methodName); + var fastifyPath = httpMethodInfo.Path; + foreach (var httpPathField in httpMethodInfo.PathFields) + fastifyPath = ReplaceOrdinal(fastifyPath, "{" + httpPathField.Name + "}", $":{httpPathField.Name}"); - foreach (var httpMethodInfo in httpServiceInfo.Methods) + code.WriteLine(); + using (code.Block("fastify.route({", "});")) { - var methodName = httpMethodInfo.ServiceMethod.Name; - var capMethodName = CodeGenUtility.Capitalize(methodName); - var fastifyPath = httpMethodInfo.Path; - foreach (var httpPathField in httpMethodInfo.PathFields) - fastifyPath = ReplaceOrdinal(fastifyPath, "{" + httpPathField.Name + "}", $":{httpPathField.Name}"); - - code.WriteLine(); - using (code.Block("fastify.route({", "});")) + code.WriteLine($"url: '{fastifyPath}',"); + code.WriteLine($"method: '{httpMethodInfo.Method.ToUpperInvariant()}',"); + using (code.Block("handler: async function (req, res) {", "}")) { - code.WriteLine($"url: '{fastifyPath}',"); - code.WriteLine($"method: '{httpMethodInfo.Method.ToUpperInvariant()}',"); - using (code.Block("handler: async function (req, res) {", "}")) + code.WriteLine("const request" + IfTypeScript($": I{capMethodName}Request") + " = {};"); + if (httpMethodInfo.PathFields.Count != 0) { - code.WriteLine("const request" + IfTypeScript($": I{capMethodName}Request") + " = {};"); - if (httpMethodInfo.PathFields.Count != 0) + code.WriteLine(); + code.WriteLine($"const params = req.params{IfTypeScript(" as Record")};"); + foreach (var pathParam in httpMethodInfo.PathFields) { - code.WriteLine(); - code.WriteLine($"const params = req.params{IfTypeScript(" as Record")};"); - foreach (var pathParam in httpMethodInfo.PathFields) - { - code.WriteLine($"if (typeof params['{pathParam.Name}'] === 'string') request.{pathParam.ServiceField.Name} = {ParseFieldValue(pathParam.ServiceField, service, $"params['{pathParam.Name}']")};"); - } - } - - if (httpMethodInfo.QueryFields.Count != 0) - { - code.WriteLine(); - code.WriteLine($"const query = req.query{IfTypeScript(" as Record")};"); - foreach (var queryParam in httpMethodInfo.QueryFields) - { - code.WriteLine($"if (typeof query['{queryParam.Name}'] === 'string') request.{queryParam.ServiceField.Name} = {ParseFieldValue(queryParam.ServiceField, service, $"query['{queryParam.Name}']")};"); - } + code.WriteLine($"if (typeof params['{pathParam.Name}'] === 'string') request.{pathParam.ServiceField.Name} = {ParseFieldValue(pathParam.ServiceField, service, $"params['{pathParam.Name}']")};"); } + } - if (httpMethodInfo.RequestHeaderFields.Count != 0) + if (httpMethodInfo.QueryFields.Count != 0) + { + code.WriteLine(); + code.WriteLine($"const query = req.query{IfTypeScript(" as Record")};"); + foreach (var queryParam in httpMethodInfo.QueryFields) { - code.WriteLine(); - code.WriteLine($"const headers = req.headers{IfTypeScript(" as Record")};"); - foreach (var header in httpMethodInfo.RequestHeaderFields) - { - string lowerHeaderName = header.Name.ToLowerInvariant(); - code.WriteLine($"if (typeof headers['{lowerHeaderName}'] === 'string') request.{header.ServiceField.Name} = {ParseFieldValue(header.ServiceField, service, $"headers['{lowerHeaderName}']")};"); - } + code.WriteLine($"if (typeof query['{queryParam.Name}'] === 'string') request.{queryParam.ServiceField.Name} = {ParseFieldValue(queryParam.ServiceField, service, $"query['{queryParam.Name}']")};"); } + } - if (httpMethodInfo.RequestBodyField != null) - { - code.WriteLine(); - code.WriteLine($"request.{httpMethodInfo.RequestBodyField.ServiceField.Name} = req.body{IfTypeScript(" as never")};"); - } - else if (httpMethodInfo.RequestNormalFields.Count != 0) + if (httpMethodInfo.RequestHeaderFields.Count != 0) + { + code.WriteLine(); + code.WriteLine($"const headers = req.headers{IfTypeScript(" as Record")};"); + foreach (var header in httpMethodInfo.RequestHeaderFields) { - code.WriteLine(); - code.WriteLine($"const body = req.body{IfTypeScript(" as Record")};"); - foreach (var field in httpMethodInfo.RequestNormalFields) - code.WriteLine($"request.{field.ServiceField.Name} = body.{field.ServiceField.Name};"); + string lowerHeaderName = header.Name.ToLowerInvariant(); + code.WriteLine($"if (typeof headers['{lowerHeaderName}'] === 'string') request.{header.ServiceField.Name} = {ParseFieldValue(header.ServiceField, service, $"headers['{lowerHeaderName}']")};"); } + } + if (httpMethodInfo.RequestBodyField != null) + { code.WriteLine(); - code.WriteLine($"const result = await api.{methodName}(request);"); - + code.WriteLine($"request.{httpMethodInfo.RequestBodyField.ServiceField.Name} = req.body{IfTypeScript(" as never")};"); + } + else if (httpMethodInfo.RequestNormalFields.Count != 0) + { code.WriteLine(); - using (code.Block("if (result.error) {", "}")) + code.WriteLine($"const body = req.body{IfTypeScript(" as Record")};"); + foreach (var field in httpMethodInfo.RequestNormalFields) + code.WriteLine($"request.{field.ServiceField.Name} = body.{field.ServiceField.Name};"); + } + + code.WriteLine(); + code.WriteLine($"const result = await api.{methodName}(request);"); + + code.WriteLine(); + using (code.Block("if (result.error) {", "}")) + { + code.WriteLine("const status = result.error.code && standardErrorCodes[result.error.code];"); + code.WriteLine("res.status(status || 500).send(result.error);"); + code.WriteLine("return;"); + } + + code.WriteLine(); + using (code.Block("if (result.value) {", "}")) + { + if (httpMethodInfo.ResponseHeaderFields.Count != 0) { - code.WriteLine("const status = result.error.code && standardErrorCodes[result.error.code];"); - code.WriteLine("res.status(status || 500).send(result.error);"); - code.WriteLine("return;"); + code.WriteLineSkipOnce(); + foreach (var field in httpMethodInfo.ResponseHeaderFields) + code.WriteLine($"if (result.value.{field.ServiceField.Name} != null) res.header('{field.Name}', result.value.{field.ServiceField.Name});"); } - code.WriteLine(); - using (code.Block("if (result.value) {", "}")) + var handledResponses = httpMethodInfo.ValidResponses.ToList(); + foreach (var validResponse in httpMethodInfo.ValidResponses.Where(x => x.BodyField is not null)) { - if (httpMethodInfo.ResponseHeaderFields.Count != 0) - { - code.WriteLineSkipOnce(); - foreach (var field in httpMethodInfo.ResponseHeaderFields) - code.WriteLine($"if (result.value.{field.ServiceField.Name} != null) res.header('{field.Name}', result.value.{field.ServiceField.Name});"); - } + handledResponses.Remove(validResponse); + var bodyField = validResponse.BodyField!; + var statusCode = (int) validResponse.StatusCode; - var handledResponses = httpMethodInfo.ValidResponses.ToList(); - foreach (var validResponse in httpMethodInfo.ValidResponses.Where(x => x.BodyField is not null)) + code.WriteLineSkipOnce(); + using (code.Block($"if (result.value.{bodyField.ServiceField.Name}) {{", "}")) { - handledResponses.Remove(validResponse); - var bodyField = validResponse.BodyField!; - var statusCode = (int) validResponse.StatusCode; - - code.WriteLineSkipOnce(); - using (code.Block($"if (result.value.{bodyField.ServiceField.Name}) {{", "}")) - { - var bodyFieldType = service.GetFieldType(bodyField.ServiceField)!; - if (bodyFieldType.Kind == ServiceTypeKind.Boolean) - { - code.WriteLine($"res.status({statusCode});"); - code.WriteLine("return;"); - } - else - { - code.WriteLine($"res.status({statusCode}).send(result.value.{bodyField.ServiceField.Name});"); - code.WriteLine("return;"); - } - } - } - - if (handledResponses.Count == 1) - { - var lastValidResponse = handledResponses[0]; - var statusCode = (int) lastValidResponse.StatusCode; - - if (lastValidResponse.NormalFields?.Count > 0) + var bodyFieldType = service.GetFieldType(bodyField.ServiceField)!; + if (bodyFieldType.Kind == ServiceTypeKind.Boolean) { - code.WriteLineSkipOnce(); - code.WriteLine($"res.status({statusCode}).send(result.value);"); + code.WriteLine($"res.status({statusCode});"); + code.WriteLine("return;"); } else { - code.WriteLineSkipOnce(); - code.WriteLine($"res.status({statusCode});"); + code.WriteLine($"res.status({statusCode}).send(result.value.{bodyField.ServiceField.Name});"); + code.WriteLine("return;"); } - code.WriteLine("return;"); } - else if (handledResponses.Count > 1) + } + + if (handledResponses.Count == 1) + { + var lastValidResponse = handledResponses[0]; + var statusCode = (int) lastValidResponse.StatusCode; + + if (lastValidResponse.NormalFields?.Count > 0) { - throw new InvalidOperationException("More than one response is left."); + code.WriteLineSkipOnce(); + code.WriteLine($"res.status({statusCode}).send(result.value);"); } + else + { + code.WriteLineSkipOnce(); + code.WriteLine($"res.status({statusCode});"); + } + code.WriteLine("return;"); + } + else if (handledResponses.Count > 1) + { + throw new InvalidOperationException("More than one response is left."); } - - code.WriteLine(); - code.WriteLine("throw new Error('Result must have an error or value.');"); } + + code.WriteLine(); + code.WriteLine("throw new Error('Result must have an error or value.');"); } } } + } - if (TypeScript) - WriteTypes(code, httpServiceInfo); - })); - } - - return new CodeGenOutput(namedTexts, new List()); - } + if (TypeScript) + WriteTypes(code, httpServiceInfo); + }); - /// - /// Applies generator-specific settings. - /// - public override void ApplySettings(FileGeneratorSettings settings) - { - var ourSettings = (JavaScriptGeneratorSettings) settings; - ModuleName = ourSettings.ModuleName; - TypeScript = ourSettings.TypeScript; - Express = ourSettings.Express; - Fastify = ourSettings.Fastify; - DisableESLint = ourSettings.DisableESLint; + return new CodeGenOutput(file); } - /// - /// Supports writing output to a single file. - /// - public override bool SupportsSingleOutput => true; - private void WriteFileHeader(CodeWriter code) { code.WriteLine($"// DO NOT EDIT: generated by {GeneratorName}"); diff --git a/tools/Build/Build.cs b/tools/Build/Build.cs index c952d98..bbdad3f 100644 --- a/tools/Build/Build.cs +++ b/tools/Build/Build.cs @@ -46,8 +46,12 @@ void CodeGen(bool verify) RunDotNet("FacilityConformance", "fsd", "--output", "conformance/ConformanceApi.fsd", verifyOption); RunDotNet("FacilityConformance", "json", "--output", "conformance/ConformanceTests.json", verifyOption); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/js/", "--fastify", "--indent", "2", "--disable-eslint"); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/ts/", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); + + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/js/", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/js/fastify", "--fastify", "--indent", "2", "--disable-eslint"); + + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/ts/", "--typescript", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/ts/fastify", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); void RunCodeGen(params string?[] args) => RunDotNet(new[] { "run", "--no-build", "--project", $"src/{codegen}", "-f", "net6.0", "-c", configuration, "--", "--newline", "lf", verifyOption }.Concat(args)); From da680744ce95bb4a526d4903231c0bc8605bae81 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 18 Dec 2023 05:25:30 -0800 Subject: [PATCH 13/19] Validate ConformanceTests.json before running tests. --- conformance/test/conformanceApiTests.ts | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/conformance/test/conformanceApiTests.ts b/conformance/test/conformanceApiTests.ts index 88d8dd5..00b61cb 100644 --- a/conformance/test/conformanceApiTests.ts +++ b/conformance/test/conformanceApiTests.ts @@ -2,7 +2,11 @@ import { createHttpClient } from "../src/ts/conformanceApi"; import { expect, should } from "chai"; import fetch from "node-fetch"; import conformanceTestsJson from "../ConformanceTests.json"; +import { isDeepStrictEqual } from "util"; +const tests = conformanceTestsJson.tests; + +validateTests(); should(); const httpClient = createHttpClient({ @@ -12,7 +16,7 @@ const httpClient = createHttpClient({ }); describe("tests", () => { - conformanceTestsJson.tests.forEach((data: any) => { + tests.forEach((data: any) => { it(data.test, async () => { return (httpClient as any) [data.method](data.request) @@ -28,3 +32,27 @@ describe("tests", () => { }); }); }); + +function validateTests() { + tests.forEach((data) => { + if (!data.test) { + throw new Error(`Test is missing 'test'`); + } + if (!data.method) { + throw new Error(`'${data.test}' is missing 'method'`); + } + if (data.httpRequest && !data.httpRequest.method) { + throw new Error(`Test '${data.test}' is missintg 'httpRequest.method'`); + } + if (data.httpRequest && !data.httpRequest.path) { + throw new Error(`Test '${data.test}' is missintg 'httpRequest.path'`); + } + if (tests.filter((x) => x.test === data.test).length !== 1) { + throw new Error(`Multiple tests found with name '${data.test}'`); + } + if (tests.filter((x) => x.method === data.method && isDeepStrictEqual(x.request, data.request)).length !== 1) { + throw new Error(`Multiple tests found for with method '${data.method}' and request '${JSON.stringify(data.request)}'`); + } + }); +} + From 26b39c158eba0b75bff33ad6618c6e7969dc98ee Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 18 Dec 2023 13:08:04 -0800 Subject: [PATCH 14/19] Support raw http requests in conformance tests. --- conformance/test/conformanceApiTests.ts | 51 +++++++++++++++---------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/conformance/test/conformanceApiTests.ts b/conformance/test/conformanceApiTests.ts index 00b61cb..17e262e 100644 --- a/conformance/test/conformanceApiTests.ts +++ b/conformance/test/conformanceApiTests.ts @@ -1,35 +1,44 @@ import { createHttpClient } from "../src/ts/conformanceApi"; import { expect, should } from "chai"; -import fetch from "node-fetch"; +import nodeFetch from "node-fetch"; import conformanceTestsJson from "../ConformanceTests.json"; import { isDeepStrictEqual } from "util"; +import { HttpClientUtility } from "facility-core"; const tests = conformanceTestsJson.tests; validateTests(); should(); -const httpClient = createHttpClient({ - fetch: (uri, request) => { - return fetch("http://localhost:4117/" + uri, request); - }, -}); +const fetch: HttpClientUtility.IFetch = (uri, request) => { + return nodeFetch("http://localhost:4117/" + uri, request); +}; + +const httpClient = createHttpClient({ fetch }); -describe("tests", () => { - tests.forEach((data: any) => { - it(data.test, async () => { - return (httpClient as any) - [data.method](data.request) - .then((result: any) => { - expect({ - error: result.error ?? undefined, - value: result.value ?? undefined, - }).to.be.deep.equal({ - error: data.error ?? undefined, - value: data.response ?? undefined, - }); - }); - }); +tests.forEach((data) => { + it(data.test, async () => { + if (data.httpRequest) { + const result = await fetch( + data.httpRequest.path.replace(/^\//, ""), + { method: data.httpRequest.method }); + if (result.status >= 300) { + throw new Error( + `Raw http request failed with status code ${ + result.status + }, ${JSON.stringify(await result.json())}` + ); + } + } else { + const result = await(httpClient as any)[data.method](data.request); + expect({ + error: result.error ?? undefined, + value: result.value ?? undefined, + }).to.be.deep.equal({ + error: data.error ?? undefined, + value: data.response ?? undefined, + }); + } }); }); From c576130b3c30bd457664c36b17361b02f73636f0 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 18 Dec 2023 13:08:34 -0800 Subject: [PATCH 15/19] Support case insensitive querystring keys in fastify plugin. --- .../src/js/fastify/conformanceApiPlugin.js | 15 ++++++++++++- conformance/src/ts/fastify/app.ts | 1 + .../src/ts/fastify/conformanceApiPlugin.ts | 16 +++++++++++++- .../JavaScriptGenerator.cs | 21 ++++++++++++++++++- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/conformance/src/js/fastify/conformanceApiPlugin.js b/conformance/src/js/fastify/conformanceApiPlugin.js index 0efc9b5..2d83264 100644 --- a/conformance/src/js/fastify/conformanceApiPlugin.js +++ b/conformance/src/js/fastify/conformanceApiPlugin.js @@ -32,7 +32,20 @@ function parseBoolean(value) { } export const conformanceApiPlugin = async (fastify, opts) => { - const { api } = opts; + const { api, caseInsenstiveQueryStringKeys } = opts; + + if (caseInsenstiveQueryStringKeys) { + fastify.addHook('onRequest', async (req, res) => { + const query = req.query; + for (const key of Object.keys(query)) { + const lowerKey = key.toLowerCase(); + if (lowerKey !== key) { + query[lowerKey] = query[key]; + delete query[key]; + } + } + }); + } fastify.route({ url: '/', diff --git a/conformance/src/ts/fastify/app.ts b/conformance/src/ts/fastify/app.ts index ad91fde..5755d32 100644 --- a/conformance/src/ts/fastify/app.ts +++ b/conformance/src/ts/fastify/app.ts @@ -12,6 +12,7 @@ const app: FastifyPluginAsync = async ( ): Promise => { fastify.register(conformanceApiPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests), + caseInsenstiveQueryStringKeys: true, }); }; diff --git a/conformance/src/ts/fastify/conformanceApiPlugin.ts b/conformance/src/ts/fastify/conformanceApiPlugin.ts index 8dc9442..e3de206 100644 --- a/conformance/src/ts/fastify/conformanceApiPlugin.ts +++ b/conformance/src/ts/fastify/conformanceApiPlugin.ts @@ -34,10 +34,24 @@ function parseBoolean(value: string | undefined) { export type ConformanceApiPluginOptions = { api: IConformanceApi; + caseInsenstiveQueryStringKeys?: boolean; } export const conformanceApiPlugin: FastifyPluginAsync = async (fastify, opts) => { - const { api } = opts; + const { api, caseInsenstiveQueryStringKeys } = opts; + + if (caseInsenstiveQueryStringKeys) { + fastify.addHook('onRequest', async (req, res) => { + const query = req.query as Record; + for (const key of Object.keys(query)) { + const lowerKey = key.toLowerCase(); + if (lowerKey !== key) { + query[lowerKey] = query[key]; + delete query[key]; + } + } + }); + } fastify.route({ url: '/', diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 4a4ed46..caac2e8 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -575,13 +575,32 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) using (code.Block($"export type {capModuleName}PluginOptions = {{", "}")) { code.WriteLine($"api: I{capModuleName};"); + code.WriteLine("caseInsenstiveQueryStringKeys?: boolean;"); } } code.WriteLine(); using (code.Block($"export const {camelCaseModuleName}Plugin" + IfTypeScript($": FastifyPluginAsync<{capModuleName}PluginOptions>") + " = async (fastify, opts) => {", "}")) { - code.WriteLine("const { api } = opts;"); + code.WriteLine("const { api, caseInsenstiveQueryStringKeys } = opts;"); + + code.WriteLine(); + using (code.Block("if (caseInsenstiveQueryStringKeys) {", "}")) + { + using (code.Block("fastify.addHook('onRequest', async (req, res) => {", "});")) + { + code.WriteLine($"const query = req.query{IfTypeScript(" as Record")};"); + using (code.Block("for (const key of Object.keys(query)) {", "}")) + { + code.WriteLine("const lowerKey = key.toLowerCase();"); + using (code.Block("if (lowerKey !== key) {", "}")) + { + code.WriteLine("query[lowerKey] = query[key];"); + code.WriteLine("delete query[key];"); + } + } + } + } foreach (var httpMethodInfo in httpServiceInfo.Methods) { From d7bfd596ff383efb4ef782b5485653d5c36b56b0 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 18 Dec 2023 13:44:05 -0800 Subject: [PATCH 16/19] Enable case insensitve routing for fastify conformance servers. --- conformance/package.json | 4 ++-- conformance/src/js/fastify/app.js | 7 ++++--- conformance/src/ts/fastify/app.ts | 11 +++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/conformance/package.json b/conformance/package.json index a2f35f1..d1c341f 100644 --- a/conformance/package.json +++ b/conformance/package.json @@ -5,8 +5,8 @@ "scripts": { "build": "tsc", "test": "npm run --silent build && mocha dist/test/conformanceApiTests.js", - "fastify:ts": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/src/ts/fastify/app.js", - "fastify:js": "npm run --silent build && fastify start -p 4117 -w -l info -P dist/src/js/fastify/app.js" + "fastify:ts": "npm run --silent build && fastify start --options -p 4117 -w -l info -P dist/src/ts/fastify/app.js", + "fastify:js": "npm run --silent build && fastify start --options -p 4117 -w -l info -P dist/src/js/fastify/app.js" }, "repository": { "type": "git", diff --git a/conformance/src/js/fastify/app.js b/conformance/src/js/fastify/app.js index 7760f90..0a25081 100644 --- a/conformance/src/js/fastify/app.js +++ b/conformance/src/js/fastify/app.js @@ -2,13 +2,14 @@ import { ConformanceApiService } from "../../ts/conformanceApiService.js"; import conformanceTestsJson from "../../../ConformanceTests.json"; import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; -const options = {}; - const app = async (fastify, opts) => { fastify.register(conformanceApiPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests), + caseInsenstiveQueryStringKeys: true, }); }; export default app; -export { app, options }; +export const options = { + caseSensitive: false, +}; diff --git a/conformance/src/ts/fastify/app.ts b/conformance/src/ts/fastify/app.ts index 5755d32..a77ec09 100644 --- a/conformance/src/ts/fastify/app.ts +++ b/conformance/src/ts/fastify/app.ts @@ -1,12 +1,9 @@ -import { FastifyPluginAsync } from "fastify"; +import { FastifyPluginAsync, FastifyServerOptions } from "fastify"; import { ConformanceApiService } from "../conformanceApiService.js"; import conformanceTestsJson from "../../../ConformanceTests.json"; import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; -export type AppOptions = {}; -const options: AppOptions = {}; - -const app: FastifyPluginAsync = async ( +const app: FastifyPluginAsync = async ( fastify, opts ): Promise => { @@ -17,4 +14,6 @@ const app: FastifyPluginAsync = async ( }; export default app; -export { app, options }; +export const options: FastifyServerOptions = { + caseSensitive: false, +}; From 610278928d806d12077760406d223035af8c5ae3 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 18 Dec 2023 15:20:08 -0800 Subject: [PATCH 17/19] Add service error handling support to fastify plugin. --- conformance/src/js/fastify/app.js | 1 + .../src/js/fastify/conformanceApiPlugin.js | 21 +++++++++++- conformance/src/ts/fastify/app.ts | 1 + .../src/ts/fastify/conformanceApiPlugin.ts | 22 ++++++++++++- conformance/test/conformanceApiTests.ts | 1 + .../JavaScriptGenerator.cs | 33 ++++++++++++++++++- 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/conformance/src/js/fastify/app.js b/conformance/src/js/fastify/app.js index 0a25081..fb083e7 100644 --- a/conformance/src/js/fastify/app.js +++ b/conformance/src/js/fastify/app.js @@ -6,6 +6,7 @@ const app = async (fastify, opts) => { fastify.register(conformanceApiPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests), caseInsenstiveQueryStringKeys: true, + includeErrorDetails: true, }); }; diff --git a/conformance/src/js/fastify/conformanceApiPlugin.js b/conformance/src/js/fastify/conformanceApiPlugin.js index 2d83264..1bf09d7 100644 --- a/conformance/src/js/fastify/conformanceApiPlugin.js +++ b/conformance/src/js/fastify/conformanceApiPlugin.js @@ -32,7 +32,26 @@ function parseBoolean(value) { } export const conformanceApiPlugin = async (fastify, opts) => { - const { api, caseInsenstiveQueryStringKeys } = opts; + const { api, caseInsenstiveQueryStringKeys, includeErrorDetails } = opts; + + fastify.setErrorHandler((error, req, res) => { + req.log.error(error); + if (includeErrorDetails) { + res.status(500).send({ + code: 'InternalError', + message: error.message, + details: { + stack: error.stack?.split('\n').filter((x) => x.length > 0), + } + }); + } + else { + res.status(500).send({ + code: 'InternalError', + message: 'The service experienced an unexpected internal error.', + }); + } + }); if (caseInsenstiveQueryStringKeys) { fastify.addHook('onRequest', async (req, res) => { diff --git a/conformance/src/ts/fastify/app.ts b/conformance/src/ts/fastify/app.ts index a77ec09..5b5ef87 100644 --- a/conformance/src/ts/fastify/app.ts +++ b/conformance/src/ts/fastify/app.ts @@ -10,6 +10,7 @@ const app: FastifyPluginAsync = async ( fastify.register(conformanceApiPlugin, { api: new ConformanceApiService(conformanceTestsJson.tests), caseInsenstiveQueryStringKeys: true, + includeErrorDetails: true, }); }; diff --git a/conformance/src/ts/fastify/conformanceApiPlugin.ts b/conformance/src/ts/fastify/conformanceApiPlugin.ts index e3de206..6f5bd68 100644 --- a/conformance/src/ts/fastify/conformanceApiPlugin.ts +++ b/conformance/src/ts/fastify/conformanceApiPlugin.ts @@ -35,10 +35,30 @@ function parseBoolean(value: string | undefined) { export type ConformanceApiPluginOptions = { api: IConformanceApi; caseInsenstiveQueryStringKeys?: boolean; + includeErrorDetails?: boolean; } export const conformanceApiPlugin: FastifyPluginAsync = async (fastify, opts) => { - const { api, caseInsenstiveQueryStringKeys } = opts; + const { api, caseInsenstiveQueryStringKeys, includeErrorDetails } = opts; + + fastify.setErrorHandler((error, req, res) => { + req.log.error(error); + if (includeErrorDetails) { + res.status(500).send({ + code: 'InternalError', + message: error.message, + details: { + stack: error.stack?.split('\n').filter((x) => x.length > 0), + } + }); + } + else { + res.status(500).send({ + code: 'InternalError', + message: 'The service experienced an unexpected internal error.', + }); + } + }); if (caseInsenstiveQueryStringKeys) { fastify.addHook('onRequest', async (req, res) => { diff --git a/conformance/test/conformanceApiTests.ts b/conformance/test/conformanceApiTests.ts index 17e262e..6b66c62 100644 --- a/conformance/test/conformanceApiTests.ts +++ b/conformance/test/conformanceApiTests.ts @@ -17,6 +17,7 @@ const fetch: HttpClientUtility.IFetch = (uri, request) => { const httpClient = createHttpClient({ fetch }); tests.forEach((data) => { + if (data.test !== "getApiInfo") return; it(data.test, async () => { if (data.httpRequest) { const result = await fetch( diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index caac2e8..6cde8fe 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -576,13 +576,44 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) { code.WriteLine($"api: I{capModuleName};"); code.WriteLine("caseInsenstiveQueryStringKeys?: boolean;"); + code.WriteLine("includeErrorDetails?: boolean;"); } } code.WriteLine(); using (code.Block($"export const {camelCaseModuleName}Plugin" + IfTypeScript($": FastifyPluginAsync<{capModuleName}PluginOptions>") + " = async (fastify, opts) => {", "}")) { - code.WriteLine("const { api, caseInsenstiveQueryStringKeys } = opts;"); + code.WriteLine("const { api, caseInsenstiveQueryStringKeys, includeErrorDetails } = opts;"); + + code.WriteLine(); + using (code.Block("fastify.setErrorHandler((error, req, res) => {", "});")) + { + code.WriteLine("req.log.error(error);"); + using (code.Block("if (includeErrorDetails) {", "}")) + { + code.WriteLine("res.status(500).send({"); + using (code.Indent()) + { + code.WriteLine("code: 'InternalError',"); + code.WriteLine("message: error.message,"); + using (code.Block("details: {", "}")) + { + code.WriteLine("stack: error.stack?.split('\\n').filter((x) => x.length > 0),"); + } + } + code.WriteLine("});"); + } + using (code.Block("else {", "}")) + { + code.WriteLine("res.status(500).send({"); + using (code.Indent()) + { + code.WriteLine("code: 'InternalError',"); + code.WriteLine("message: 'The service experienced an unexpected internal error.',"); + } + code.WriteLine("});"); + } + } code.WriteLine(); using (code.Block("if (caseInsenstiveQueryStringKeys) {", "}")) From c625c24c46f61538d1a9c597d2c4e4dceeb77b15 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Mon, 18 Dec 2023 15:39:46 -0800 Subject: [PATCH 18/19] Add clarifying doc comments. --- README.md | 15 +++++++++++++++ .../JavaScriptGenerator.cs | 3 +++ .../JavaScriptGeneratorSettings.cs | 3 +++ src/fsdgenjs/FsdGenJavaScriptApp.cs | 2 +- 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29cf797..83cd269 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,18 @@ fsdgenjs | A tool that generates JavaScript or TypeScript for a Facility Service Facility.CodeGen.JavaScript | A library that generates JavaScript or TypeScript for a Facility Service Definition. | [![NuGet](https://img.shields.io/nuget/v/Facility.CodeGen.JavaScript.svg)](https://www.nuget.org/packages/Facility.CodeGen.JavaScript) [Documentation](https://facilityapi.github.io/) | [Release Notes](https://github.com/FacilityApi/FacilityJavaScript/blob/master/ReleaseNotes.md) | [Contributing](https://github.com/FacilityApi/FacilityJavaScript/blob/master/CONTRIBUTING.md) + +## Conformance + +To run conformance tests, first start one of the conformance servers from within the `/conformance` folder: + +``` +npm run fastify:ts +npm run fastify:js +``` + +Then run the conformance tool against the running service. + +``` +npm run test +``` diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 6cde8fe..62de7cb 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -36,6 +36,9 @@ public static int GenerateJavaScript(JavaScriptGeneratorSettings settings) => /// /// True to generate Fastify plugin. /// + /// + /// When specified, only the server plugin is generated, not the client. + /// public bool Fastify { get; set; } /// diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs index 45ffb04..dbf0b2e 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGeneratorSettings.cs @@ -25,6 +25,9 @@ public sealed class JavaScriptGeneratorSettings : FileGeneratorSettings /// /// True to generate Fastify plugin. /// + /// + /// When specified, only the server plugin is generated, not the client. + /// public bool Fastify { get; set; } /// diff --git a/src/fsdgenjs/FsdGenJavaScriptApp.cs b/src/fsdgenjs/FsdGenJavaScriptApp.cs index 9425fc9..45437d7 100644 --- a/src/fsdgenjs/FsdGenJavaScriptApp.cs +++ b/src/fsdgenjs/FsdGenJavaScriptApp.cs @@ -23,7 +23,7 @@ public sealed class FsdGenJavaScriptApp : CodeGeneratorApp " --express", " Generates Express service.", " --fastify", - " Generates Fastify plugin.", + " Generates a Fastify plugin. When specified, only the server plugin is generated, not the client.", " --disable-eslint", " Disables ESLint via code comment.", }; From 0c72936001f8deea44767d0234e7e45e95a674e1 Mon Sep 17 00:00:00 2001 From: Mitch Rosenburg Date: Tue, 19 Dec 2023 06:23:07 -0800 Subject: [PATCH 19/19] Run conformance js and ts fastify plugins together. As well, we are now running all conformance tests together, testing all client and server code. --- README.md | 3 +- conformance/package.json | 3 +- conformance/src/{ts => }/conformanceApi.ts | 0 .../src/{ts => }/conformanceApiService.ts | 0 .../src/{ts => }/conformanceApiTypes.ts | 0 conformance/src/fastify/app.ts | 21 ++++++ .../{ts => }/fastify/conformanceApiPlugin.ts | 4 +- .../jsConformanceApiPlugin.js} | 2 +- conformance/src/js/fastify/app.js | 16 ----- .../conformanceApi.js => jsConformanceApi.js} | 6 +- conformance/src/ts/fastify/app.ts | 20 ------ conformance/test/conformanceApiTests.ts | 66 ++++++++++--------- .../JavaScriptGenerator.cs | 6 +- tools/Build/Build.cs | 8 +-- 14 files changed, 72 insertions(+), 83 deletions(-) rename conformance/src/{ts => }/conformanceApi.ts (100%) rename conformance/src/{ts => }/conformanceApiService.ts (100%) rename conformance/src/{ts => }/conformanceApiTypes.ts (100%) create mode 100644 conformance/src/fastify/app.ts rename conformance/src/{ts => }/fastify/conformanceApiPlugin.ts (99%) rename conformance/src/{js/fastify/conformanceApiPlugin.js => fastify/jsConformanceApiPlugin.js} (99%) delete mode 100644 conformance/src/js/fastify/app.js rename conformance/src/{js/conformanceApi.js => jsConformanceApi.js} (99%) delete mode 100644 conformance/src/ts/fastify/app.ts diff --git a/README.md b/README.md index 83cd269..80b8c2d 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,7 @@ Facility.CodeGen.JavaScript | A library that generates JavaScript or TypeScript To run conformance tests, first start one of the conformance servers from within the `/conformance` folder: ``` -npm run fastify:ts -npm run fastify:js +npm run fastify ``` Then run the conformance tool against the running service. diff --git a/conformance/package.json b/conformance/package.json index d1c341f..9f58ec8 100644 --- a/conformance/package.json +++ b/conformance/package.json @@ -5,8 +5,7 @@ "scripts": { "build": "tsc", "test": "npm run --silent build && mocha dist/test/conformanceApiTests.js", - "fastify:ts": "npm run --silent build && fastify start --options -p 4117 -w -l info -P dist/src/ts/fastify/app.js", - "fastify:js": "npm run --silent build && fastify start --options -p 4117 -w -l info -P dist/src/js/fastify/app.js" + "fastify": "npm run --silent build && fastify start --options -p 4117 -w -l info -P dist/src/fastify/app.js" }, "repository": { "type": "git", diff --git a/conformance/src/ts/conformanceApi.ts b/conformance/src/conformanceApi.ts similarity index 100% rename from conformance/src/ts/conformanceApi.ts rename to conformance/src/conformanceApi.ts diff --git a/conformance/src/ts/conformanceApiService.ts b/conformance/src/conformanceApiService.ts similarity index 100% rename from conformance/src/ts/conformanceApiService.ts rename to conformance/src/conformanceApiService.ts diff --git a/conformance/src/ts/conformanceApiTypes.ts b/conformance/src/conformanceApiTypes.ts similarity index 100% rename from conformance/src/ts/conformanceApiTypes.ts rename to conformance/src/conformanceApiTypes.ts diff --git a/conformance/src/fastify/app.ts b/conformance/src/fastify/app.ts new file mode 100644 index 0000000..6612dea --- /dev/null +++ b/conformance/src/fastify/app.ts @@ -0,0 +1,21 @@ +import { FastifyPluginAsync, FastifyServerOptions } from "fastify"; +import { ConformanceApiService } from "../conformanceApiService.js"; +import conformanceTestsJson from "../../ConformanceTests.json"; +import { conformanceApiPlugin, ConformanceApiPluginOptions } from "./conformanceApiPlugin.js"; +import { jsConformanceApiPlugin } from "./jsConformanceApiPlugin.js"; + +const app: FastifyPluginAsync = async (fastify): Promise => { + const conformanceApiPluginOptions: ConformanceApiPluginOptions = { + api: new ConformanceApiService(conformanceTestsJson.tests), + caseInsenstiveQueryStringKeys: true, + includeErrorDetails: true, + }; + + fastify.register(conformanceApiPlugin, conformanceApiPluginOptions); + fastify.register(jsConformanceApiPlugin, {...conformanceApiPluginOptions, prefix: "/js" }); +}; + +export default app; +export const options: FastifyServerOptions = { + caseSensitive: false, +}; diff --git a/conformance/src/ts/fastify/conformanceApiPlugin.ts b/conformance/src/fastify/conformanceApiPlugin.ts similarity index 99% rename from conformance/src/ts/fastify/conformanceApiPlugin.ts rename to conformance/src/fastify/conformanceApiPlugin.ts index 6f5bd68..dc1d797 100644 --- a/conformance/src/ts/fastify/conformanceApiPlugin.ts +++ b/conformance/src/fastify/conformanceApiPlugin.ts @@ -1,7 +1,7 @@ // DO NOT EDIT: generated by fsdgenjs /* eslint-disable */ -import { FastifyPluginAsync } from 'fastify'; +import { FastifyPluginAsync, RegisterOptions } from 'fastify'; import { IServiceResult, IServiceError } from 'facility-core'; const standardErrorCodes: { [code: string]: number } = { @@ -32,7 +32,7 @@ function parseBoolean(value: string | undefined) { return undefined; } -export type ConformanceApiPluginOptions = { +export type ConformanceApiPluginOptions = RegisterOptions & { api: IConformanceApi; caseInsenstiveQueryStringKeys?: boolean; includeErrorDetails?: boolean; diff --git a/conformance/src/js/fastify/conformanceApiPlugin.js b/conformance/src/fastify/jsConformanceApiPlugin.js similarity index 99% rename from conformance/src/js/fastify/conformanceApiPlugin.js rename to conformance/src/fastify/jsConformanceApiPlugin.js index 1bf09d7..ab257a0 100644 --- a/conformance/src/js/fastify/conformanceApiPlugin.js +++ b/conformance/src/fastify/jsConformanceApiPlugin.js @@ -31,7 +31,7 @@ function parseBoolean(value) { return undefined; } -export const conformanceApiPlugin = async (fastify, opts) => { +export const jsConformanceApiPlugin = async (fastify, opts) => { const { api, caseInsenstiveQueryStringKeys, includeErrorDetails } = opts; fastify.setErrorHandler((error, req, res) => { diff --git a/conformance/src/js/fastify/app.js b/conformance/src/js/fastify/app.js deleted file mode 100644 index fb083e7..0000000 --- a/conformance/src/js/fastify/app.js +++ /dev/null @@ -1,16 +0,0 @@ -import { ConformanceApiService } from "../../ts/conformanceApiService.js"; -import conformanceTestsJson from "../../../ConformanceTests.json"; -import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; - -const app = async (fastify, opts) => { - fastify.register(conformanceApiPlugin, { - api: new ConformanceApiService(conformanceTestsJson.tests), - caseInsenstiveQueryStringKeys: true, - includeErrorDetails: true, - }); -}; - -export default app; -export const options = { - caseSensitive: false, -}; diff --git a/conformance/src/js/conformanceApi.js b/conformance/src/jsConformanceApi.js similarity index 99% rename from conformance/src/js/conformanceApi.js rename to conformance/src/jsConformanceApi.js index c42987f..d1559ee 100644 --- a/conformance/src/js/conformanceApi.js +++ b/conformance/src/jsConformanceApi.js @@ -4,9 +4,9 @@ import { HttpClientUtility } from 'facility-core'; -/** Provides access to ConformanceApi over HTTP via fetch. */ +/** Provides access to JsConformanceApi over HTTP via fetch. */ export function createHttpClient({ fetch, baseUri }) { - return new ConformanceApiHttpClient(fetch, baseUri); + return new JsConformanceApiHttpClient(fetch, baseUri); } const { fetchResponse, createResponseError, createRequiredRequestFieldError } = HttpClientUtility; @@ -24,7 +24,7 @@ function parseBoolean(value) { return undefined; } -class ConformanceApiHttpClient { +class JsConformanceApiHttpClient { constructor(fetch, baseUri) { if (typeof fetch !== 'function') { throw new TypeError('fetch must be a function.'); diff --git a/conformance/src/ts/fastify/app.ts b/conformance/src/ts/fastify/app.ts deleted file mode 100644 index 5b5ef87..0000000 --- a/conformance/src/ts/fastify/app.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { FastifyPluginAsync, FastifyServerOptions } from "fastify"; -import { ConformanceApiService } from "../conformanceApiService.js"; -import conformanceTestsJson from "../../../ConformanceTests.json"; -import { conformanceApiPlugin } from "./conformanceApiPlugin.js"; - -const app: FastifyPluginAsync = async ( - fastify, - opts -): Promise => { - fastify.register(conformanceApiPlugin, { - api: new ConformanceApiService(conformanceTestsJson.tests), - caseInsenstiveQueryStringKeys: true, - includeErrorDetails: true, - }); -}; - -export default app; -export const options: FastifyServerOptions = { - caseSensitive: false, -}; diff --git a/conformance/test/conformanceApiTests.ts b/conformance/test/conformanceApiTests.ts index 6b66c62..81b5212 100644 --- a/conformance/test/conformanceApiTests.ts +++ b/conformance/test/conformanceApiTests.ts @@ -1,45 +1,48 @@ -import { createHttpClient } from "../src/ts/conformanceApi"; +import { createHttpClient } from "../src/conformanceApi"; +import { createHttpClient as jsCreateHttpClient } from "../src/jsConformanceApi"; import { expect, should } from "chai"; -import nodeFetch from "node-fetch"; +import fetch from "node-fetch"; import conformanceTestsJson from "../ConformanceTests.json"; import { isDeepStrictEqual } from "util"; -import { HttpClientUtility } from "facility-core"; const tests = conformanceTestsJson.tests; validateTests(); should(); -const fetch: HttpClientUtility.IFetch = (uri, request) => { - return nodeFetch("http://localhost:4117/" + uri, request); -}; +const clients = [ + { + baseUri: "http://localhost:4117/", + createHttpClient: createHttpClient, + }, + { + baseUri: "http://localhost:4117/js/", + createHttpClient: jsCreateHttpClient as never, + }, +]; -const httpClient = createHttpClient({ fetch }); - -tests.forEach((data) => { - if (data.test !== "getApiInfo") return; - it(data.test, async () => { - if (data.httpRequest) { - const result = await fetch( - data.httpRequest.path.replace(/^\//, ""), - { method: data.httpRequest.method }); - if (result.status >= 300) { - throw new Error( - `Raw http request failed with status code ${ - result.status - }, ${JSON.stringify(await result.json())}` - ); - } - } else { - const result = await(httpClient as any)[data.method](data.request); - expect({ - error: result.error ?? undefined, - value: result.value ?? undefined, - }).to.be.deep.equal({ - error: data.error ?? undefined, - value: data.response ?? undefined, +clients.forEach(({baseUri, createHttpClient}) => { + describe(`ConformanceApi (${baseUri})`, () => { + const httpClient = createHttpClient({ fetch, baseUri}); + tests.forEach((data) => { + it(data.test, async () => { + if (data.httpRequest) { + const result = await fetch(baseUri + data.httpRequest.path.replace(/^\//, ""), { method: data.httpRequest.method }); + if (result.status >= 300) { + throw new Error(`Raw http request failed with status code ${result.status}, ${JSON.stringify(await result.json())}`); + } + } else { + const result = await (httpClient as any)[data.method](data.request); + expect({ + error: result.error ?? undefined, + value: result.value ?? undefined, + }).to.be.deep.equal({ + error: data.error ?? undefined, + value: data.response ?? undefined, + }); + } }); - } + }); }); }); @@ -65,4 +68,3 @@ function validateTests() { } }); } - diff --git a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs index 62de7cb..6d8b11d 100644 --- a/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs +++ b/src/Facility.CodeGen.JavaScript/JavaScriptGenerator.cs @@ -555,7 +555,11 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) var fastifyImports = new List(); if (TypeScript) + { fastifyImports.Add("FastifyPluginAsync"); + fastifyImports.Add("RegisterOptions"); + } + WriteImports(code, fastifyImports, "fastify"); var facilityImports = new List(); @@ -575,7 +579,7 @@ private CodeGenOutput GenerateFastifyPluginOutput(ServiceInfo service) if (TypeScript) { code.WriteLine(); - using (code.Block($"export type {capModuleName}PluginOptions = {{", "}")) + using (code.Block($"export type {capModuleName}PluginOptions = RegisterOptions & {{", "}")) { code.WriteLine($"api: I{capModuleName};"); code.WriteLine("caseInsenstiveQueryStringKeys?: boolean;"); diff --git a/tools/Build/Build.cs b/tools/Build/Build.cs index bbdad3f..3e29038 100644 --- a/tools/Build/Build.cs +++ b/tools/Build/Build.cs @@ -47,11 +47,11 @@ void CodeGen(bool verify) RunDotNet("FacilityConformance", "fsd", "--output", "conformance/ConformanceApi.fsd", verifyOption); RunDotNet("FacilityConformance", "json", "--output", "conformance/ConformanceTests.json", verifyOption); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/js/", "--indent", "2", "--disable-eslint"); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/js/fastify", "--fastify", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/", "--typescript", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/fastify", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/ts/", "--typescript", "--indent", "2", "--disable-eslint"); - RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/ts/fastify", "--typescript", "--fastify", "--indent", "2", "--disable-eslint"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/", "--indent", "2", "--disable-eslint", "--module", "jsConformanceApi"); + RunCodeGen("conformance/ConformanceApi.fsd", "conformance/src/fastify/", "--fastify", "--indent", "2", "--disable-eslint", "--module", "jsConformanceApi"); void RunCodeGen(params string?[] args) => RunDotNet(new[] { "run", "--no-build", "--project", $"src/{codegen}", "-f", "net6.0", "-c", configuration, "--", "--newline", "lf", verifyOption }.Concat(args));