diff --git a/.gitignore b/.gitignore index c011ebff8..0957ef648 100644 --- a/.gitignore +++ b/.gitignore @@ -49,7 +49,6 @@ test-results/ site/ -biome-main/ .review/ pglinter_repo/ .review/ diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..590e8515c --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "singleQuote": false, + "experimentalSortImports": {} +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..3dca2b876 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,20 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "categories": { + "correctness": "error", + "suspicious": "warn" + }, + "rules": { + "no-shadow": "off", + "unicorn/require-post-message-target-origin": "off", + "unicorn/prefer-add-event-listener": "off" + }, + "overrides": [ + { + "files": ["**/e2e/**", "**/tests/**"], + "rules": { + "typescript/no-explicit-any": "off" + } + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 64d876302..1836eb193 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,7 +40,7 @@ just format # Lint entire codebase just lint -# or: cargo clippy && cargo run -p rules_check && bun biome lint +# or: cargo clippy && cargo run -p rules_check && bun oxlint packages/ # Fix linting issues just lint-fix @@ -137,7 +137,7 @@ cargo insta review - `clippy.toml` - Clippy linting configuration ### Other Tools -- `biome.jsonc` - Biome formatter/linter configuration for JS/TS +- `.oxlintrc.json` / `.oxfmtrc.json` - oxlint linter and oxfmt formatter configuration for JS/TS - `taplo.toml` - TOML formatting configuration - `justfile` - Task runner with all development commands - `docker-compose.yml` - Database setup for testing diff --git a/biome.jsonc b/biome.jsonc deleted file mode 100644 index 7aaf8918c..000000000 --- a/biome.jsonc +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": false, - "ignore": [], - "include": ["packages/**/*"] - }, - "formatter": { - "enabled": true - }, - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - }, - "overrides": [ - { - "include": ["**/e2e/**", "**/tests/**"], - "linter": { - "rules": { - "suspicious": { - "noExplicitAny": "off" - } - } - } - } - ] -} diff --git a/bun.lock b/bun.lock index f26512f35..0026ec815 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,9 @@ "": { "name": "postgres_lsp", "devDependencies": { - "@biomejs/biome": "1.9.4", "@types/bun": "latest", + "oxfmt": "0.34.0", + "oxlint": "1.49.0", }, "peerDependencies": { "typescript": "^5", @@ -74,23 +75,81 @@ }, }, "packages": { - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.34.0", "", { "os": "android", "cpu": "arm" }, "sha512-sqkqjh/Z38l+duOb1HtVqJTAj1grt2ttkobCopC/72+a4Xxz4xUgZPFyQ4HxrYMvyqO/YA0tvM1QbfOu70Gk1Q=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.34.0", "", { "os": "android", "cpu": "arm64" }, "sha512-1KRCtasHcVcGOMwfOP9d5Bus2NFsN8yAYM5cBwi8LBg5UtXC3C49WHKrlEa8iF1BjOS6CR2qIqiFbGoA0DJQNQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.34.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b+Rmw9Bva6e/7PBES2wLO8sEU7Mi0+/Kv+pXSe/Y8i4fWNftZZlGwp8P01eECaUqpXATfSgNxdEKy7+ssVNz7g=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.34.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-QGjpevWzf1T9COEokZEWt80kPOtthW1zhRbo7x4Qoz646eTTfi6XsHG2uHeDWJmTbgBoJZPMgj2TAEV/ppEZaA=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.34.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VMSaC02cG75qL59M9M/szEaqq/RsLfgpzQ4nqUu8BUnX1zkiZIW2gTpUv3ZJ6qpWnHxIlAXiRZjQwmcwpvtbcg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Klm367PFJhH6vYK3vdIOxFepSJZHPaBfIuqwxdkOcfSQ4qqc/M8sgK0UTFnJWWTA/IkhMIh1kW6uEqiZ/xtQqg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.34.0", "", { "os": "linux", "cpu": "arm" }, "sha512-nqn0QueVXRfbN9m58/E9Zij0Ap8lzayx591eWBYn0sZrGzY1IRv9RYS7J/1YUXbb0Ugedo0a8qIWzUHU9bWQuA=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DDn+dcqW+sMTCEjvLoQvC/VWJjG7h8wcdN/J+g7ZTdf/3/Dx730pQElxPPGsCXPhprb11OsPyMp5FwXjMY3qvA=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.34.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.34.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.34.0", "", { "os": "linux", "cpu": "none" }, "sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.34.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.34.0", "", { "os": "linux", "cpu": "x64" }, "sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.34.0", "", { "os": "none", "cpu": "arm64" }, "sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.34.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6id8kK0t5hKfbV6LHDzRO21wRTA6ctTlKGTZIsG/mcoir0rssvaYsedUymF4HDj7tbCUlnxCX/qOajKlEuqbIw=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.34.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHaz+w673mlYqn9v/+fuiKZpjkmagleXQ+NygShDv8tdHpRYX2oYhTJwwt9j1ZfVhRgza1EIUW3JmzCXmtPdhQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.34.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CXKQM/VaF+yuvGru8ktleHLJoBdjBtTFmAsLGePiESiTN0NjCI/PiaiOCfHMJ1HdP1LykvARUwMvgaN3tDhcrg=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-2WPoh/2oK9r/i2R4o4J18AOrm3HVlWiHZ8TnuCaS4dX8m5ZzRmHW0I3eLxEurQLHWVruhQN7fHgZnah+ag5iQg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.49.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YqJAGvNB11EzoKm1euVhZntb79alhMvWW/j12bYqdvVxn6xzEQWrEDCJg9BPo3A3tBCSUBKH7bVkAiCBqK/L1w=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.49.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WFocCRlvVkMhChCJ2qpJfp1Gj/IjvyjuifH9Pex8m8yHonxxQa3d8DZYreuDQU3T4jvSY8rqhoRqnpc61Nlbxw=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.49.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BN0KniwvehbUfYztOMwEDkYoojGm/narf5oJf+/ap+6PnzMeWLezMaVARNIS0j3OdMkjHTEP8s3+GdPJ7WDywQ=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.49.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SnkAc/DPIY6joMCiP/+53Q+N2UOGMU6ULvbztpmvPJNF/jYPGhNbKtN982uj2Gs6fpbxYkmyj08QnpkD4fbHJA=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-6Z3EzRvpQVIpO7uFhdiGhdE8Mh3S2VWKLL9xuxVqD6fzPhyI3ugthpYXlCChXzO8FzcYIZ3t1+Kau+h2NY1hqA=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wdjXaQYAL/L25732mLlngfst4Jdmi/HLPVHb3yfCoP5mE3lO/pFFrmOJpqWodgv29suWY74Ij+RmJ/YIG5VuzQ=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oSHpm8zmSvAG1BWUumbDRSg7moJbnwoEXKAkwDf/xTQJOzvbUknq95NVQdw/AduZr5dePftalB8rzJNGBogUMg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.49.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.49.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.49.0", "", { "os": "none", "cpu": "arm64" }, "sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.49.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-6rrKe/wL9tn0qnOy76i1/0f4Dc3dtQnibGlU4HqR/brVHlVjzLSoaH0gAFnLnznh9yQ6gcFTBFOPrcN/eKPDGA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.49.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-CXHLWAtLs2xG/aVy1OZiYJzrULlq0QkYpI6cd7VKMrab+qur4fXVE/B1Bp1m0h1qKTj5/FTGg6oU4qaXMjS/ug=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.49.0", "", { "os": "win32", "cpu": "x64" }, "sha512-VteIelt78kwzSglOozaQcs6BCS4Lk0j+QA+hGV0W8UeyaqQ3XpbZRhDU55NW1PPvCy1tg4VXsTlEaPovqto7nQ=="], "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], @@ -140,10 +199,16 @@ "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "oxfmt": ["oxfmt@0.34.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.34.0", "@oxfmt/binding-android-arm64": "0.34.0", "@oxfmt/binding-darwin-arm64": "0.34.0", "@oxfmt/binding-darwin-x64": "0.34.0", "@oxfmt/binding-freebsd-x64": "0.34.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.34.0", "@oxfmt/binding-linux-arm-musleabihf": "0.34.0", "@oxfmt/binding-linux-arm64-gnu": "0.34.0", "@oxfmt/binding-linux-arm64-musl": "0.34.0", "@oxfmt/binding-linux-ppc64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-gnu": "0.34.0", "@oxfmt/binding-linux-riscv64-musl": "0.34.0", "@oxfmt/binding-linux-s390x-gnu": "0.34.0", "@oxfmt/binding-linux-x64-gnu": "0.34.0", "@oxfmt/binding-linux-x64-musl": "0.34.0", "@oxfmt/binding-openharmony-arm64": "0.34.0", "@oxfmt/binding-win32-arm64-msvc": "0.34.0", "@oxfmt/binding-win32-ia32-msvc": "0.34.0", "@oxfmt/binding-win32-x64-msvc": "0.34.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-t+zTE4XGpzPTK+Zk9gSwcJcFi4pqjl6PwO/ZxPBJiJQ2XCKMucwjPlHxvPHyVKJtkMSyrDGfQ7Ntg/hUr4OgHQ=="], + + "oxlint": ["oxlint@1.49.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.49.0", "@oxlint/binding-android-arm64": "1.49.0", "@oxlint/binding-darwin-arm64": "1.49.0", "@oxlint/binding-darwin-x64": "1.49.0", "@oxlint/binding-freebsd-x64": "1.49.0", "@oxlint/binding-linux-arm-gnueabihf": "1.49.0", "@oxlint/binding-linux-arm-musleabihf": "1.49.0", "@oxlint/binding-linux-arm64-gnu": "1.49.0", "@oxlint/binding-linux-arm64-musl": "1.49.0", "@oxlint/binding-linux-ppc64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-gnu": "1.49.0", "@oxlint/binding-linux-riscv64-musl": "1.49.0", "@oxlint/binding-linux-s390x-gnu": "1.49.0", "@oxlint/binding-linux-x64-gnu": "1.49.0", "@oxlint/binding-linux-x64-musl": "1.49.0", "@oxlint/binding-openharmony-arm64": "1.49.0", "@oxlint/binding-win32-arm64-msvc": "1.49.0", "@oxlint/binding-win32-ia32-msvc": "1.49.0", "@oxlint/binding-win32-x64-msvc": "1.49.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-YZffp0gM+63CJoRhHjtjRnwKtAgUnXM6j63YQ++aigji2NVvLGsUlrXo9gJUXZOdcbfShLYtA6RuTu8GZ4lzOQ=="], + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], diff --git a/justfile b/justfile index 39425293a..07aff5fb8 100644 --- a/justfile +++ b/justfile @@ -43,17 +43,17 @@ new-lintrule group rulename severity="error": format: cargo fmt taplo format - bun biome format --write + bun oxfmt packages/ format-ci: cargo fmt --all --check taplo format --check - bun biome format + bun oxfmt --check packages/ format-ci-versions: cargo --version taplo --version - echo "Biome $(bun biome --version)" + bun oxfmt --version [unix] _touch file: @@ -79,12 +79,12 @@ test-doc: lint: cargo clippy cargo run -p rules_check - bun biome lint + bun oxlint packages/ lint-fix: cargo clippy --fix cargo run -p rules_check - bun biome lint --write + bun oxlint --fix packages/ lint-ci-versions: rustc --version @@ -92,13 +92,13 @@ lint-ci-versions: cargo --version cargo sqlx --version cargo clippy --version - echo "Biome $(bun biome --version)" + bun oxlint --version lint-ci: cargo sqlx prepare --check --workspace cargo clippy --fix cargo run -p rules_check - bun biome lint --write + bun oxlint --fix packages/ serve-docs: uv sync diff --git a/package.json b/package.json index e7692c4b9..72eef5bf1 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "0.0.0", "private": true, "devDependencies": { - "@biomejs/biome": "1.9.4", + "oxlint": "1.49.0", + "oxfmt": "0.34.0", "@types/bun": "latest" }, "peerDependencies": { diff --git a/packages/@postgres-language-server/backend-jsonrpc/package.json b/packages/@postgres-language-server/backend-jsonrpc/package.json index 293e8430f..96d8f798d 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/package.json +++ b/packages/@postgres-language-server/backend-jsonrpc/package.json @@ -1,32 +1,38 @@ { - "name": "@postgres-language-server/backend-jsonrpc", - "version": "", - "main": "dist/index.js", - "scripts": { - "test": "bun test", - "test:ci": "bun build && bun test", - "build": "bun build ./src/index.ts --outdir ./dist --target node" - }, - "files": ["dist/", "README.md"], - "repository": { - "type": "git", - "url": "git+https://github.com/supabase-community/postgres-language-server.git", - "directory": "packages/@postgres-language-server/backend-jsonrpc" - }, - "author": "Supabase Community", - "bugs": "ttps://github.com/supabase-community/postgres-language-server/issues", - "description": "Bindings to the JSON-RPC Workspace API of the Postgres Language Tools daemon", - "keywords": ["TypeScript", "Postgres"], - "license": "MIT", - "publishConfig": { - "provenance": true - }, - "optionalDependencies": { - "@postgres-language-server/cli-win32-x64": "", - "@postgres-language-server/cli-win32-arm64": "", - "@postgres-language-server/cli-darwin-x64": "", - "@postgres-language-server/cli-darwin-arm64": "", - "@postgres-language-server/cli-linux-x64": "", - "@postgres-language-server/cli-linux-arm64": "" - } + "name": "@postgres-language-server/backend-jsonrpc", + "version": "", + "description": "Bindings to the JSON-RPC Workspace API of the Postgres Language Tools daemon", + "keywords": [ + "Postgres", + "TypeScript" + ], + "bugs": "ttps://github.com/supabase-community/postgres-language-server/issues", + "license": "MIT", + "author": "Supabase Community", + "repository": { + "type": "git", + "url": "git+https://github.com/supabase-community/postgres-language-server.git", + "directory": "packages/@postgres-language-server/backend-jsonrpc" + }, + "files": [ + "dist/", + "README.md" + ], + "main": "dist/index.js", + "publishConfig": { + "provenance": true + }, + "scripts": { + "test": "bun test", + "test:ci": "bun build && bun test", + "build": "bun build ./src/index.ts --outdir ./dist --target node" + }, + "optionalDependencies": { + "@postgres-language-server/cli-darwin-arm64": "", + "@postgres-language-server/cli-darwin-x64": "", + "@postgres-language-server/cli-linux-arm64": "", + "@postgres-language-server/cli-linux-x64": "", + "@postgres-language-server/cli-win32-arm64": "", + "@postgres-language-server/cli-win32-x64": "" + } } diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/command.ts b/packages/@postgres-language-server/backend-jsonrpc/src/command.ts index b36677567..f20389b50 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/command.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/command.ts @@ -6,72 +6,66 @@ import { execSync } from "node:child_process"; * @returns Filesystem path to the binary, or null if no prebuilt distribution exists for the current platform */ export function getCommand(): string | null { - const { platform, arch } = process; + const { platform, arch } = process; - const PLATFORMS: Partial< - Record< - NodeJS.Platform | "linux-musl", - Partial> - > - > = { - win32: { - x64: "@postgres-language-server/cli-x86_64-windows-msvc/postgres-language-server.exe", - arm64: - "@postgres-language-server/cli-aarch64-windows-msvc/postgres-language-server.exe", - }, - darwin: { - x64: "@postgres-language-server/cli-x86_64-apple-darwin/postgres-language-server", - arm64: - "@postgres-language-server/cli-aarch64-apple-darwin/postgres-language-server", - }, - linux: { - x64: "@postgres-language-server/cli-x86_64-linux-gnu/postgres-language-server", - arm64: - "@postgres-language-server/cli-aarch64-linux-gnu/postgres-language-server", - }, - "linux-musl": { - x64: "@postgres-language-server/cli-x86_64-linux-musl/postgres-language-server", - // no arm64 build for musl - }, - }; + const PLATFORMS: Partial< + Record>> + > = { + win32: { + x64: "@postgres-language-server/cli-x86_64-windows-msvc/postgres-language-server.exe", + arm64: "@postgres-language-server/cli-aarch64-windows-msvc/postgres-language-server.exe", + }, + darwin: { + x64: "@postgres-language-server/cli-x86_64-apple-darwin/postgres-language-server", + arm64: "@postgres-language-server/cli-aarch64-apple-darwin/postgres-language-server", + }, + linux: { + x64: "@postgres-language-server/cli-x86_64-linux-gnu/postgres-language-server", + arm64: "@postgres-language-server/cli-aarch64-linux-gnu/postgres-language-server", + }, + "linux-musl": { + x64: "@postgres-language-server/cli-x86_64-linux-musl/postgres-language-server", + // no arm64 build for musl + }, + }; - function isMusl() { - let stderr = ""; - try { - stderr = execSync("ldd --version", { - stdio: [ - "ignore", // stdin - "pipe", // stdout – glibc systems print here - "pipe", // stderr – musl systems print here - ], - }).toString(); - } catch (err: unknown) { - if (hasStdErr(err)) { - stderr = err.stderr; - } - } - if (stderr.indexOf("musl") > -1) { - return true; - } - return false; - } + function isMusl() { + let stderr = ""; + try { + stderr = execSync("ldd --version", { + stdio: [ + "ignore", // stdin + "pipe", // stdout – glibc systems print here + "pipe", // stderr – musl systems print here + ], + }).toString(); + } catch (err: unknown) { + if (hasStdErr(err)) { + stderr = err.stderr; + } + } + if (stderr.indexOf("musl") > -1) { + return true; + } + return false; + } - function getPlatform(): NodeJS.Platform | "linux-musl" { - if (platform === "linux") { - return isMusl() ? "linux-musl" : "linux"; - } + function getPlatform(): NodeJS.Platform | "linux-musl" { + if (platform === "linux") { + return isMusl() ? "linux-musl" : "linux"; + } - return platform; - } + return platform; + } - const binPath = PLATFORMS?.[getPlatform()]?.[arch]; - if (!binPath) { - return null; - } + const binPath = PLATFORMS?.[getPlatform()]?.[arch]; + if (!binPath) { + return null; + } - return require.resolve(binPath); + return require.resolve(binPath); } function hasStdErr(err: unknown): err is { stderr: string } { - return typeof err === "object" && err !== null && "stderr" in err; + return typeof err === "object" && err !== null && "stderr" in err; } diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/index.ts b/packages/@postgres-language-server/backend-jsonrpc/src/index.ts index f0f11151f..767645860 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/index.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/index.ts @@ -10,12 +10,12 @@ import { type Workspace, createWorkspace as wrapTransport } from "./workspace"; * @returns A Workspace client, or null if the underlying platform is not supported */ export async function createWorkspace(): Promise { - const command = getCommand(); - if (!command) { - return null; - } + const command = getCommand(); + if (!command) { + return null; + } - return createWorkspaceWithBinary(command); + return createWorkspaceWithBinary(command); } /** @@ -26,21 +26,19 @@ export async function createWorkspace(): Promise { * @param command Path to the binary * @returns A Workspace client, or null if the underlying platform is not supported */ -export async function createWorkspaceWithBinary( - command: string, -): Promise { - const socket = await createSocket(command); - const transport = new Transport(socket); +export async function createWorkspaceWithBinary(command: string): Promise { + const socket = await createSocket(command); + const transport = new Transport(socket); - await transport.request("initialize", { - capabilities: {}, - client_info: { - name: "@postgres-language-server/backend-jsonrpc", - version: "0.0.0", - }, - }); + await transport.request("initialize", { + capabilities: {}, + client_info: { + name: "@postgres-language-server/backend-jsonrpc", + version: "0.0.0", + }, + }); - return wrapTransport(transport); + return wrapTransport(transport); } export * from "./workspace"; diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/socket.ts b/packages/@postgres-language-server/backend-jsonrpc/src/socket.ts index 6fd2902f9..dda919b0e 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/socket.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/socket.ts @@ -2,30 +2,26 @@ import { spawn } from "node:child_process"; import { type Socket, connect } from "node:net"; function getSocket(command: string): Promise { - return new Promise((resolve, reject) => { - const process = spawn(command, ["__print_socket"], { - stdio: "pipe", - }); + return new Promise((resolve, reject) => { + const process = spawn(command, ["__print_socket"], { + stdio: "pipe", + }); - process.on("error", reject); + process.on("error", reject); - let pipeName = ""; - process.stdout.on("data", (data) => { - pipeName += data.toString("utf-8"); - }); + let pipeName = ""; + process.stdout.on("data", (data) => { + pipeName += data.toString("utf-8"); + }); - process.on("exit", (code) => { - if (code === 0) { - resolve(pipeName.trimEnd()); - } else { - reject( - new Error( - `Command '${command} __print_socket' exited with code ${code}`, - ), - ); - } - }); - }); + process.on("exit", (code) => { + if (code === 0) { + resolve(pipeName.trimEnd()); + } else { + reject(new Error(`Command '${command} __print_socket' exited with code ${code}`)); + } + }); + }); } /** @@ -35,13 +31,13 @@ function getSocket(command: string): Promise { * @returns Socket instance connected to the daemon */ export async function createSocket(command: string): Promise { - const path = await getSocket(command); - const socket = connect(path); + const path = await getSocket(command); + const socket = connect(path); - await new Promise((resolve, reject) => { - socket.once("error", reject); - socket.once("ready", resolve); - }); + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.once("ready", resolve); + }); - return socket; + return socket; } diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/transport.ts b/packages/@postgres-language-server/backend-jsonrpc/src/transport.ts index b1cdad445..7d2e111a6 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/transport.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/transport.ts @@ -1,99 +1,95 @@ interface Socket { - on(event: "data", fn: (data: Buffer) => void): void; - write(data: Buffer): void; - destroy(): void; + on(event: "data", fn: (data: Buffer) => void): void; + write(data: Buffer): void; + destroy(): void; } enum ReaderStateKind { - Header = 0, - Body = 1, + Header = 0, + Body = 1, } interface ReaderStateHeader { - readonly kind: ReaderStateKind.Header; - contentLength?: number; - contentType?: string; + readonly kind: ReaderStateKind.Header; + contentLength?: number; + contentType?: string; } interface ReaderStateBody { - readonly kind: ReaderStateKind.Body; - readonly contentLength: number; - readonly contentType?: string; + readonly kind: ReaderStateKind.Body; + readonly contentLength: number; + readonly contentType?: string; } type ReaderState = ReaderStateHeader | ReaderStateBody; interface JsonRpcRequest { - jsonrpc: "2.0"; - id: number; - method: string; - params: unknown; + jsonrpc: "2.0"; + id: number; + method: string; + params: unknown; } function isJsonRpcRequest(message: JsonRpcMessage): message is JsonRpcRequest { - return ( - "id" in message && - typeof message.id === "number" && - "method" in message && - typeof message.method === "string" && - "params" in message - ); + return ( + "id" in message && + typeof message.id === "number" && + "method" in message && + typeof message.method === "string" && + "params" in message + ); } interface JsonRpcNotification { - jsonrpc: "2.0"; - method: string; - params: unknown; + jsonrpc: "2.0"; + method: string; + params: unknown; } -function isJsonRpcNotification( - message: JsonRpcMessage, -): message is JsonRpcNotification { - return ( - !("id" in message) && - "method" in message && - typeof message.method === "string" && - "params" in message - ); +function isJsonRpcNotification(message: JsonRpcMessage): message is JsonRpcNotification { + return ( + !("id" in message) && + "method" in message && + typeof message.method === "string" && + "params" in message + ); } type JsonRpcResponse = - | { - jsonrpc: "2.0"; - id: number; - result: unknown; - } - | { - jsonrpc: "2.0"; - id: number; - error: unknown; - }; - -function isJsonRpcResponse( - message: JsonRpcMessage, -): message is JsonRpcResponse { - return ( - "id" in message && - typeof message.id === "number" && - !("method" in message) && - ("result" in message || "error" in message) - ); + | { + jsonrpc: "2.0"; + id: number; + result: unknown; + } + | { + jsonrpc: "2.0"; + id: number; + error: unknown; + }; + +function isJsonRpcResponse(message: JsonRpcMessage): message is JsonRpcResponse { + return ( + "id" in message && + typeof message.id === "number" && + !("method" in message) && + ("result" in message || "error" in message) + ); } type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse; function isJsonRpcMessage(message: unknown): message is JsonRpcMessage { - return ( - typeof message === "object" && - message !== null && - "jsonrpc" in message && - message.jsonrpc === "2.0" - ); + return ( + typeof message === "object" && + message !== null && + "jsonrpc" in message && + message.jsonrpc === "2.0" + ); } interface PendingRequest { - resolve(result: unknown): void; - reject(error: unknown): void; + resolve(result: unknown): void; + reject(error: unknown): void; } const MIME_JSONRPC = "application/vscode-jsonrpc"; @@ -102,192 +98,185 @@ const MIME_JSONRPC = "application/vscode-jsonrpc"; * Implements the daemon server JSON-RPC protocol over a Socket instance */ export class Transport { - /** - * Counter incremented for each outgoing request to generate a unique ID - */ - private nextRequestId = 0; - - /** - * Storage for the promise resolver functions of pending requests, - * keyed by ID of the request - */ - private pendingRequests: Map = new Map(); - - constructor(private socket: Socket) { - socket.on("data", (data) => { - this.processIncoming(data); - }); - } - - /** - * Send a request to the remote server - * - * @param method Name of the remote method to call - * @param params Parameters object the remote method should be called with - * @return Promise resolving with the value returned by the remote method, or rejecting with an RPC error if the remote call failed - */ - // biome-ignore lint/suspicious/noExplicitAny: if i change it to Promise typescript breaks - request(method: string, params: unknown): Promise { - return new Promise((resolve, reject) => { - const id = this.nextRequestId++; - this.pendingRequests.set(id, { resolve, reject }); - this.sendMessage({ - jsonrpc: "2.0", - id, - method, - params, - }); - }); - } - - /** - * Send a notification message to the remote server - * - * @param method Name of the remote method to call - * @param params Parameters object the remote method should be called with - */ - notify(method: string, params: unknown) { - this.sendMessage({ - jsonrpc: "2.0", - method, - params, - }); - } - - /** - * Destroy the internal socket instance for this Transport - */ - destroy() { - this.socket.destroy(); - } - - private sendMessage(message: JsonRpcMessage) { - const body = Buffer.from(JSON.stringify(message)); - const headers = Buffer.from( - `Content-Length: ${body.length}\r\nContent-Type: ${MIME_JSONRPC};charset=utf-8\r\n\r\n`, - ); - this.socket.write(Buffer.concat([headers, body])); - } - - private pendingData = Buffer.from(""); - private readerState: ReaderState = { - kind: ReaderStateKind.Header, - }; - - private processIncoming(data: Buffer) { - this.pendingData = Buffer.concat([this.pendingData, data]); - - while (this.pendingData.length > 0) { - if (this.readerState.kind === ReaderStateKind.Header) { - const lineBreakIndex = this.pendingData.indexOf("\n"); - if (lineBreakIndex < 0) { - break; - } - - const header = this.pendingData.subarray(0, lineBreakIndex + 1); - this.pendingData = this.pendingData.subarray(lineBreakIndex + 1); - this.processIncomingHeader(this.readerState, header.toString("utf-8")); - } else if (this.pendingData.length >= this.readerState.contentLength) { - const body = this.pendingData.subarray( - 0, - this.readerState.contentLength, - ); - this.pendingData = this.pendingData.subarray( - this.readerState.contentLength, - ); - this.processIncomingBody(body); - - this.readerState = { - kind: ReaderStateKind.Header, - }; - } else { - break; - } - } - } - - private processIncomingHeader(readerState: ReaderStateHeader, line: string) { - if (line === "\r\n") { - const { contentLength, contentType } = readerState; - if (typeof contentLength !== "number") { - throw new Error( - "incoming message from the remote workspace is missing the Content-Length header", - ); - } - - this.readerState = { - kind: ReaderStateKind.Body, - contentLength, - contentType, - }; - return; - } - - const colonIndex = line.indexOf(":"); - if (colonIndex < 0) { - throw new Error(`could not find colon token in "${line}"`); - } - - const headerName = line.substring(0, colonIndex); - const headerValue = line.substring(colonIndex + 1).trim(); - - switch (headerName) { - case "Content-Length": { - const value = Number.parseInt(headerValue); - readerState.contentLength = value; - break; - } - case "Content-Type": { - if (!headerValue.startsWith(MIME_JSONRPC)) { - throw new Error( - `invalid value for Content-Type expected "${MIME_JSONRPC}", got "${headerValue}"`, - ); - } - - readerState.contentType = headerValue; - break; - } - default: - console.warn(`ignoring unknown header "${headerName}"`); - } - } - - private processIncomingBody(buffer: Buffer) { - const data = buffer.toString("utf-8"); - const body = JSON.parse(data); - - if (isJsonRpcMessage(body)) { - if (isJsonRpcRequest(body)) { - // TODO: Not implemented at the moment - return; - } - - if (isJsonRpcNotification(body)) { - // TODO: Not implemented at the moment - return; - } - - if (isJsonRpcResponse(body)) { - const pendingRequest = this.pendingRequests.get(body.id); - if (pendingRequest) { - this.pendingRequests.delete(body.id); - const { resolve, reject } = pendingRequest; - if ("result" in body) { - resolve(body.result); - } else { - reject(body.error); - } - } else { - throw new Error( - `could not find any pending request matching RPC response ID ${body.id}`, - ); - } - return; - } - } - - throw new Error( - `failed to deserialize incoming message from remote workspace, "${data}" is not a valid JSON-RPC message body`, - ); - } + /** + * Counter incremented for each outgoing request to generate a unique ID + */ + private nextRequestId = 0; + + /** + * Storage for the promise resolver functions of pending requests, + * keyed by ID of the request + */ + private pendingRequests: Map = new Map(); + + constructor(private socket: Socket) { + socket.on("data", (data) => { + this.processIncoming(data); + }); + } + + /** + * Send a request to the remote server + * + * @param method Name of the remote method to call + * @param params Parameters object the remote method should be called with + * @return Promise resolving with the value returned by the remote method, or rejecting with an RPC error if the remote call failed + */ + // oxlint-disable-next-line typescript/no-explicit-any -- if i change it to Promise typescript breaks + request(method: string, params: unknown): Promise { + return new Promise((resolve, reject) => { + const id = this.nextRequestId++; + this.pendingRequests.set(id, { resolve, reject }); + this.sendMessage({ + jsonrpc: "2.0", + id, + method, + params, + }); + }); + } + + /** + * Send a notification message to the remote server + * + * @param method Name of the remote method to call + * @param params Parameters object the remote method should be called with + */ + notify(method: string, params: unknown) { + this.sendMessage({ + jsonrpc: "2.0", + method, + params, + }); + } + + /** + * Destroy the internal socket instance for this Transport + */ + destroy() { + this.socket.destroy(); + } + + private sendMessage(message: JsonRpcMessage) { + const body = Buffer.from(JSON.stringify(message)); + const headers = Buffer.from( + `Content-Length: ${body.length}\r\nContent-Type: ${MIME_JSONRPC};charset=utf-8\r\n\r\n`, + ); + this.socket.write(Buffer.concat([headers, body])); + } + + private pendingData = Buffer.from(""); + private readerState: ReaderState = { + kind: ReaderStateKind.Header, + }; + + private processIncoming(data: Buffer) { + this.pendingData = Buffer.concat([this.pendingData, data]); + + while (this.pendingData.length > 0) { + if (this.readerState.kind === ReaderStateKind.Header) { + const lineBreakIndex = this.pendingData.indexOf("\n"); + if (lineBreakIndex < 0) { + break; + } + + const header = this.pendingData.subarray(0, lineBreakIndex + 1); + this.pendingData = this.pendingData.subarray(lineBreakIndex + 1); + this.processIncomingHeader(this.readerState, header.toString("utf-8")); + } else if (this.pendingData.length >= this.readerState.contentLength) { + const body = this.pendingData.subarray(0, this.readerState.contentLength); + this.pendingData = this.pendingData.subarray(this.readerState.contentLength); + this.processIncomingBody(body); + + this.readerState = { + kind: ReaderStateKind.Header, + }; + } else { + break; + } + } + } + + private processIncomingHeader(readerState: ReaderStateHeader, line: string) { + if (line === "\r\n") { + const { contentLength, contentType } = readerState; + if (typeof contentLength !== "number") { + throw new Error( + "incoming message from the remote workspace is missing the Content-Length header", + ); + } + + this.readerState = { + kind: ReaderStateKind.Body, + contentLength, + contentType, + }; + return; + } + + const colonIndex = line.indexOf(":"); + if (colonIndex < 0) { + throw new Error(`could not find colon token in "${line}"`); + } + + const headerName = line.substring(0, colonIndex); + const headerValue = line.substring(colonIndex + 1).trim(); + + switch (headerName) { + case "Content-Length": { + const value = Number.parseInt(headerValue); + readerState.contentLength = value; + break; + } + case "Content-Type": { + if (!headerValue.startsWith(MIME_JSONRPC)) { + throw new Error( + `invalid value for Content-Type expected "${MIME_JSONRPC}", got "${headerValue}"`, + ); + } + + readerState.contentType = headerValue; + break; + } + default: + console.warn(`ignoring unknown header "${headerName}"`); + } + } + + private processIncomingBody(buffer: Buffer) { + const data = buffer.toString("utf-8"); + const body = JSON.parse(data); + + if (isJsonRpcMessage(body)) { + if (isJsonRpcRequest(body)) { + // TODO: Not implemented at the moment + return; + } + + if (isJsonRpcNotification(body)) { + // TODO: Not implemented at the moment + return; + } + + if (isJsonRpcResponse(body)) { + const pendingRequest = this.pendingRequests.get(body.id); + if (pendingRequest) { + this.pendingRequests.delete(body.id); + const { resolve, reject } = pendingRequest; + if ("result" in body) { + resolve(body.result); + } else { + reject(body.error); + } + } else { + throw new Error(`could not find any pending request matching RPC response ID ${body.id}`); + } + return; + } + } + + throw new Error( + `failed to deserialize incoming message from remote workspace, "${data}" is not a valid JSON-RPC message body`, + ); + } } diff --git a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts index 4d4a25d4d..3d4383427 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgres-language-server/backend-jsonrpc/src/workspace.ts @@ -1,18 +1,18 @@ // Generated file, do not edit by hand, see `xtask/codegen` import type { Transport } from "./transport"; export interface IsPathIgnoredParams { - pgls_path: PgLSPath; + pgls_path: PgLSPath; } export interface PgLSPath { - /** - * Determines the kind of the file inside Postgres Language Server. Some files are considered as configuration files, others as manifest files, and others as files to handle - */ - kind: FileKind; - path: string; - /** - * Whether this path (usually a file) was fixed as a result of a format/lint/check command with the `--write` filag. - */ - was_written: boolean; + /** + * Determines the kind of the file inside Postgres Language Server. Some files are considered as configuration files, others as manifest files, and others as files to handle + */ + kind: FileKind; + path: string; + /** + * Whether this path (usually a file) was fixed as a result of a format/lint/check command with the `--write` filag. + */ + was_written: boolean; } export type FileKind = FileKind2[]; /** @@ -20,155 +20,155 @@ export type FileKind = FileKind2[]; */ export type FileKind2 = "Config" | "Ignore" | "Inspectable" | "Handleable"; export interface RegisterProjectFolderParams { - path?: string; - setAsCurrentWorkspace: boolean; + path?: string; + setAsCurrentWorkspace: boolean; } export type ProjectKey = string; export interface GetFileContentParams { - path: PgLSPath; + path: PgLSPath; } export interface PullFileDiagnosticsParams { - categories: RuleCategories; - max_diagnostics: number; - only: RuleCode[]; - path: PgLSPath; - skip: RuleCode[]; + categories: RuleCategories; + max_diagnostics: number; + only: RuleCode[]; + path: PgLSPath; + skip: RuleCode[]; } export type RuleCategories = RuleCategory[]; export type RuleCode = string; export type RuleCategory = "Lint" | "Action" | "Transformation"; export interface PullDiagnosticsResult { - diagnostics: Diagnostic[]; - skipped_diagnostics: number; + diagnostics: Diagnostic[]; + skipped_diagnostics: number; } /** * Serializable representation for a [Diagnostic](super::Diagnostic). */ export interface Diagnostic { - advices: Advices; - category?: Category; - description: string; - location: Location; - message: MarkupBuf; - severity: Severity; - source?: Diagnostic; - tags: DiagnosticTags; - verboseAdvices: Advices; + advices: Advices; + category?: Category; + description: string; + location: Location; + message: MarkupBuf; + severity: Severity; + source?: Diagnostic; + tags: DiagnosticTags; + verboseAdvices: Advices; } /** * Implementation of [Visitor] collecting serializable [Advice] into a vector. */ export interface Advices { - advices: Advice[]; + advices: Advice[]; } export type Category = - | "lint/safety/addSerialColumn" - | "lint/safety/addingFieldWithDefault" - | "lint/safety/addingForeignKeyConstraint" - | "lint/safety/addingNotNullField" - | "lint/safety/addingPrimaryKeyConstraint" - | "lint/safety/addingRequiredField" - | "lint/safety/banCharField" - | "lint/safety/banConcurrentIndexCreationInTransaction" - | "lint/safety/banDropColumn" - | "lint/safety/banDropDatabase" - | "lint/safety/banDropNotNull" - | "lint/safety/banDropTable" - | "lint/safety/banTruncateCascade" - | "lint/safety/changingColumnType" - | "lint/safety/constraintMissingNotValid" - | "lint/safety/creatingEnum" - | "lint/safety/disallowUniqueConstraint" - | "lint/safety/lockTimeoutWarning" - | "lint/safety/multipleAlterTable" - | "lint/safety/preferBigInt" - | "lint/safety/preferBigintOverInt" - | "lint/safety/preferBigintOverSmallint" - | "lint/safety/preferIdentity" - | "lint/safety/preferJsonb" - | "lint/safety/preferRobustStmts" - | "lint/safety/preferTextField" - | "lint/safety/preferTimestamptz" - | "lint/safety/renamingColumn" - | "lint/safety/renamingTable" - | "lint/safety/requireConcurrentIndexCreation" - | "lint/safety/requireConcurrentIndexDeletion" - | "lint/safety/runningStatementWhileHoldingAccessExclusive" - | "lint/safety/transactionNesting" - | "pglinter/extensionNotInstalled" - | "pglinter/ruleDisabledInExtension" - | "pglinter/base/compositePrimaryKeyTooManyColumns" - | "pglinter/base/howManyObjectsWithUppercase" - | "pglinter/base/howManyRedudantIndex" - | "pglinter/base/howManyTableWithoutIndexOnFk" - | "pglinter/base/howManyTableWithoutPrimaryKey" - | "pglinter/base/howManyTablesNeverSelected" - | "pglinter/base/howManyTablesWithFkMismatch" - | "pglinter/base/howManyTablesWithFkOutsideSchema" - | "pglinter/base/howManyTablesWithReservedKeywords" - | "pglinter/base/howManyTablesWithSameTrigger" - | "pglinter/base/howManyUnusedIndex" - | "pglinter/base/severalTableOwnerInSchema" - | "pglinter/cluster/passwordEncryptionIsMd5" - | "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" - | "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists" - | "pglinter/schema/ownerSchemaIsInternalRole" - | "pglinter/schema/schemaOwnerDoNotMatchTableOwner" - | "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt" - | "pglinter/schema/schemaWithDefaultRoleNotGranted" - | "pglinter/schema/unsecuredPublicSchema" - | "splinter/performance/authRlsInitplan" - | "splinter/performance/duplicateIndex" - | "splinter/performance/multiplePermissivePolicies" - | "splinter/performance/noPrimaryKey" - | "splinter/performance/tableBloat" - | "splinter/performance/unindexedForeignKeys" - | "splinter/performance/unusedIndex" - | "splinter/security/authUsersExposed" - | "splinter/security/extensionInPublic" - | "splinter/security/extensionVersionsOutdated" - | "splinter/security/fkeyToAuthUnique" - | "splinter/security/foreignTableInApi" - | "splinter/security/functionSearchPathMutable" - | "splinter/security/insecureQueueExposedInApi" - | "splinter/security/materializedViewInApi" - | "splinter/security/policyExistsRlsDisabled" - | "splinter/security/rlsDisabledInPublic" - | "splinter/security/rlsEnabledNoPolicy" - | "splinter/security/rlsPolicyAlwaysTrue" - | "splinter/security/rlsReferencesUserMetadata" - | "splinter/security/securityDefinerView" - | "splinter/security/sensitiveColumnsExposed" - | "splinter/security/unsupportedRegTypes" - | "stdin" - | "check" - | "format" - | "configuration" - | "database/connection" - | "internalError/io" - | "internalError/runtime" - | "internalError/fs" - | "flags/invalid" - | "project" - | "typecheck" - | "plpgsql_check" - | "internalError/panic" - | "syntax" - | "dummy" - | "lint" - | "lint/performance" - | "lint/safety" - | "splinter" - | "splinter/performance" - | "splinter/security" - | "pglinter" - | "pglinter/base" - | "pglinter/cluster" - | "pglinter/schema"; + | "lint/safety/addSerialColumn" + | "lint/safety/addingFieldWithDefault" + | "lint/safety/addingForeignKeyConstraint" + | "lint/safety/addingNotNullField" + | "lint/safety/addingPrimaryKeyConstraint" + | "lint/safety/addingRequiredField" + | "lint/safety/banCharField" + | "lint/safety/banConcurrentIndexCreationInTransaction" + | "lint/safety/banDropColumn" + | "lint/safety/banDropDatabase" + | "lint/safety/banDropNotNull" + | "lint/safety/banDropTable" + | "lint/safety/banTruncateCascade" + | "lint/safety/changingColumnType" + | "lint/safety/constraintMissingNotValid" + | "lint/safety/creatingEnum" + | "lint/safety/disallowUniqueConstraint" + | "lint/safety/lockTimeoutWarning" + | "lint/safety/multipleAlterTable" + | "lint/safety/preferBigInt" + | "lint/safety/preferBigintOverInt" + | "lint/safety/preferBigintOverSmallint" + | "lint/safety/preferIdentity" + | "lint/safety/preferJsonb" + | "lint/safety/preferRobustStmts" + | "lint/safety/preferTextField" + | "lint/safety/preferTimestamptz" + | "lint/safety/renamingColumn" + | "lint/safety/renamingTable" + | "lint/safety/requireConcurrentIndexCreation" + | "lint/safety/requireConcurrentIndexDeletion" + | "lint/safety/runningStatementWhileHoldingAccessExclusive" + | "lint/safety/transactionNesting" + | "pglinter/extensionNotInstalled" + | "pglinter/ruleDisabledInExtension" + | "pglinter/base/compositePrimaryKeyTooManyColumns" + | "pglinter/base/howManyObjectsWithUppercase" + | "pglinter/base/howManyRedudantIndex" + | "pglinter/base/howManyTableWithoutIndexOnFk" + | "pglinter/base/howManyTableWithoutPrimaryKey" + | "pglinter/base/howManyTablesNeverSelected" + | "pglinter/base/howManyTablesWithFkMismatch" + | "pglinter/base/howManyTablesWithFkOutsideSchema" + | "pglinter/base/howManyTablesWithReservedKeywords" + | "pglinter/base/howManyTablesWithSameTrigger" + | "pglinter/base/howManyUnusedIndex" + | "pglinter/base/severalTableOwnerInSchema" + | "pglinter/cluster/passwordEncryptionIsMd5" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists" + | "pglinter/schema/ownerSchemaIsInternalRole" + | "pglinter/schema/schemaOwnerDoNotMatchTableOwner" + | "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt" + | "pglinter/schema/schemaWithDefaultRoleNotGranted" + | "pglinter/schema/unsecuredPublicSchema" + | "splinter/performance/authRlsInitplan" + | "splinter/performance/duplicateIndex" + | "splinter/performance/multiplePermissivePolicies" + | "splinter/performance/noPrimaryKey" + | "splinter/performance/tableBloat" + | "splinter/performance/unindexedForeignKeys" + | "splinter/performance/unusedIndex" + | "splinter/security/authUsersExposed" + | "splinter/security/extensionInPublic" + | "splinter/security/extensionVersionsOutdated" + | "splinter/security/fkeyToAuthUnique" + | "splinter/security/foreignTableInApi" + | "splinter/security/functionSearchPathMutable" + | "splinter/security/insecureQueueExposedInApi" + | "splinter/security/materializedViewInApi" + | "splinter/security/policyExistsRlsDisabled" + | "splinter/security/rlsDisabledInPublic" + | "splinter/security/rlsEnabledNoPolicy" + | "splinter/security/rlsPolicyAlwaysTrue" + | "splinter/security/rlsReferencesUserMetadata" + | "splinter/security/securityDefinerView" + | "splinter/security/sensitiveColumnsExposed" + | "splinter/security/unsupportedRegTypes" + | "stdin" + | "check" + | "format" + | "configuration" + | "database/connection" + | "internalError/io" + | "internalError/runtime" + | "internalError/fs" + | "flags/invalid" + | "project" + | "typecheck" + | "plpgsql_check" + | "internalError/panic" + | "syntax" + | "dummy" + | "lint" + | "lint/performance" + | "lint/safety" + | "splinter" + | "splinter/performance" + | "splinter/security" + | "pglinter" + | "pglinter/base" + | "pglinter/cluster" + | "pglinter/schema"; export interface Location { - path?: Resource_for_String; - sourceCode?: string; - span?: TextRange; + path?: Resource_for_String; + sourceCode?: string; + span?: TextRange; } export type MarkupBuf = MarkupNodeBuf[]; /** @@ -182,43 +182,39 @@ export type DiagnosticTags = DiagnosticTag[]; See the [Visitor] trait for additional documentation on all the supported advice types. */ export type Advice = - | { log: [LogCategory, MarkupBuf] } - | { list: MarkupBuf[] } - | { frame: Location } - | { diff: TextEdit } - | { diffWithOffset: [TextEdit, number] } - | { backtrace: [MarkupBuf, Backtrace] } - | { command: string } - | { group: [MarkupBuf, Advices] }; + | { log: [LogCategory, MarkupBuf] } + | { list: MarkupBuf[] } + | { frame: Location } + | { diff: TextEdit } + | { diffWithOffset: [TextEdit, number] } + | { backtrace: [MarkupBuf, Backtrace] } + | { command: string } + | { group: [MarkupBuf, Advices] }; /** * Represents the resource a diagnostic is associated with. */ -export type Resource_for_String = - | "database" - | "argv" - | "memory" - | { file: string }; +export type Resource_for_String = "database" | "argv" | "memory" | { file: string }; export type TextRange = [TextSize, TextSize]; export interface MarkupNodeBuf { - content: string; - elements: MarkupElement[]; + content: string; + elements: MarkupElement[]; } /** * Internal enum used to automatically generate bit offsets for [DiagnosticTags] and help with the implementation of `serde` and `schemars` for tags. */ export type DiagnosticTag = - | "fixable" - | "internal" - | "unnecessaryCode" - | "deprecatedCode" - | "verbose"; + | "fixable" + | "internal" + | "unnecessaryCode" + | "deprecatedCode" + | "verbose"; /** * The category for a log advice, defines how the message should be presented to the user. */ export type LogCategory = "none" | "info" | "warn" | "error"; export interface TextEdit { - dictionary: string; - ops: CompressedOp[]; + dictionary: string; + ops: CompressedOp[]; } export type Backtrace = BacktraceFrame[]; export type TextSize = number; @@ -226,65 +222,63 @@ export type TextSize = number; * Enumeration of all the supported markup elements */ export type MarkupElement = - | "Emphasis" - | "Dim" - | "Italic" - | "Underline" - | "Error" - | "Success" - | "Warn" - | "Info" - | "Debug" - | "Trace" - | "Inverse" - | { Hyperlink: { href: string } }; -export type CompressedOp = - | { diffOp: DiffOp } - | { equalLines: { line_count: number } }; + | "Emphasis" + | "Dim" + | "Italic" + | "Underline" + | "Error" + | "Success" + | "Warn" + | "Info" + | "Debug" + | "Trace" + | "Inverse" + | { Hyperlink: { href: string } }; +export type CompressedOp = { diffOp: DiffOp } | { equalLines: { line_count: number } }; /** * Serializable representation of a backtrace frame. */ export interface BacktraceFrame { - ip: number; - symbols: BacktraceSymbol[]; + ip: number; + symbols: BacktraceSymbol[]; } export type DiffOp = - | { equal: { range: TextRange } } - | { insert: { range: TextRange } } - | { delete: { range: TextRange } }; + | { equal: { range: TextRange } } + | { insert: { range: TextRange } } + | { delete: { range: TextRange } }; /** * Serializable representation of a backtrace frame symbol. */ export interface BacktraceSymbol { - colno?: number; - filename?: string; - lineno?: number; - name?: string; + colno?: number; + filename?: string; + lineno?: number; + name?: string; } export interface GetCompletionsParams { - /** - * The File for which a completion is requested. - */ - path: PgLSPath; - /** - * The Cursor position in the file for which a completion is requested. - */ - position: TextSize; + /** + * The File for which a completion is requested. + */ + path: PgLSPath; + /** + * The Cursor position in the file for which a completion is requested. + */ + position: TextSize; } export interface CompletionsResult { - items: CompletionItem[]; + items: CompletionItem[]; } export interface CompletionItem { - completion_text?: CompletionText; - description: string; - detail?: string; - kind: CompletionItemKind; - label: string; - preselected: boolean; - /** - * String used for sorting by LSP clients. - */ - sort_text: string; + completion_text?: CompletionText; + description: string; + detail?: string; + kind: CompletionItemKind; + label: string; + preselected: boolean; + /** + * String used for sorting by LSP clients. + */ + sort_text: string; } /** * The text that the editor should fill in. If `None`, the `label` should be used. Tables, for example, might have different completion_texts: @@ -292,280 +286,280 @@ export interface CompletionItem { label: "users", description: "Schema: auth", completion_text: "auth.users". */ export interface CompletionText { - is_snippet: boolean; - /** - * A `range` is required because some editors replace the current token, others naively insert the text. Having a range where start == end makes it an insertion. - */ - range: TextRange; - text: string; + is_snippet: boolean; + /** + * A `range` is required because some editors replace the current token, others naively insert the text. Having a range where start == end makes it an insertion. + */ + range: TextRange; + text: string; } export type CompletionItemKind = - | "table" - | "function" - | "column" - | "schema" - | "policy" - | "role" - | "keyword"; + | "table" + | "function" + | "column" + | "schema" + | "policy" + | "role" + | "keyword"; export interface UpdateSettingsParams { - configuration: PartialConfiguration; - gitignore_matches: string[]; - vcs_base_path?: string; - workspace_directory?: string; + configuration: PartialConfiguration; + gitignore_matches: string[]; + vcs_base_path?: string; + workspace_directory?: string; } /** * The configuration that is contained inside the configuration file. */ export interface PartialConfiguration { - /** - * A field for the [JSON schema](https://json-schema.org/) specification - */ - $schema?: string; - /** - * The configuration of the database connection - */ - db?: PartialDatabaseConfiguration; - /** - * A list of paths to other JSON files, used to extends the current configuration. - */ - extends?: StringSet; - /** - * The configuration of the filesystem - */ - files?: PartialFilesConfiguration; - /** - * The configuration for the SQL formatter - */ - format?: PartialFormatConfiguration; - /** - * The configuration for the linter - */ - linter?: PartialLinterConfiguration; - /** - * Configure migrations - */ - migrations?: PartialMigrationsConfiguration; - /** - * The configuration for pglinter - */ - pglinter?: PartialPglinterConfiguration; - /** - * The configuration for type checking - */ - plpgsqlCheck?: PartialPlPgSqlCheckConfiguration; - /** - * The configuration for splinter - */ - splinter?: PartialSplinterConfiguration; - /** - * The configuration for type checking - */ - typecheck?: PartialTypecheckConfiguration; - /** - * The configuration of the VCS integration - */ - vcs?: PartialVcsConfiguration; + /** + * A field for the [JSON schema](https://json-schema.org/) specification + */ + $schema?: string; + /** + * The configuration of the database connection + */ + db?: PartialDatabaseConfiguration; + /** + * A list of paths to other JSON files, used to extends the current configuration. + */ + extends?: StringSet; + /** + * The configuration of the filesystem + */ + files?: PartialFilesConfiguration; + /** + * The configuration for the SQL formatter + */ + format?: PartialFormatConfiguration; + /** + * The configuration for the linter + */ + linter?: PartialLinterConfiguration; + /** + * Configure migrations + */ + migrations?: PartialMigrationsConfiguration; + /** + * The configuration for pglinter + */ + pglinter?: PartialPglinterConfiguration; + /** + * The configuration for type checking + */ + plpgsqlCheck?: PartialPlPgSqlCheckConfiguration; + /** + * The configuration for splinter + */ + splinter?: PartialSplinterConfiguration; + /** + * The configuration for type checking + */ + typecheck?: PartialTypecheckConfiguration; + /** + * The configuration of the VCS integration + */ + vcs?: PartialVcsConfiguration; } /** * The configuration of the database connection. */ export interface PartialDatabaseConfiguration { - allowStatementExecutionsAgainst?: StringSet; - /** - * The connection timeout in seconds. - */ - connTimeoutSecs?: number; - /** - * A connection string that encodes the full connection setup. When provided, it takes precedence over the individual fields. Can also be set via the `DATABASE_URL` environment variable. - */ - connectionString?: string; - /** - * The name of the database. Can also be set via the `PGDATABASE` environment variable. - */ - database?: string; - /** - * The host of the database. Required if you want database-related features. All else falls back to sensible defaults. Can also be set via the `PGHOST` environment variable. - */ - host?: string; - /** - * The password to connect to the database. Can also be set via the `PGPASSWORD` environment variable. - */ - password?: string; - /** - * The port of the database. Can also be set via the `PGPORT` environment variable. - */ - port?: number; - /** - * The username to connect to the database. Can also be set via the `PGUSER` environment variable. - */ - username?: string; + allowStatementExecutionsAgainst?: StringSet; + /** + * The connection timeout in seconds. + */ + connTimeoutSecs?: number; + /** + * A connection string that encodes the full connection setup. When provided, it takes precedence over the individual fields. Can also be set via the `DATABASE_URL` environment variable. + */ + connectionString?: string; + /** + * The name of the database. Can also be set via the `PGDATABASE` environment variable. + */ + database?: string; + /** + * The host of the database. Required if you want database-related features. All else falls back to sensible defaults. Can also be set via the `PGHOST` environment variable. + */ + host?: string; + /** + * The password to connect to the database. Can also be set via the `PGPASSWORD` environment variable. + */ + password?: string; + /** + * The port of the database. Can also be set via the `PGPORT` environment variable. + */ + port?: number; + /** + * The username to connect to the database. Can also be set via the `PGUSER` environment variable. + */ + username?: string; } export type StringSet = string[]; /** * The configuration of the filesystem */ export interface PartialFilesConfiguration { - /** - * A list of Unix shell style patterns. Will ignore files/folders that will match these patterns. - */ - ignore?: StringSet; - /** - * A list of Unix shell style patterns. Will handle only those files/folders that will match these patterns. - */ - include?: StringSet; - /** - * The maximum allowed size for source code files in bytes. Files above this limit will be ignored for performance reasons. Defaults to 1 MiB - */ - maxSize?: number; + /** + * A list of Unix shell style patterns. Will ignore files/folders that will match these patterns. + */ + ignore?: StringSet; + /** + * A list of Unix shell style patterns. Will handle only those files/folders that will match these patterns. + */ + include?: StringSet; + /** + * The maximum allowed size for source code files in bytes. Files above this limit will be ignored for performance reasons. Defaults to 1 MiB + */ + maxSize?: number; } /** * The configuration for SQL formatting. */ export interface PartialFormatConfiguration { - /** - * Constant casing (NULL, TRUE, FALSE): "upper" or "lower". Default: "lower". - */ - constantCase?: KeywordCase; - /** - * If `false`, it disables the formatter. `true` by default. - */ - enabled?: boolean; - /** - * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. - */ - ignore?: StringSet; - /** - * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. - */ - include?: StringSet; - /** - * Number of spaces (or tab width) for indentation. Default: 2. - */ - indentSize?: number; - /** - * Indentation style: "spaces" or "tabs". Default: "spaces". - */ - indentStyle?: IndentStyle; - /** - * Keyword casing: "upper" or "lower". Default: "lower". - */ - keywordCase?: KeywordCase; - /** - * Maximum line width before breaking. Default: 100. - */ - lineWidth?: number; - /** - * If `true`, skip formatting of SQL function bodies (keep them verbatim). Default: `false`. - */ - skipFnBodies?: boolean; - /** - * Data type casing (text, varchar, int): "upper" or "lower". Default: "lower". - */ - typeCase?: KeywordCase; + /** + * Constant casing (NULL, TRUE, FALSE): "upper" or "lower". Default: "lower". + */ + constantCase?: KeywordCase; + /** + * If `false`, it disables the formatter. `true` by default. + */ + enabled?: boolean; + /** + * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet; + /** + * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. + */ + include?: StringSet; + /** + * Number of spaces (or tab width) for indentation. Default: 2. + */ + indentSize?: number; + /** + * Indentation style: "spaces" or "tabs". Default: "spaces". + */ + indentStyle?: IndentStyle; + /** + * Keyword casing: "upper" or "lower". Default: "lower". + */ + keywordCase?: KeywordCase; + /** + * Maximum line width before breaking. Default: 100. + */ + lineWidth?: number; + /** + * If `true`, skip formatting of SQL function bodies (keep them verbatim). Default: `false`. + */ + skipFnBodies?: boolean; + /** + * Data type casing (text, varchar, int): "upper" or "lower". Default: "lower". + */ + typeCase?: KeywordCase; } export interface PartialLinterConfiguration { - /** - * if `false`, it disables the feature and the linter won't be executed. `true` by default - */ - enabled?: boolean; - /** - * A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns. - */ - ignore?: StringSet; - /** - * A list of Unix shell style patterns. The linter will include files/folders that will match these patterns. - */ - include?: StringSet; - /** - * List of rules - */ - rules?: LinterRules; + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet; + /** + * A list of Unix shell style patterns. The linter will include files/folders that will match these patterns. + */ + include?: StringSet; + /** + * List of rules + */ + rules?: LinterRules; } /** * The configuration of the filesystem */ export interface PartialMigrationsConfiguration { - /** - * Ignore any migrations before this timestamp - */ - after?: number; - /** - * The directory where the migration files are stored - */ - migrationsDir?: string; + /** + * Ignore any migrations before this timestamp + */ + after?: number; + /** + * The directory where the migration files are stored + */ + migrationsDir?: string; } export interface PartialPglinterConfiguration { - /** - * if `false`, it disables the feature and the linter won't be executed. `true` by default - */ - enabled?: boolean; - /** - * List of rules - */ - rules?: PglinterRules; + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * List of rules + */ + rules?: PglinterRules; } /** * The configuration for type checking. */ export interface PartialPlPgSqlCheckConfiguration { - /** - * if `false`, it disables the feature and pglpgsql_check won't be executed. `true` by default - */ - enabled?: boolean; + /** + * if `false`, it disables the feature and pglpgsql_check won't be executed. `true` by default + */ + enabled?: boolean; } export interface PartialSplinterConfiguration { - /** - * if `false`, it disables the feature and the linter won't be executed. `true` by default - */ - enabled?: boolean; - /** - * A list of glob patterns for database objects to ignore across all rules. Patterns use Unix-style globs where `*` matches any sequence of characters. Format: `schema.object_name`, e.g., "public.my_table", "audit.*" - */ - ignore?: StringSet; - /** - * List of rules - */ - rules?: SplinterRules; + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * A list of glob patterns for database objects to ignore across all rules. Patterns use Unix-style globs where `*` matches any sequence of characters. Format: `schema.object_name`, e.g., "public.my_table", "audit.*" + */ + ignore?: StringSet; + /** + * List of rules + */ + rules?: SplinterRules; } /** * The configuration for type checking. */ export interface PartialTypecheckConfiguration { - /** - * if `false`, it disables the feature and the typechecker won't be executed. `true` by default - */ - enabled?: boolean; - /** - * Default search path schemas for type checking. Can be a list of schema names or glob patterns like ["public", "app_*"]. If not specified, defaults to ["public"]. - */ - searchPath?: StringSet; + /** + * if `false`, it disables the feature and the typechecker won't be executed. `true` by default + */ + enabled?: boolean; + /** + * Default search path schemas for type checking. Can be a list of schema names or glob patterns like ["public", "app_*"]. If not specified, defaults to ["public"]. + */ + searchPath?: StringSet; } /** * Set of properties to integrate with a VCS software. */ export interface PartialVcsConfiguration { - /** - * The kind of client. - */ - clientKind?: VcsClientKind; - /** - * The main branch of the project - */ - defaultBranch?: string; - /** - * Whether we should integrate itself with the VCS client - */ - enabled?: boolean; - /** + /** + * The kind of client. + */ + clientKind?: VcsClientKind; + /** + * The main branch of the project + */ + defaultBranch?: string; + /** + * Whether we should integrate itself with the VCS client + */ + enabled?: boolean; + /** * The folder where we should check for VCS files. By default, we will use the same folder where `postgres-language-server.jsonc` was found. If we can't find the configuration, it will attempt to use the current working directory. If no current working directory can't be found, we won't use the VCS integration, and a diagnostic will be emitted */ - root?: string; - /** - * Whether we should use the VCS ignore file. When [true], we will ignore the files specified in the ignore file. - */ - useIgnoreFile?: boolean; + root?: string; + /** + * Whether we should use the VCS ignore file. When [true], we will ignore the files specified in the ignore file. + */ + useIgnoreFile?: boolean; } /** * Keyword casing style for the formatter. @@ -576,450 +570,448 @@ export type KeywordCase = "upper" | "lower"; */ export type IndentStyle = "spaces" | "tabs"; export interface LinterRules { - /** - * It enables ALL rules. The rules that belong to `nursery` won't be enabled. - */ - all?: boolean; - /** - * It enables the lint rules recommended by Postgres Language Server. `true` by default. - */ - recommended?: boolean; - safety?: Safety; + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + safety?: Safety; } export interface PglinterRules { - /** - * It enables ALL rules. The rules that belong to `nursery` won't be enabled. - */ - all?: boolean; - base?: Base; - cluster?: Cluster; - /** - * It enables the lint rules recommended by Postgres Language Server. `true` by default. - */ - recommended?: boolean; - schema?: Schema; + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + base?: Base; + cluster?: Cluster; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + schema?: Schema; } export interface SplinterRules { - /** - * It enables ALL rules. The rules that belong to `nursery` won't be enabled. - */ - all?: boolean; - performance?: Performance; - /** - * It enables the lint rules recommended by Postgres Language Server. `true` by default. - */ - recommended?: boolean; - security?: Security; + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + performance?: Performance; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + security?: Security; } export type VcsClientKind = "git"; /** * A list of rules that belong to this group */ export interface Safety { - /** - * Adding a column with a SERIAL type or GENERATED ALWAYS AS ... STORED causes a full table rewrite. - */ - addSerialColumn?: RuleConfiguration_for_Null; - /** - * Adding a column with a DEFAULT value may lead to a table rewrite while holding an ACCESS EXCLUSIVE lock. - */ - addingFieldWithDefault?: RuleConfiguration_for_Null; - /** - * Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes. - */ - addingForeignKeyConstraint?: RuleConfiguration_for_Null; - /** - * Setting a column NOT NULL blocks reads while the table is scanned. - */ - addingNotNullField?: RuleConfiguration_for_Null; - /** - * Adding a primary key constraint results in locks and table rewrites. - */ - addingPrimaryKeyConstraint?: RuleConfiguration_for_Null; - /** - * Adding a new column that is NOT NULL and has no default value to an existing table effectively makes it required. - */ - addingRequiredField?: RuleConfiguration_for_Null; - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * Using CHAR(n) or CHARACTER(n) types is discouraged. - */ - banCharField?: RuleConfiguration_for_Null; - /** - * Concurrent index creation is not allowed within a transaction. - */ - banConcurrentIndexCreationInTransaction?: RuleConfiguration_for_Null; - /** - * Dropping a column may break existing clients. - */ - banDropColumn?: RuleConfiguration_for_Null; - /** - * Dropping a database may break existing clients (and everything else, really). - */ - banDropDatabase?: RuleConfiguration_for_Null; - /** - * Dropping a NOT NULL constraint may break existing clients. - */ - banDropNotNull?: RuleConfiguration_for_Null; - /** - * Dropping a table may break existing clients. - */ - banDropTable?: RuleConfiguration_for_Null; - /** - * Using TRUNCATE's CASCADE option will truncate any tables that are also foreign-keyed to the specified tables. - */ - banTruncateCascade?: RuleConfiguration_for_Null; - /** - * Changing a column type may break existing clients. - */ - changingColumnType?: RuleConfiguration_for_Null; - /** - * Adding constraints without NOT VALID blocks all reads and writes. - */ - constraintMissingNotValid?: RuleConfiguration_for_Null; - /** - * Creating enum types is not recommended for new applications. - */ - creatingEnum?: RuleConfiguration_for_Null; - /** - * Disallow adding a UNIQUE constraint without using an existing index. - */ - disallowUniqueConstraint?: RuleConfiguration_for_Null; - /** - * Taking a dangerous lock without setting a lock timeout can cause indefinite blocking. - */ - lockTimeoutWarning?: RuleConfiguration_for_Null; - /** - * Multiple ALTER TABLE statements on the same table should be combined into a single statement. - */ - multipleAlterTable?: RuleConfiguration_for_Null; - /** - * Prefer BIGINT over smaller integer types. - */ - preferBigInt?: RuleConfiguration_for_Null; - /** - * Prefer BIGINT over INT/INTEGER types. - */ - preferBigintOverInt?: RuleConfiguration_for_Null; - /** - * Prefer BIGINT over SMALLINT types. - */ - preferBigintOverSmallint?: RuleConfiguration_for_Null; - /** - * Prefer using IDENTITY columns over serial columns. - */ - preferIdentity?: RuleConfiguration_for_Null; - /** - * Prefer JSONB over JSON types. - */ - preferJsonb?: RuleConfiguration_for_Null; - /** - * Prefer statements with guards for robustness in migrations. - */ - preferRobustStmts?: RuleConfiguration_for_Null; - /** - * Prefer using TEXT over VARCHAR(n) types. - */ - preferTextField?: RuleConfiguration_for_Null; - /** - * Prefer TIMESTAMPTZ over TIMESTAMP types. - */ - preferTimestamptz?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * Renaming columns may break existing queries and application code. - */ - renamingColumn?: RuleConfiguration_for_Null; - /** - * Renaming tables may break existing queries and application code. - */ - renamingTable?: RuleConfiguration_for_Null; - /** - * Creating indexes non-concurrently can lock the table for writes. - */ - requireConcurrentIndexCreation?: RuleConfiguration_for_Null; - /** - * Dropping indexes non-concurrently can lock the table for reads. - */ - requireConcurrentIndexDeletion?: RuleConfiguration_for_Null; - /** - * Running additional statements while holding an ACCESS EXCLUSIVE lock blocks all table access. - */ - runningStatementWhileHoldingAccessExclusive?: RuleConfiguration_for_Null; - /** - * Detects problematic transaction nesting that could lead to unexpected behavior. - */ - transactionNesting?: RuleConfiguration_for_Null; + /** + * Adding a column with a SERIAL type or GENERATED ALWAYS AS ... STORED causes a full table rewrite. + */ + addSerialColumn?: RuleConfiguration_for_Null; + /** + * Adding a column with a DEFAULT value may lead to a table rewrite while holding an ACCESS EXCLUSIVE lock. + */ + addingFieldWithDefault?: RuleConfiguration_for_Null; + /** + * Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes. + */ + addingForeignKeyConstraint?: RuleConfiguration_for_Null; + /** + * Setting a column NOT NULL blocks reads while the table is scanned. + */ + addingNotNullField?: RuleConfiguration_for_Null; + /** + * Adding a primary key constraint results in locks and table rewrites. + */ + addingPrimaryKeyConstraint?: RuleConfiguration_for_Null; + /** + * Adding a new column that is NOT NULL and has no default value to an existing table effectively makes it required. + */ + addingRequiredField?: RuleConfiguration_for_Null; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Using CHAR(n) or CHARACTER(n) types is discouraged. + */ + banCharField?: RuleConfiguration_for_Null; + /** + * Concurrent index creation is not allowed within a transaction. + */ + banConcurrentIndexCreationInTransaction?: RuleConfiguration_for_Null; + /** + * Dropping a column may break existing clients. + */ + banDropColumn?: RuleConfiguration_for_Null; + /** + * Dropping a database may break existing clients (and everything else, really). + */ + banDropDatabase?: RuleConfiguration_for_Null; + /** + * Dropping a NOT NULL constraint may break existing clients. + */ + banDropNotNull?: RuleConfiguration_for_Null; + /** + * Dropping a table may break existing clients. + */ + banDropTable?: RuleConfiguration_for_Null; + /** + * Using TRUNCATE's CASCADE option will truncate any tables that are also foreign-keyed to the specified tables. + */ + banTruncateCascade?: RuleConfiguration_for_Null; + /** + * Changing a column type may break existing clients. + */ + changingColumnType?: RuleConfiguration_for_Null; + /** + * Adding constraints without NOT VALID blocks all reads and writes. + */ + constraintMissingNotValid?: RuleConfiguration_for_Null; + /** + * Creating enum types is not recommended for new applications. + */ + creatingEnum?: RuleConfiguration_for_Null; + /** + * Disallow adding a UNIQUE constraint without using an existing index. + */ + disallowUniqueConstraint?: RuleConfiguration_for_Null; + /** + * Taking a dangerous lock without setting a lock timeout can cause indefinite blocking. + */ + lockTimeoutWarning?: RuleConfiguration_for_Null; + /** + * Multiple ALTER TABLE statements on the same table should be combined into a single statement. + */ + multipleAlterTable?: RuleConfiguration_for_Null; + /** + * Prefer BIGINT over smaller integer types. + */ + preferBigInt?: RuleConfiguration_for_Null; + /** + * Prefer BIGINT over INT/INTEGER types. + */ + preferBigintOverInt?: RuleConfiguration_for_Null; + /** + * Prefer BIGINT over SMALLINT types. + */ + preferBigintOverSmallint?: RuleConfiguration_for_Null; + /** + * Prefer using IDENTITY columns over serial columns. + */ + preferIdentity?: RuleConfiguration_for_Null; + /** + * Prefer JSONB over JSON types. + */ + preferJsonb?: RuleConfiguration_for_Null; + /** + * Prefer statements with guards for robustness in migrations. + */ + preferRobustStmts?: RuleConfiguration_for_Null; + /** + * Prefer using TEXT over VARCHAR(n) types. + */ + preferTextField?: RuleConfiguration_for_Null; + /** + * Prefer TIMESTAMPTZ over TIMESTAMP types. + */ + preferTimestamptz?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * Renaming columns may break existing queries and application code. + */ + renamingColumn?: RuleConfiguration_for_Null; + /** + * Renaming tables may break existing queries and application code. + */ + renamingTable?: RuleConfiguration_for_Null; + /** + * Creating indexes non-concurrently can lock the table for writes. + */ + requireConcurrentIndexCreation?: RuleConfiguration_for_Null; + /** + * Dropping indexes non-concurrently can lock the table for reads. + */ + requireConcurrentIndexDeletion?: RuleConfiguration_for_Null; + /** + * Running additional statements while holding an ACCESS EXCLUSIVE lock blocks all table access. + */ + runningStatementWhileHoldingAccessExclusive?: RuleConfiguration_for_Null; + /** + * Detects problematic transaction nesting that could lead to unexpected behavior. + */ + transactionNesting?: RuleConfiguration_for_Null; } /** * A list of rules that belong to this group */ export interface Base { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns - */ - compositePrimaryKeyTooManyColumns?: RuleConfiguration_for_Null; - /** - * HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns. - */ - howManyObjectsWithUppercase?: RuleConfiguration_for_Null; - /** - * HowManyRedudantIndex (B002): Count number of redundant index vs nb index. - */ - howManyRedudantIndex?: RuleConfiguration_for_Null; - /** - * HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key. - */ - howManyTableWithoutIndexOnFk?: RuleConfiguration_for_Null; - /** - * HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key. - */ - howManyTableWithoutPrimaryKey?: RuleConfiguration_for_Null; - /** - * HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected. - */ - howManyTablesNeverSelected?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type. - */ - howManyTablesWithFkMismatch?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema. - */ - howManyTablesWithFkOutsideSchema?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names. - */ - howManyTablesWithReservedKeywords?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers. - */ - howManyTablesWithSameTrigger?: RuleConfiguration_for_Null; - /** - * HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.) - */ - howManyUnusedIndex?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners. - */ - severalTableOwnerInSchema?: RuleConfiguration_for_Null; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns + */ + compositePrimaryKeyTooManyColumns?: RuleConfiguration_for_Null; + /** + * HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns. + */ + howManyObjectsWithUppercase?: RuleConfiguration_for_Null; + /** + * HowManyRedudantIndex (B002): Count number of redundant index vs nb index. + */ + howManyRedudantIndex?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key. + */ + howManyTableWithoutIndexOnFk?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key. + */ + howManyTableWithoutPrimaryKey?: RuleConfiguration_for_Null; + /** + * HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected. + */ + howManyTablesNeverSelected?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type. + */ + howManyTablesWithFkMismatch?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema. + */ + howManyTablesWithFkOutsideSchema?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names. + */ + howManyTablesWithReservedKeywords?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers. + */ + howManyTablesWithSameTrigger?: RuleConfiguration_for_Null; + /** + * HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.) + */ + howManyUnusedIndex?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners. + */ + severalTableOwnerInSchema?: RuleConfiguration_for_Null; } /** * A list of rules that belong to this group */ export interface Cluster { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256. - */ - passwordEncryptionIsMd5?: RuleConfiguration_for_Null; - /** - * PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. - */ - pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists?: RuleConfiguration_for_Null; - /** - * PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. - */ - pgHbaEntriesWithMethodTrustShouldNotExists?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256. + */ + passwordEncryptionIsMd5?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustShouldNotExists?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; } /** * A list of rules that belong to this group */ export interface Schema { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary). - */ - ownerSchemaIsInternalRole?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match. - */ - schemaOwnerDoNotMatchTableOwner?: RuleConfiguration_for_Null; - /** - * SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy. - */ - schemaPrefixedOrSuffixedWithEnvt?: RuleConfiguration_for_Null; - /** - * SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it. - */ - schemaWithDefaultRoleNotGranted?: RuleConfiguration_for_Null; - /** - * UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects. - */ - unsecuredPublicSchema?: RuleConfiguration_for_Null; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary). + */ + ownerSchemaIsInternalRole?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match. + */ + schemaOwnerDoNotMatchTableOwner?: RuleConfiguration_for_Null; + /** + * SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy. + */ + schemaPrefixedOrSuffixedWithEnvt?: RuleConfiguration_for_Null; + /** + * SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it. + */ + schemaWithDefaultRoleNotGranted?: RuleConfiguration_for_Null; + /** + * UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects. + */ + unsecuredPublicSchema?: RuleConfiguration_for_Null; } /** * A list of rules that belong to this group */ export interface Performance { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row - */ - authRlsInitplan?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Duplicate Index: Detects cases where two ore more identical indexes exist. - */ - duplicateIndex?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. - */ - multiplePermissivePolicies?: RuleConfiguration_for_SplinterRuleOptions; - /** - * No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. - */ - noPrimaryKey?: RuleConfiguration_for_SplinterRuleOptions; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. - */ - tableBloat?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance. - */ - unindexedForeignKeys?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Unused Index: Detects if an index has never been used and may be a candidate for removal. - */ - unusedIndex?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row + */ + authRlsInitplan?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Duplicate Index: Detects cases where two ore more identical indexes exist. + */ + duplicateIndex?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. + */ + multiplePermissivePolicies?: RuleConfiguration_for_SplinterRuleOptions; + /** + * No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. + */ + noPrimaryKey?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. + */ + tableBloat?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance. + */ + unindexedForeignKeys?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unused Index: Detects if an index has never been used and may be a candidate for removal. + */ + unusedIndex?: RuleConfiguration_for_SplinterRuleOptions; } /** * A list of rules that belong to this group */ export interface Security { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. - */ - authUsersExposed?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Extension in Public: Detects extensions installed in the `public` schema. - */ - extensionInPublic?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Extension Versions Outdated: Detects extensions that are not using the default (recommended) version. - */ - extensionVersionsOutdated?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema. - */ - fkeyToAuthUnique?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. - */ - foreignTableInApi?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Function Search Path Mutable: Detects functions where the search_path parameter is not set. - */ - functionSearchPathMutable?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs - */ - insecureQueueExposedInApi?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Materialized View in API: Detects materialized views that are accessible over the Data APIs. - */ - materializedViewInApi?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. - */ - policyExistsRlsDisabled?: RuleConfiguration_for_SplinterRuleOptions; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST - */ - rlsDisabledInPublic?: RuleConfiguration_for_SplinterRuleOptions; - /** - * RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. - */ - rlsEnabledNoPolicy?: RuleConfiguration_for_SplinterRuleOptions; - /** - * RLS Policy Always True: Detects RLS policies that use overly permissive expressions like USING (true) or WITH CHECK (true) for UPDATE, DELETE, or INSERT operations. SELECT policies with USING (true) are intentionally excluded as this pattern is often used deliberately for public read access. - */ - rlsPolicyAlwaysTrue?: RuleConfiguration_for_SplinterRuleOptions; - /** - * RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. - */ - rlsReferencesUserMetadata?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user - */ - securityDefinerView?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Sensitive Columns Exposed: Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection. - */ - sensitiveColumnsExposed?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. - */ - unsupportedRegTypes?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. + */ + authUsersExposed?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension in Public: Detects extensions installed in the `public` schema. + */ + extensionInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension Versions Outdated: Detects extensions that are not using the default (recommended) version. + */ + extensionVersionsOutdated?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema. + */ + fkeyToAuthUnique?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. + */ + foreignTableInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Function Search Path Mutable: Detects functions where the search_path parameter is not set. + */ + functionSearchPathMutable?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs + */ + insecureQueueExposedInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Materialized View in API: Detects materialized views that are accessible over the Data APIs. + */ + materializedViewInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. + */ + policyExistsRlsDisabled?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST + */ + rlsDisabledInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. + */ + rlsEnabledNoPolicy?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS Policy Always True: Detects RLS policies that use overly permissive expressions like USING (true) or WITH CHECK (true) for UPDATE, DELETE, or INSERT operations. SELECT policies with USING (true) are intentionally excluded as this pattern is often used deliberately for public read access. + */ + rlsPolicyAlwaysTrue?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. + */ + rlsReferencesUserMetadata?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user + */ + securityDefinerView?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Sensitive Columns Exposed: Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection. + */ + sensitiveColumnsExposed?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. + */ + unsupportedRegTypes?: RuleConfiguration_for_SplinterRuleOptions; } -export type RuleConfiguration_for_Null = - | RulePlainConfiguration - | RuleWithOptions_for_Null; +export type RuleConfiguration_for_Null = RulePlainConfiguration | RuleWithOptions_for_Null; export type RuleConfiguration_for_SplinterRuleOptions = - | RulePlainConfiguration - | RuleWithOptions_for_SplinterRuleOptions; + | RulePlainConfiguration + | RuleWithOptions_for_SplinterRuleOptions; export type RulePlainConfiguration = "warn" | "error" | "info" | "off"; export interface RuleWithOptions_for_Null { - /** - * The severity of the emitted diagnostics by the rule - */ - level: RulePlainConfiguration; - /** - * Rule's options - */ - options: null; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: null; } export interface RuleWithOptions_for_SplinterRuleOptions { - /** - * The severity of the emitted diagnostics by the rule - */ - level: RulePlainConfiguration; - /** - * Rule's options - */ - options: SplinterRuleOptions; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: SplinterRuleOptions; } /** * Shared options for all splinter rules. @@ -1027,76 +1019,72 @@ export interface RuleWithOptions_for_SplinterRuleOptions { These options allow configuring per-rule filtering of database objects. */ export interface SplinterRuleOptions { - /** + /** * A list of glob patterns for database objects to ignore. Patterns use Unix-style globs where: - `*` matches any sequence of characters - `?` matches any single character Each pattern should be in the format `schema.object_name`, for example: - `"public.my_table"` - ignores a specific table - `"audit.*"` - ignores all objects in the audit schema - `"*.audit_*"` - ignores objects with audit_ prefix in any schema */ - ignore?: string[]; + ignore?: string[]; } export interface OpenFileParams { - content: string; - path: PgLSPath; - version: number; + content: string; + path: PgLSPath; + version: number; } export interface ChangeFileParams { - content: string; - path: PgLSPath; - version: number; + content: string; + path: PgLSPath; + version: number; } export interface CloseFileParams { - path: PgLSPath; + path: PgLSPath; } export type Configuration = PartialConfiguration; export interface Workspace { - isPathIgnored(params: IsPathIgnoredParams): Promise; - registerProjectFolder( - params: RegisterProjectFolderParams, - ): Promise; - getFileContent(params: GetFileContentParams): Promise; - pullFileDiagnostics( - params: PullFileDiagnosticsParams, - ): Promise; - getCompletions(params: GetCompletionsParams): Promise; - updateSettings(params: UpdateSettingsParams): Promise; - openFile(params: OpenFileParams): Promise; - changeFile(params: ChangeFileParams): Promise; - closeFile(params: CloseFileParams): Promise; - destroy(): void; + isPathIgnored(params: IsPathIgnoredParams): Promise; + registerProjectFolder(params: RegisterProjectFolderParams): Promise; + getFileContent(params: GetFileContentParams): Promise; + pullFileDiagnostics(params: PullFileDiagnosticsParams): Promise; + getCompletions(params: GetCompletionsParams): Promise; + updateSettings(params: UpdateSettingsParams): Promise; + openFile(params: OpenFileParams): Promise; + changeFile(params: ChangeFileParams): Promise; + closeFile(params: CloseFileParams): Promise; + destroy(): void; } export function createWorkspace(transport: Transport): Workspace { - return { - isPathIgnored(params) { - return transport.request("pgls/is_path_ignored", params); - }, - registerProjectFolder(params) { - return transport.request("pgls/register_project_folder", params); - }, - getFileContent(params) { - return transport.request("pgls/get_file_content", params); - }, - pullFileDiagnostics(params) { - return transport.request("pgls/pull_file_diagnostics", params); - }, - getCompletions(params) { - return transport.request("pgls/get_completions", params); - }, - updateSettings(params) { - return transport.request("pgls/update_settings", params); - }, - openFile(params) { - return transport.request("pgls/open_file", params); - }, - changeFile(params) { - return transport.request("pgls/change_file", params); - }, - closeFile(params) { - return transport.request("pgls/close_file", params); - }, - destroy() { - transport.destroy(); - }, - }; + return { + isPathIgnored(params) { + return transport.request("pgls/is_path_ignored", params); + }, + registerProjectFolder(params) { + return transport.request("pgls/register_project_folder", params); + }, + getFileContent(params) { + return transport.request("pgls/get_file_content", params); + }, + pullFileDiagnostics(params) { + return transport.request("pgls/pull_file_diagnostics", params); + }, + getCompletions(params) { + return transport.request("pgls/get_completions", params); + }, + updateSettings(params) { + return transport.request("pgls/update_settings", params); + }, + openFile(params) { + return transport.request("pgls/open_file", params); + }, + changeFile(params) { + return transport.request("pgls/change_file", params); + }, + closeFile(params) { + return transport.request("pgls/close_file", params); + }, + destroy() { + transport.destroy(); + }, + }; } diff --git a/packages/@postgres-language-server/backend-jsonrpc/tests/transport.test.mjs b/packages/@postgres-language-server/backend-jsonrpc/tests/transport.test.mjs index 32a103eea..91df2e128 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/tests/transport.test.mjs +++ b/packages/@postgres-language-server/backend-jsonrpc/tests/transport.test.mjs @@ -3,158 +3,154 @@ import { describe, expect, it, mock } from "bun:test"; import { Transport } from "../src/transport"; function makeMessage(body) { - const content = JSON.stringify(body); - return Buffer.from( - `Content-Length: ${content.length}\r\nContent-Type: application/vscode-jsonrpc;charset=utf-8\r\n\r\n${content}`, - ); + const content = JSON.stringify(body); + return Buffer.from( + `Content-Length: ${content.length}\r\nContent-Type: application/vscode-jsonrpc;charset=utf-8\r\n\r\n${content}`, + ); } describe("Transport Layer", () => { - it("should encode requests into the socket", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - const result = transport.request("method", "params"); - - expect(socket.write).toHaveBeenCalledWith( - makeMessage({ - jsonrpc: "2.0", - id: 0, - method: "method", - params: "params", - }), - ); - - onData( - makeMessage({ - jsonrpc: "2.0", - id: 0, - result: "result", - }), - ); - - const response = await result; - expect(response).toBe("result"); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on missing Content-Length headers", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => onData(Buffer.from("\r\n"))).toThrowError( - "incoming message from the remote workspace is missing the Content-Length header", - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on missing colon token", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => onData(Buffer.from("Content-Length\r\n"))).toThrowError( - 'could not find colon token in "Content-Length\r\n"', - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on invalid Content-Type", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => - onData(Buffer.from("Content-Type: text/plain\r\n")), - ).toThrowError( - 'invalid value for Content-Type expected "application/vscode-jsonrpc", got "text/plain"', - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on unknown request ID", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => - onData(makeMessage({ jsonrpc: "2.0", id: 0, result: "result" })), - ).toThrowError( - "could not find any pending request matching RPC response ID 0", - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on invalid messages", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => onData(makeMessage({}))).toThrowError( - 'failed to deserialize incoming message from remote workspace, "{}" is not a valid JSON-RPC message body', - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); + it("should encode requests into the socket", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + const result = transport.request("method", "params"); + + expect(socket.write).toHaveBeenCalledWith( + makeMessage({ + jsonrpc: "2.0", + id: 0, + method: "method", + params: "params", + }), + ); + + onData( + makeMessage({ + jsonrpc: "2.0", + id: 0, + result: "result", + }), + ); + + const response = await result; + expect(response).toBe("result"); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on missing Content-Length headers", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(Buffer.from("\r\n"))).toThrowError( + "incoming message from the remote workspace is missing the Content-Length header", + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on missing colon token", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(Buffer.from("Content-Length\r\n"))).toThrowError( + 'could not find colon token in "Content-Length\r\n"', + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on invalid Content-Type", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(Buffer.from("Content-Type: text/plain\r\n"))).toThrowError( + 'invalid value for Content-Type expected "application/vscode-jsonrpc", got "text/plain"', + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on unknown request ID", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(makeMessage({ jsonrpc: "2.0", id: 0, result: "result" }))).toThrowError( + "could not find any pending request matching RPC response ID 0", + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on invalid messages", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(makeMessage({}))).toThrowError( + 'failed to deserialize incoming message from remote workspace, "{}" is not a valid JSON-RPC message body', + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); }); diff --git a/packages/@postgres-language-server/backend-jsonrpc/tests/workspace.test.mjs b/packages/@postgres-language-server/backend-jsonrpc/tests/workspace.test.mjs index 3c32d574c..1f816e41c 100644 --- a/packages/@postgres-language-server/backend-jsonrpc/tests/workspace.test.mjs +++ b/packages/@postgres-language-server/backend-jsonrpc/tests/workspace.test.mjs @@ -1,57 +1,56 @@ import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; + import { describe, expect, it } from "vitest"; import { createWorkspaceWithBinary } from "../src"; describe("Workspace API", () => { - it("should process remote requests", async () => { - const extension = process.platform === "win32" ? ".exe" : ""; - const command = resolve( - fileURLToPath(import.meta.url), - "../../../../..", - `target/release/postgres-language-server${extension}`, - ); - - const workspace = await createWorkspaceWithBinary(command); - workspace.registerProjectFolder({ - setAsCurrentWorkspace: true, - }); - await workspace.openFile({ - path: { - path: "test.sql", - was_written: false, - kind: ["Handleable"], - }, - content: "select 1 from", - version: 0, - }); - - const { diagnostics } = await workspace.pullFileDiagnostics({ - only: [], - skip: [], - max_diagnostics: 100, - categories: [], - path: { - path: "test.sql", - was_written: false, - kind: ["Handleable"], - }, - }); - - expect(diagnostics).toHaveLength(1); - expect(diagnostics[0].description).toBe( - "Invalid statement: syntax error at end of input", - ); - - await workspace.closeFile({ - path: { - path: "test.sql", - was_written: false, - kind: ["Handleable"], - }, - }); - - workspace.destroy(); - }); + it("should process remote requests", async () => { + const extension = process.platform === "win32" ? ".exe" : ""; + const command = resolve( + fileURLToPath(import.meta.url), + "../../../../..", + `target/release/postgres-language-server${extension}`, + ); + + const workspace = await createWorkspaceWithBinary(command); + workspace.registerProjectFolder({ + setAsCurrentWorkspace: true, + }); + await workspace.openFile({ + path: { + path: "test.sql", + was_written: false, + kind: ["Handleable"], + }, + content: "select 1 from", + version: 0, + }); + + const { diagnostics } = await workspace.pullFileDiagnostics({ + only: [], + skip: [], + max_diagnostics: 100, + categories: [], + path: { + path: "test.sql", + was_written: false, + kind: ["Handleable"], + }, + }); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0].description).toBe("Invalid statement: syntax error at end of input"); + + await workspace.closeFile({ + path: { + path: "test.sql", + was_written: false, + kind: ["Handleable"], + }, + }); + + workspace.destroy(); + }); }); diff --git a/packages/@postgres-language-server/cli/package.json b/packages/@postgres-language-server/cli/package.json index c8855be99..28326124e 100644 --- a/packages/@postgres-language-server/cli/package.json +++ b/packages/@postgres-language-server/cli/package.json @@ -1,44 +1,48 @@ { - "name": "@postgres-language-server/cli", - "version": "", - "bin": { - "postgres-language-server": "bin/postgres-language-server" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/supabase-community/postgres-language-server.git", - "directory": "packages/@postgres-language-server/cli" - }, - "author": "Supabase Community", - "contributors": [ - { - "name": "Philipp Steinrötter", - "url": "https://github.com/psteinroe" - }, - { - "name": "Julian Domke", - "url": "https://github.com/juleswritescode" - } - ], - "license": "MIT or Apache-2.0", - "description": "A collection of language tools and a Language Server Protocol (LSP) implementation for Postgres, focusing on developer experience and reliable SQL tooling.", - "files": ["bin/postgres-language-server", "schema.json", "README.md"], - "engines": { - "node": ">=20" - }, - "publishConfig": { - "provenance": true - }, - "optionalDependencies": { - "@postgres-language-server/cli-x86_64-windows-msvc": "", - "@postgres-language-server/cli-aarch64-windows-msvc": "", - "@postgres-language-server/cli-x86_64-apple-darwin": "", - "@postgres-language-server/cli-aarch64-apple-darwin": "", - "@postgres-language-server/cli-x86_64-linux-gnu": "", - "@postgres-language-server/cli-aarch64-linux-gnu": "", - "@postgres-language-server/cli-x86_64-linux-musl": "" - }, - "scripts": { - "test": "bun test" - } + "name": "@postgres-language-server/cli", + "version": "", + "description": "A collection of language tools and a Language Server Protocol (LSP) implementation for Postgres, focusing on developer experience and reliable SQL tooling.", + "license": "MIT or Apache-2.0", + "author": "Supabase Community", + "contributors": [ + { + "name": "Philipp Steinrötter", + "url": "https://github.com/psteinroe" + }, + { + "name": "Julian Domke", + "url": "https://github.com/juleswritescode" + } + ], + "repository": { + "type": "git", + "url": "git+https://github.com/supabase-community/postgres-language-server.git", + "directory": "packages/@postgres-language-server/cli" + }, + "bin": { + "postgres-language-server": "bin/postgres-language-server" + }, + "files": [ + "bin/postgres-language-server", + "schema.json", + "README.md" + ], + "publishConfig": { + "provenance": true + }, + "scripts": { + "test": "bun test" + }, + "optionalDependencies": { + "@postgres-language-server/cli-aarch64-apple-darwin": "", + "@postgres-language-server/cli-aarch64-linux-gnu": "", + "@postgres-language-server/cli-aarch64-windows-msvc": "", + "@postgres-language-server/cli-x86_64-apple-darwin": "", + "@postgres-language-server/cli-x86_64-linux-gnu": "", + "@postgres-language-server/cli-x86_64-linux-musl": "", + "@postgres-language-server/cli-x86_64-windows-msvc": "" + }, + "engines": { + "node": ">=20" + } } diff --git a/packages/@postgres-language-server/cli/scripts/generate-packages.mjs b/packages/@postgres-language-server/cli/scripts/generate-packages.mjs index f3fc946d1..d196edca6 100644 --- a/packages/@postgres-language-server/cli/scripts/generate-packages.mjs +++ b/packages/@postgres-language-server/cli/scripts/generate-packages.mjs @@ -12,310 +12,299 @@ const PGLS_ROOT = resolve(PACKAGES_PGLS_ROOT, "../.."); const MANIFEST_PATH = resolve(CLI_ROOT, "package.json"); function platformArchCombinations() { - const SUPPORTED_PLATFORMS = [ - "pc-windows-msvc", - "apple-darwin", - "unknown-linux-gnu", - "unknown-linux-musl", - ]; - - const SUPPORTED_ARCHITECTURES = ["x86_64", "aarch64"]; - - return SUPPORTED_PLATFORMS.flatMap((platform) => { - return SUPPORTED_ARCHITECTURES.flatMap((arch) => { - // we do not support MUSL builds on aarch64, as this would - // require difficult cross compilation and most aarch64 users should - // have sufficiently modern glibc versions - if (platform.endsWith("musl") && arch === "aarch64") { - return []; - } - - return { - platform, - arch, - }; - }); - }); + const SUPPORTED_PLATFORMS = [ + "pc-windows-msvc", + "apple-darwin", + "unknown-linux-gnu", + "unknown-linux-musl", + ]; + + const SUPPORTED_ARCHITECTURES = ["x86_64", "aarch64"]; + + return SUPPORTED_PLATFORMS.flatMap((platform) => { + return SUPPORTED_ARCHITECTURES.flatMap((arch) => { + // we do not support MUSL builds on aarch64, as this would + // require difficult cross compilation and most aarch64 users should + // have sufficiently modern glibc versions + if (platform.endsWith("musl") && arch === "aarch64") { + return []; + } + + return { + platform, + arch, + }; + }); + }); } async function downloadSchema(releaseTag, githubToken) { - const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/schema.json`; + const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/schema.json`; - const response = await fetch(assetUrl.trim(), { - headers: { - Authorization: `token ${githubToken}`, - Accept: "application/octet-stream", - }, - }); + const response = await fetch(assetUrl.trim(), { + headers: { + Authorization: `token ${githubToken}`, + Accept: "application/octet-stream", + }, + }); - if (!response.ok) { - throw new Error(`Failed to Fetch Asset from ${assetUrl}`); - } + if (!response.ok) { + throw new Error(`Failed to Fetch Asset from ${assetUrl}`); + } - // download to root. - const fileStream = fs.createWriteStream(resolve(PGLS_ROOT, "schema.json")); + // download to root. + const fileStream = fs.createWriteStream(resolve(PGLS_ROOT, "schema.json")); - await streamPipeline(response.body, fileStream); + await streamPipeline(response.body, fileStream); - console.log(`Downloaded schema for ${releaseTag}`); + console.log(`Downloaded schema for ${releaseTag}`); } async function downloadWasmAssets(releaseTag, githubToken) { - const wasmDir = resolve(PACKAGES_PGLS_ROOT, "wasm", "wasm"); - fs.mkdirSync(wasmDir, { recursive: true }); + const wasmDir = resolve(PACKAGES_PGLS_ROOT, "wasm", "wasm"); + fs.mkdirSync(wasmDir, { recursive: true }); - for (const asset of ["pgls.js", "pgls.wasm"]) { - const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/${asset}`; + for (const asset of ["pgls.js", "pgls.wasm"]) { + const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/${asset}`; - const response = await fetch(assetUrl.trim(), { - headers: { - Authorization: `token ${githubToken}`, - Accept: "application/octet-stream", - }, - }); + const response = await fetch(assetUrl.trim(), { + headers: { + Authorization: `token ${githubToken}`, + Accept: "application/octet-stream", + }, + }); - if (!response.ok) { - throw new Error(`Failed to Fetch WASM Asset from ${assetUrl}`); - } + if (!response.ok) { + throw new Error(`Failed to Fetch WASM Asset from ${assetUrl}`); + } - const fileStream = fs.createWriteStream(resolve(wasmDir, asset)); - await streamPipeline(response.body, fileStream); + const fileStream = fs.createWriteStream(resolve(wasmDir, asset)); + await streamPipeline(response.body, fileStream); - console.log(`Downloaded WASM asset ${asset} for ${releaseTag}`); - } + console.log(`Downloaded WASM asset ${asset} for ${releaseTag}`); + } } async function downloadBinary(platform, arch, os, releaseTag, githubToken) { - const buildName = getBuildName(platform, arch); - const ext = getBinaryExt(os); - const assetName = - ext && buildName.endsWith(ext) ? buildName : `${buildName}${ext}`; + const buildName = getBuildName(platform, arch); + const ext = getBinaryExt(os); + const assetName = ext && buildName.endsWith(ext) ? buildName : `${buildName}${ext}`; - const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/${assetName}`; + const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/${assetName}`; - const response = await fetch(assetUrl.trim(), { - headers: { - Authorization: `token ${githubToken}`, - Accept: "application/octet-stream", - }, - }); + const response = await fetch(assetUrl.trim(), { + headers: { + Authorization: `token ${githubToken}`, + Accept: "application/octet-stream", + }, + }); - if (!response.ok) { - const error = await response.text(); - throw new Error( - `Failed to Fetch Asset from ${assetUrl} (Reason: ${error})`, - ); - } + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to Fetch Asset from ${assetUrl} (Reason: ${error})`); + } - // just download to root. - const fileStream = fs.createWriteStream(getBinarySource(platform, arch, os)); + // just download to root. + const fileStream = fs.createWriteStream(getBinarySource(platform, arch, os)); - await streamPipeline(response.body, fileStream); + await streamPipeline(response.body, fileStream); - console.log(`Downloaded asset for ${buildName} (v${releaseTag})`); + console.log(`Downloaded asset for ${buildName} (v${releaseTag})`); } -async function writeManifest( - packagePath, - version, - { versionOnly = false } = {}, -) { - const manifestPath = resolve(PACKAGES_PGLS_ROOT, packagePath, "package.json"); - - const manifestData = JSON.parse( - fs.readFileSync(manifestPath).toString("utf-8"), - ); - - manifestData.version = version; - - if (!versionOnly) { - const nativePackages = platformArchCombinations().map( - ({ platform, arch }) => [getPackageName(platform, arch), version], - ); - manifestData.optionalDependencies = Object.fromEntries(nativePackages); - } - - console.log(`Update manifest ${manifestPath}`); - const content = JSON.stringify(manifestData, null, 2); - - /** - * writeFileSync seemed to not work reliably? - */ - await new Promise((res, rej) => { - fs.writeFile(manifestPath, content, (e) => (e ? rej(e) : res())); - }); +async function writeManifest(packagePath, version, { versionOnly = false } = {}) { + const manifestPath = resolve(PACKAGES_PGLS_ROOT, packagePath, "package.json"); + + const manifestData = JSON.parse(fs.readFileSync(manifestPath).toString("utf-8")); + + manifestData.version = version; + + if (!versionOnly) { + const nativePackages = platformArchCombinations().map(({ platform, arch }) => [ + getPackageName(platform, arch), + version, + ]); + manifestData.optionalDependencies = Object.fromEntries(nativePackages); + } + + console.log(`Update manifest ${manifestPath}`); + const content = JSON.stringify(manifestData, null, 2); + + /** + * writeFileSync seemed to not work reliably? + */ + await new Promise((res, rej) => { + fs.writeFile(manifestPath, content, (e) => (e ? rej(e) : res())); + }); } async function makePackageDir(platform, arch) { - const buildName = getBuildName(platform, arch); - const packageRoot = resolve(PACKAGES_PGLS_ROOT, buildName); + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_PGLS_ROOT, buildName); - await new Promise((res, rej) => { - fs.mkdir(packageRoot, {}, (e) => (e ? rej(e) : res())); - }); + await new Promise((res, rej) => { + fs.mkdir(packageRoot, {}, (e) => (e ? rej(e) : res())); + }); } function copyBinaryToNativePackage(platform, arch, os) { - // Update the package.json manifest - const buildName = getBuildName(platform, arch); - const packageRoot = resolve(PACKAGES_PGLS_ROOT, buildName); - const packageName = getPackageName(platform, arch); - - const { version, license, repository, engines } = rootManifest(); - - /** - * We need to map rust triplets to NPM-known values. - * Otherwise, npm will abort the package installation. - */ - const npm_arch = arch === "aarch64" ? "arm64" : "x64"; - let libc = undefined; - let npm_os = undefined; - - switch (os) { - case "linux": { - libc = platform.endsWith("musl") ? "musl" : "gnu"; - npm_os = "linux"; - break; - } - case "windows": { - libc = "msvc"; - npm_os = "win32"; - break; - } - case "darwin": { - libc = undefined; - npm_os = "darwin"; - break; - } - default: { - throw new Error(`Unsupported os: ${os}`); - } - } - - const manifest = JSON.stringify( - { - name: packageName, - version, - license, - repository, - engines, - os: [npm_os], - cpu: [npm_arch], - libc, - }, - null, - 2, - ); - - const ext = getBinaryExt(os); - const manifestPath = resolve(packageRoot, "package.json"); - console.info(`Update manifest ${manifestPath}`); - fs.writeFileSync(manifestPath, manifest); - - // Copy the CLI binary - const binarySource = getBinarySource(platform, arch, os); - const binaryTarget = resolve(packageRoot, `postgres-language-server${ext}`); - - if (!fs.existsSync(binarySource)) { - console.error( - `Source for binary for ${buildName} not found at: ${binarySource}`, - ); - process.exit(1); - } - - console.info(`Copy binary ${binaryTarget}`); - fs.copyFileSync(binarySource, binaryTarget); - fs.chmodSync(binaryTarget, 0o755); + // Update the package.json manifest + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_PGLS_ROOT, buildName); + const packageName = getPackageName(platform, arch); + + const { version, license, repository, engines } = rootManifest(); + + /** + * We need to map rust triplets to NPM-known values. + * Otherwise, npm will abort the package installation. + */ + const npm_arch = arch === "aarch64" ? "arm64" : "x64"; + let libc = undefined; + let npm_os = undefined; + + switch (os) { + case "linux": { + libc = platform.endsWith("musl") ? "musl" : "gnu"; + npm_os = "linux"; + break; + } + case "windows": { + libc = "msvc"; + npm_os = "win32"; + break; + } + case "darwin": { + libc = undefined; + npm_os = "darwin"; + break; + } + default: { + throw new Error(`Unsupported os: ${os}`); + } + } + + const manifest = JSON.stringify( + { + name: packageName, + version, + license, + repository, + engines, + os: [npm_os], + cpu: [npm_arch], + libc, + }, + null, + 2, + ); + + const ext = getBinaryExt(os); + const manifestPath = resolve(packageRoot, "package.json"); + console.info(`Update manifest ${manifestPath}`); + fs.writeFileSync(manifestPath, manifest); + + // Copy the CLI binary + const binarySource = getBinarySource(platform, arch, os); + const binaryTarget = resolve(packageRoot, `postgres-language-server${ext}`); + + if (!fs.existsSync(binarySource)) { + console.error(`Source for binary for ${buildName} not found at: ${binarySource}`); + process.exit(1); + } + + console.info(`Copy binary ${binaryTarget}`); + fs.copyFileSync(binarySource, binaryTarget); + fs.chmodSync(binaryTarget, 0o755); } function copySchemaToNativePackage(platform, arch) { - const buildName = getBuildName(platform, arch); - const packageRoot = resolve(PACKAGES_PGLS_ROOT, buildName); + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_PGLS_ROOT, buildName); - const schemaSrc = resolve(PGLS_ROOT, "schema.json"); - const schemaTarget = resolve(packageRoot, "schema.json"); + const schemaSrc = resolve(PGLS_ROOT, "schema.json"); + const schemaTarget = resolve(packageRoot, "schema.json"); - if (!fs.existsSync(schemaSrc)) { - console.error(`schema.json not found at: ${schemaSrc}`); - process.exit(1); - } + if (!fs.existsSync(schemaSrc)) { + console.error(`schema.json not found at: ${schemaSrc}`); + process.exit(1); + } - console.info("Copying schema.json"); - fs.copyFileSync(schemaSrc, schemaTarget); - fs.chmodSync(schemaTarget, 0o666); + console.info("Copying schema.json"); + fs.copyFileSync(schemaSrc, schemaTarget); + fs.chmodSync(schemaTarget, 0o666); } function copyReadmeToPackage(packagePath) { - const packageRoot = resolve(PACKAGES_PGLS_ROOT, packagePath); - const readmeSrc = resolve(PGLS_ROOT, "README.md"); - const readmeTarget = resolve(packageRoot, "README.md"); - - if (!fs.existsSync(readmeSrc)) { - console.error(`README.md not found at: ${readmeSrc}`); - process.exit(1); - } - - console.info(`Copying README.md to ${packagePath}`); - fs.copyFileSync(readmeSrc, readmeTarget); - fs.chmodSync(readmeTarget, 0o666); + const packageRoot = resolve(PACKAGES_PGLS_ROOT, packagePath); + const readmeSrc = resolve(PGLS_ROOT, "README.md"); + const readmeTarget = resolve(packageRoot, "README.md"); + + if (!fs.existsSync(readmeSrc)) { + console.error(`README.md not found at: ${readmeSrc}`); + process.exit(1); + } + + console.info(`Copying README.md to ${packagePath}`); + fs.copyFileSync(readmeSrc, readmeTarget); + fs.chmodSync(readmeTarget, 0o666); } -const rootManifest = () => - JSON.parse(fs.readFileSync(MANIFEST_PATH).toString("utf-8")); +const rootManifest = () => JSON.parse(fs.readFileSync(MANIFEST_PATH).toString("utf-8")); function getBinaryExt(os) { - return os === "windows" ? ".exe" : ""; + return os === "windows" ? ".exe" : ""; } function getBinarySource(platform, arch, os) { - const ext = getBinaryExt(os); - return resolve(PGLS_ROOT, `${getBuildName(platform, arch)}${ext}`); + const ext = getBinaryExt(os); + return resolve(PGLS_ROOT, `${getBuildName(platform, arch)}${ext}`); } function getBuildName(platform, arch) { - return `postgres-language-server_${arch}-${platform}${platform.includes("windows") ? ".exe" : ""}`; + return `postgres-language-server_${arch}-${platform}${platform.includes("windows") ? ".exe" : ""}`; } function getPackageName(platform, arch) { - // trim the "unknown" from linux and the "pc" from windows - const platformName = platform.split("-").slice(-2).join("-"); - return `@postgres-language-server/cli-${arch}-${platformName}`; + // trim the "unknown" from linux and the "pc" from windows + const platformName = platform.split("-").slice(-2).join("-"); + return `@postgres-language-server/cli-${arch}-${platformName}`; } function getOs(platform) { - return platform.split("-").find((_, idx) => idx === 1); + return platform.split("-").find((_, idx) => idx === 1); } function getVersion(releaseTag, isPrerelease) { - return releaseTag + (isPrerelease ? "-rc" : ""); + return releaseTag + (isPrerelease ? "-rc" : ""); } (async function main() { - const githubToken = process.env.GITHUB_TOKEN; - const releaseTag = process.env.RELEASE_TAG; - assert(githubToken, "GITHUB_TOKEN not defined!"); - assert(releaseTag, "RELEASE_TAG not defined!"); - - const isPrerelease = process.env.PRERELEASE === "true"; - - await downloadSchema(releaseTag, githubToken); - await downloadWasmAssets(releaseTag, githubToken); - const version = getVersion(releaseTag, isPrerelease); - await writeManifest("cli", version); - await writeManifest("backend-jsonrpc", version); - await writeManifest("wasm", version, { versionOnly: true }); - - // Copy README to main packages - copyReadmeToPackage("cli"); - copyReadmeToPackage("backend-jsonrpc"); - - for (const { platform, arch } of platformArchCombinations()) { - const os = getOs(platform); - await makePackageDir(platform, arch); - await downloadBinary(platform, arch, os, releaseTag, githubToken); - copyBinaryToNativePackage(platform, arch, os); - copySchemaToNativePackage(platform, arch); - } - - process.exit(0); + const githubToken = process.env.GITHUB_TOKEN; + const releaseTag = process.env.RELEASE_TAG; + assert(githubToken, "GITHUB_TOKEN not defined!"); + assert(releaseTag, "RELEASE_TAG not defined!"); + + const isPrerelease = process.env.PRERELEASE === "true"; + + await downloadSchema(releaseTag, githubToken); + await downloadWasmAssets(releaseTag, githubToken); + const version = getVersion(releaseTag, isPrerelease); + await writeManifest("cli", version); + await writeManifest("backend-jsonrpc", version); + await writeManifest("wasm", version, { versionOnly: true }); + + // Copy README to main packages + copyReadmeToPackage("cli"); + copyReadmeToPackage("backend-jsonrpc"); + + for (const { platform, arch } of platformArchCombinations()) { + const os = getOs(platform); + await makePackageDir(platform, arch); + await downloadBinary(platform, arch, os, releaseTag, githubToken); + copyBinaryToNativePackage(platform, arch, os); + copySchemaToNativePackage(platform, arch); + } + + process.exit(0); })(); diff --git a/packages/@postgres-language-server/cli/test/bin.test.js b/packages/@postgres-language-server/cli/test/bin.test.js index 50ca020b5..cd0e586b8 100644 --- a/packages/@postgres-language-server/cli/test/bin.test.js +++ b/packages/@postgres-language-server/cli/test/bin.test.js @@ -8,58 +8,54 @@ const binPath = join(__dirname, "../bin/postgres-language-server"); const testSqlPath = join(__dirname, "test.sql"); describe("postgres-language-server bin", () => { - it("should check a SQL file successfully", async () => { - const result = await new Promise((resolve) => { - const proc = spawn( - "node", - [binPath, "check", "--disable-db", testSqlPath], - { - env: { ...process.env }, - }, - ); - - let stdout = ""; - let stderr = ""; - - proc.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - proc.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - proc.on("close", (code) => { - resolve({ code, stdout, stderr }); - }); - }); - - expect(result.code).toBe(0); - expect(result.stderr).toBe(""); - }); - - it("should fail when file doesn't exist", async () => { - const result = await new Promise((resolve) => { - const proc = spawn("node", [binPath, "check", "nonexistent.sql"], { - env: { ...process.env }, - }); - - let stdout = ""; - let stderr = ""; - - proc.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - proc.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - proc.on("close", (code) => { - resolve({ code, stdout, stderr }); - }); - }); - - expect(result.code).not.toBe(0); - }); + it("should check a SQL file successfully", async () => { + const result = await new Promise((resolve) => { + const proc = spawn("node", [binPath, "check", "--disable-db", testSqlPath], { + env: { ...process.env }, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + resolve({ code, stdout, stderr }); + }); + }); + + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + }); + + it("should fail when file doesn't exist", async () => { + const result = await new Promise((resolve) => { + const proc = spawn("node", [binPath, "check", "nonexistent.sql"], { + env: { ...process.env }, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + resolve({ code, stdout, stderr }); + }); + }); + + expect(result.code).not.toBe(0); + }); }); diff --git a/packages/@postgres-language-server/wasm/README.md b/packages/@postgres-language-server/wasm/README.md index 23e5ef382..103306b32 100644 --- a/packages/@postgres-language-server/wasm/README.md +++ b/packages/@postgres-language-server/wasm/README.md @@ -14,10 +14,10 @@ bun add @postgres-language-server/wasm This package provides two separate APIs. Choose the one that fits your use case: -| API | Use Case | Import Path | -|-----|----------|-------------| -| **Workspace** | Direct parse, lint, complete, hover | `@postgres-language-server/wasm/workspace` | -| **LanguageServer** | Full LSP JSON-RPC protocol | `@postgres-language-server/wasm/lsp` | +| API | Use Case | Import Path | +| ------------------ | ----------------------------------- | ------------------------------------------ | +| **Workspace** | Direct parse, lint, complete, hover | `@postgres-language-server/wasm/workspace` | +| **LanguageServer** | Full LSP JSON-RPC protocol | `@postgres-language-server/wasm/lsp` | Each API manages its own workspace independently. Use one or the other, not both. @@ -26,23 +26,23 @@ Each API manages its own workspace independently. Use one or the other, not both Use this for custom editor integrations, build-time SQL linting, or simple tooling that doesn't need full LSP. ```typescript -import { createWorkspace } from '@postgres-language-server/wasm/workspace'; +import { createWorkspace } from "@postgres-language-server/wasm/workspace"; const workspace = await createWorkspace(); // Parse SQL and get errors -const errors = workspace.parse('SELECT * FROM users;'); +const errors = workspace.parse("SELECT * FROM users;"); console.log(errors); // [] // Insert a file and lint it -workspace.insertFile('/query.sql', 'SELECT * FROM users;'); -const diagnostics = workspace.lint('/query.sql'); +workspace.insertFile("/query.sql", "SELECT * FROM users;"); +const diagnostics = workspace.lint("/query.sql"); // Get completions -const completions = workspace.complete('/query.sql', 14); // position after "FROM " +const completions = workspace.complete("/query.sql", 14); // position after "FROM " // Get hover info -const hover = workspace.hover('/query.sql', 14); // position over "users" +const hover = workspace.hover("/query.sql", 14); // position over "users" ``` ### With Schema @@ -72,16 +72,16 @@ const completions = workspace.complete('/query.sql', 14); Use this for Monaco editor integration with `monaco-languageclient` or any editor that speaks LSP protocol. ```typescript -import { createLanguageServer } from '@postgres-language-server/wasm/lsp'; +import { createLanguageServer } from "@postgres-language-server/wasm/lsp"; const lsp = await createLanguageServer(); // Handle LSP messages const responses = lsp.handleMessage({ - jsonrpc: '2.0', + jsonrpc: "2.0", id: 1, - method: 'initialize', - params: { capabilities: {} } + method: "initialize", + params: { capabilities: {} }, }); // responses is an array of outgoing messages @@ -96,14 +96,14 @@ For Monaco editor, run the language server in a web worker: ```typescript // lsp-worker.js -import { createLanguageServer } from '@postgres-language-server/wasm/lsp'; +import { createLanguageServer } from "@postgres-language-server/wasm/lsp"; let lsp = null; self.onmessage = async (event) => { if (!lsp) { lsp = await createLanguageServer(); - self.postMessage({ type: 'ready' }); + self.postMessage({ type: "ready" }); } const responses = lsp.handleMessage(event.data); @@ -119,9 +119,9 @@ Use the `pgls/setSchema` notification: ```typescript lsp.handleMessage({ - jsonrpc: '2.0', - method: 'pgls/setSchema', - params: { schema: JSON.stringify(schemaCache) } + jsonrpc: "2.0", + method: "pgls/setSchema", + params: { schema: JSON.stringify(schemaCache) }, }); ``` @@ -129,22 +129,22 @@ lsp.handleMessage({ ### Workspace -| Method | Description | -|--------|-------------| -| `parse(sql: string)` | Parse SQL, returns array of error messages | -| `insertFile(path, content)` | Add or update a file in the workspace | -| `removeFile(path)` | Remove a file from the workspace | -| `lint(path)` | Get diagnostics for a file | -| `complete(path, offset)` | Get completions at position | -| `hover(path, offset)` | Get hover info at position | -| `setSchema(json)` | Set database schema | -| `clearSchema()` | Clear the current schema | -| `version()` | Get library version | +| Method | Description | +| --------------------------- | ------------------------------------------ | +| `parse(sql: string)` | Parse SQL, returns array of error messages | +| `insertFile(path, content)` | Add or update a file in the workspace | +| `removeFile(path)` | Remove a file from the workspace | +| `lint(path)` | Get diagnostics for a file | +| `complete(path, offset)` | Get completions at position | +| `hover(path, offset)` | Get hover info at position | +| `setSchema(json)` | Set database schema | +| `clearSchema()` | Clear the current schema | +| `version()` | Get library version | ### LanguageServer -| Method | Description | -|--------|-------------| +| Method | Description | +| -------------------- | -------------------------------------------------------- | | `handleMessage(msg)` | Process LSP JSON-RPC message, returns array of responses | ### Supported LSP Methods diff --git a/packages/@postgres-language-server/wasm/e2e/index.html b/packages/@postgres-language-server/wasm/e2e/index.html index 89a0779ed..b07c30f97 100644 --- a/packages/@postgres-language-server/wasm/e2e/index.html +++ b/packages/@postgres-language-server/wasm/e2e/index.html @@ -1,55 +1,66 @@ - + - - - + + + PGLS Monaco E2E Test - - + +
Loading...
@@ -58,219 +69,229 @@ - + diff --git a/packages/@postgres-language-server/wasm/e2e/lsp-worker.js b/packages/@postgres-language-server/wasm/e2e/lsp-worker.js index 6b85f76f1..3b781487d 100644 --- a/packages/@postgres-language-server/wasm/e2e/lsp-worker.js +++ b/packages/@postgres-language-server/wasm/e2e/lsp-worker.js @@ -5,42 +5,42 @@ */ let lsp = null; -let initialized = false; +let _initialized = false; // Import the LSP module async function init() { - try { - const { createLanguageServer } = await import("/dist/lsp.js"); - lsp = await createLanguageServer(); - initialized = true; - self.postMessage({ type: "ready" }); - } catch (err) { - self.postMessage({ type: "error", message: err.message }); - } + try { + const { createLanguageServer } = await import("/dist/lsp.js"); + lsp = await createLanguageServer(); + _initialized = true; + self.postMessage({ type: "ready" }); + } catch (err) { + self.postMessage({ type: "error", message: err.message }); + } } // Handle messages from main thread self.onmessage = async (event) => { - const { type, message, id } = event.data; + const { type, message, id } = event.data; - if (type === "init") { - await init(); - return; - } + if (type === "init") { + await init(); + return; + } - if (type === "message" && lsp) { - try { - const responses = lsp.handleMessage(message); - // Send all responses back - for (const response of responses) { - self.postMessage({ type: "response", response, requestId: id }); - } - } catch (err) { - self.postMessage({ - type: "error", - message: err.message, - requestId: id, - }); - } - } + if (type === "message" && lsp) { + try { + const responses = lsp.handleMessage(message); + // Send all responses back + for (const response of responses) { + self.postMessage({ type: "response", response, requestId: id }); + } + } catch (err) { + self.postMessage({ + type: "error", + message: err.message, + requestId: id, + }); + } + } }; diff --git a/packages/@postgres-language-server/wasm/e2e/monaco.test.ts b/packages/@postgres-language-server/wasm/e2e/monaco.test.ts index 93a3711df..003c6b234 100644 --- a/packages/@postgres-language-server/wasm/e2e/monaco.test.ts +++ b/packages/@postgres-language-server/wasm/e2e/monaco.test.ts @@ -13,480 +13,473 @@ import { expect, test } from "@playwright/test"; */ const TEST_SCHEMA = { - schemas: [ - { - id: 1, - name: "public", - owner: "postgres", - allowed_users: [], - allowed_creators: [], - table_count: 1, - view_count: 0, - function_count: 0, - total_size: "0 bytes", - comment: null, - }, - ], - tables: [ - { - id: 1, - schema: "public", - name: "users", - rls_enabled: false, - rls_forced: false, - replica_identity: "Default", - table_kind: "Ordinary", - bytes: 0, - size: "0 bytes", - live_rows_estimate: 0, - dead_rows_estimate: 0, - comment: null, - }, - { - id: 2, - schema: "public", - name: "orders", - rls_enabled: false, - rls_forced: false, - replica_identity: "Default", - table_kind: "Ordinary", - bytes: 0, - size: "0 bytes", - live_rows_estimate: 0, - dead_rows_estimate: 0, - comment: null, - }, - ], - columns: [ - { - name: "id", - table_name: "users", - table_oid: 1, - class_kind: "OrdinaryTable", - number: 1, - schema_name: "public", - type_id: 23, - type_name: "integer", - is_nullable: false, - is_primary_key: true, - is_unique: true, - default_expr: null, - varchar_length: null, - comment: null, - }, - { - name: "username", - table_name: "users", - table_oid: 1, - class_kind: "OrdinaryTable", - number: 2, - schema_name: "public", - type_id: 25, - type_name: "text", - is_nullable: false, - is_primary_key: false, - is_unique: true, - default_expr: null, - varchar_length: null, - comment: "The user display name", - }, - ], - functions: [], - types: [], - version: { - version: "16.0", - version_num: 160000, - major_version: 16, - active_connections: 1, - max_connections: 100, - }, - policies: [], - extensions: [], - triggers: [], - roles: [], + schemas: [ + { + id: 1, + name: "public", + owner: "postgres", + allowed_users: [], + allowed_creators: [], + table_count: 1, + view_count: 0, + function_count: 0, + total_size: "0 bytes", + comment: null, + }, + ], + tables: [ + { + id: 1, + schema: "public", + name: "users", + rls_enabled: false, + rls_forced: false, + replica_identity: "Default", + table_kind: "Ordinary", + bytes: 0, + size: "0 bytes", + live_rows_estimate: 0, + dead_rows_estimate: 0, + comment: null, + }, + { + id: 2, + schema: "public", + name: "orders", + rls_enabled: false, + rls_forced: false, + replica_identity: "Default", + table_kind: "Ordinary", + bytes: 0, + size: "0 bytes", + live_rows_estimate: 0, + dead_rows_estimate: 0, + comment: null, + }, + ], + columns: [ + { + name: "id", + table_name: "users", + table_oid: 1, + class_kind: "OrdinaryTable", + number: 1, + schema_name: "public", + type_id: 23, + type_name: "integer", + is_nullable: false, + is_primary_key: true, + is_unique: true, + default_expr: null, + varchar_length: null, + comment: null, + }, + { + name: "username", + table_name: "users", + table_oid: 1, + class_kind: "OrdinaryTable", + number: 2, + schema_name: "public", + type_id: 25, + type_name: "text", + is_nullable: false, + is_primary_key: false, + is_unique: true, + default_expr: null, + varchar_length: null, + comment: "The user display name", + }, + ], + functions: [], + types: [], + version: { + version: "16.0", + version_num: 160000, + major_version: 16, + active_connections: 1, + max_connections: 100, + }, + policies: [], + extensions: [], + triggers: [], + roles: [], }; test.describe("Monaco Editor with PGLS Language Server (Web Worker)", () => { - test.beforeEach(async ({ page }) => { - await page.goto("/"); - - // Wait for the editor to be ready (or error) - await page.waitForFunction( - () => { - const status = document.getElementById("status"); - return ( - status?.classList.contains("ready") || - status?.classList.contains("error") - ); - }, - { timeout: 30000 }, - ); - - // Check if WASM loaded successfully - const hasError = await page - .locator("#status") - .evaluate((el) => el.classList.contains("error")); - - if (hasError) { - const errorText = await page.locator("#status").textContent(); - if (errorText?.includes("expected magic")) { - test.skip(true, "WASM file is invalid or missing. Run build-wasm.sh"); - } else { - throw new Error(`WASM failed to load: ${errorText}`); - } - } - }); - - test("loads Monaco editor and LSP worker", async ({ page }) => { - // Verify Monaco editor is loaded - const editor = page.locator(".monaco-editor"); - await expect(editor).toBeVisible(); - - // Verify status shows ready - await expect(page.locator("#status")).toHaveText("Ready"); - - // Verify LSP client (worker) is available - const hasLspClient = await page.evaluate(() => { - return typeof (window as any).pglsLspClient !== "undefined"; - }); - expect(hasLspClient).toBe(true); - }); - - test("shows no diagnostics for valid SQL", async ({ page }) => { - // Clear editor and type valid SQL - await page.evaluate(() => { - (window as any).monacoEditor.setValue("SELECT 1;"); - }); - - // Wait for diagnostics to update - await page.waitForTimeout(500); - - // Check diagnostics panel - const diagnosticsEl = page.locator('[data-testid="diagnostics"]'); - await expect(diagnosticsEl).toContainText("No diagnostics"); - }); - - test("shows diagnostics for invalid SQL", async ({ page }) => { - // Type invalid SQL - await page.evaluate(() => { - (window as any).monacoEditor.setValue("SELEC * FROM users;"); - }); - - // Wait for diagnostics to update - await page.waitForTimeout(500); - - // Check that diagnostics appear - const diagnostics = await page.evaluate(() => { - return (window as any).lastDiagnostics; - }); - - expect(diagnostics).toBeDefined(); - expect(diagnostics.length).toBeGreaterThan(0); - - // Check diagnostics panel shows error - const diagnosticsEl = page.locator('[data-testid="diagnostics"]'); - await expect(diagnosticsEl).toContainText("error"); - }); - - test("updates diagnostics on content change", async ({ page }) => { - // Start with invalid SQL - await page.evaluate(() => { - (window as any).monacoEditor.setValue("SELEC 1;"); - }); - - await page.waitForTimeout(500); - - // Verify error appears - let diagnostics = await page.evaluate(() => { - return (window as any).lastDiagnostics; - }); - expect(diagnostics.length).toBeGreaterThan(0); - - // Fix the SQL - await page.evaluate(() => { - (window as any).monacoEditor.setValue("SELECT 1;"); - }); - - await page.waitForTimeout(500); - - // Verify error is gone - diagnostics = await page.evaluate(() => { - return (window as any).lastDiagnostics; - }); - expect(diagnostics.length).toBe(0); - }); - - test("LSP initialize request returns capabilities", async ({ page }) => { - const response = await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - return await client.sendMessage({ - jsonrpc: "2.0", - id: 100, - method: "initialize", - params: { capabilities: {} }, - }); - }); - - expect(response.length).toBeGreaterThanOrEqual(1); - const initResponse = response.find((r: any) => r.id === 100); - expect(initResponse).toBeDefined(); - expect(initResponse.result).toBeDefined(); - expect(initResponse.result.capabilities).toBeDefined(); - }); - - test("LSP shutdown request works", async ({ page }) => { - const response = await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - return await client.sendMessage({ - jsonrpc: "2.0", - id: 101, - method: "shutdown", - params: null, - }); - }); - - expect(response.length).toBeGreaterThanOrEqual(1); - const shutdownResponse = response.find((r: any) => r.id === 101); - expect(shutdownResponse).toBeDefined(); - expect(shutdownResponse.result).toBeNull(); - }); - - test("LSP didOpen returns publishDiagnostics", async ({ page }) => { - const response = await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - return await client.sendMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///worker-test.sql", - languageId: "sql", - version: 1, - text: "SELEC 1;", - }, - }, - }); - }); - - const diagNotification = response.find( - (m: any) => m.method === "textDocument/publishDiagnostics", - ); - expect(diagNotification).toBeDefined(); - expect(diagNotification?.params?.diagnostics?.length).toBeGreaterThan(0); - }); - - test("LSP completion request returns results", async ({ page }) => { - // Open a document first - await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - await client.sendMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///completion-test.sql", - languageId: "sql", - version: 1, - text: "SELECT ", - }, - }, - }); - }); - - // Request completions - const response = await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - return await client.sendMessage({ - jsonrpc: "2.0", - id: 102, - method: "textDocument/completion", - params: { - textDocument: { uri: "file:///completion-test.sql" }, - position: { line: 0, character: 7 }, - }, - }); - }); - - expect(response.length).toBeGreaterThanOrEqual(1); - const completionResponse = response.find((r: any) => r.id === 102); - expect(completionResponse).toBeDefined(); - expect(completionResponse.result).toBeDefined(); - expect(Array.isArray(completionResponse.result)).toBe(true); - }); - - test("LSP hover request works", async ({ page }) => { - // Open a document first - await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - await client.sendMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///hover-test.sql", - languageId: "sql", - version: 1, - text: "SELECT * FROM users;", - }, - }, - }); - }); - - // Request hover - const response = await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - return await client.sendMessage({ - jsonrpc: "2.0", - id: 103, - method: "textDocument/hover", - params: { - textDocument: { uri: "file:///hover-test.sql" }, - position: { line: 0, character: 14 }, - }, - }); - }); - - expect(response.length).toBeGreaterThanOrEqual(1); - const hoverResponse = response.find((r: any) => r.id === 103); - expect(hoverResponse).toBeDefined(); - expect(hoverResponse.jsonrpc).toBe("2.0"); - }); - - test("LSP didChange triggers publishDiagnostics", async ({ page }) => { - // Open a document - await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - await client.sendMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///change-test.sql", - languageId: "sql", - version: 1, - text: "SELECT 1;", - }, - }, - }); - }); - - // Change the document to have an error - const response = await page.evaluate(async () => { - const client = (window as any).pglsLspClient; - return await client.sendMessage({ - jsonrpc: "2.0", - method: "textDocument/didChange", - params: { - textDocument: { - uri: "file:///change-test.sql", - version: 2, - }, - contentChanges: [{ text: "SELEC 1;" }], - }, - }); - }); - - const diagNotification = response.find( - (m: any) => m.method === "textDocument/publishDiagnostics", - ); - expect(diagNotification).toBeDefined(); - expect(diagNotification.params.diagnostics.length).toBeGreaterThan(0); - }); - - test("setSchema via LSP notification enables schema-aware completions", async ({ - page, - }) => { - const schema = TEST_SCHEMA; - - const result = await page.evaluate(async (schema) => { - const client = (window as any).pglsLspClient; - - // Set schema via LSP notification - await client.sendMessage({ - jsonrpc: "2.0", - method: "pgls/setSchema", - params: { schema: JSON.stringify(schema) }, - }); - - // Open a document - await client.sendMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///schema-completion.sql", - languageId: "sql", - version: 1, - text: "SELECT * FROM ", - }, - }, - }); - - // Request completions - const response = await client.sendMessage({ - jsonrpc: "2.0", - id: 200, - method: "textDocument/completion", - params: { - textDocument: { uri: "file:///schema-completion.sql" }, - position: { line: 0, character: 14 }, - }, - }); - - const completionResponse = response.find((r: any) => r.id === 200); - return completionResponse?.result; - }, schema); - - expect(Array.isArray(result)).toBe(true); - const labels = result.map((item: any) => item.label); - expect(labels).toContain("users"); - expect(labels).toContain("orders"); - }); - - test("hover with schema returns column type info", async ({ page }) => { - const schema = TEST_SCHEMA; - - const response = await page.evaluate(async (schema) => { - const client = (window as any).pglsLspClient; - - // Set schema - await client.sendMessage({ - jsonrpc: "2.0", - method: "pgls/setSchema", - params: { schema: JSON.stringify(schema) }, - }); - - // Open a document - await client.sendMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///schema-hover.sql", - languageId: "sql", - version: 1, - text: "SELECT username FROM users;", - }, - }, - }); - - // Request hover over "username" - return await client.sendMessage({ - jsonrpc: "2.0", - id: 201, - method: "textDocument/hover", - params: { - textDocument: { uri: "file:///schema-hover.sql" }, - position: { line: 0, character: 10 }, - }, - }); - }, schema); - - const hoverResponse = response.find((r: any) => r.id === 201); - expect(hoverResponse).toBeDefined(); - // With schema, hover should return content - if (hoverResponse.result !== null) { - expect(hoverResponse.result.contents).toBeDefined(); - } - }); + test.beforeEach(async ({ page }) => { + await page.goto("/"); + + // Wait for the editor to be ready (or error) + await page.waitForFunction( + () => { + const status = document.getElementById("status"); + return status?.classList.contains("ready") || status?.classList.contains("error"); + }, + { timeout: 30000 }, + ); + + // Check if WASM loaded successfully + const hasError = await page.locator("#status").evaluate((el) => el.classList.contains("error")); + + if (hasError) { + const errorText = await page.locator("#status").textContent(); + if (errorText?.includes("expected magic")) { + test.skip(true, "WASM file is invalid or missing. Run build-wasm.sh"); + } else { + throw new Error(`WASM failed to load: ${errorText}`); + } + } + }); + + test("loads Monaco editor and LSP worker", async ({ page }) => { + // Verify Monaco editor is loaded + const editor = page.locator(".monaco-editor"); + await expect(editor).toBeVisible(); + + // Verify status shows ready + await expect(page.locator("#status")).toHaveText("Ready"); + + // Verify LSP client (worker) is available + const hasLspClient = await page.evaluate(() => { + return typeof (window as any).pglsLspClient !== "undefined"; + }); + expect(hasLspClient).toBe(true); + }); + + test("shows no diagnostics for valid SQL", async ({ page }) => { + // Clear editor and type valid SQL + await page.evaluate(() => { + (window as any).monacoEditor.setValue("SELECT 1;"); + }); + + // Wait for diagnostics to update + await page.waitForTimeout(500); + + // Check diagnostics panel + const diagnosticsEl = page.locator('[data-testid="diagnostics"]'); + await expect(diagnosticsEl).toContainText("No diagnostics"); + }); + + test("shows diagnostics for invalid SQL", async ({ page }) => { + // Type invalid SQL + await page.evaluate(() => { + (window as any).monacoEditor.setValue("SELEC * FROM users;"); + }); + + // Wait for diagnostics to update + await page.waitForTimeout(500); + + // Check that diagnostics appear + const diagnostics = await page.evaluate(() => { + return (window as any).lastDiagnostics; + }); + + expect(diagnostics).toBeDefined(); + expect(diagnostics.length).toBeGreaterThan(0); + + // Check diagnostics panel shows error + const diagnosticsEl = page.locator('[data-testid="diagnostics"]'); + await expect(diagnosticsEl).toContainText("error"); + }); + + test("updates diagnostics on content change", async ({ page }) => { + // Start with invalid SQL + await page.evaluate(() => { + (window as any).monacoEditor.setValue("SELEC 1;"); + }); + + await page.waitForTimeout(500); + + // Verify error appears + let diagnostics = await page.evaluate(() => { + return (window as any).lastDiagnostics; + }); + expect(diagnostics.length).toBeGreaterThan(0); + + // Fix the SQL + await page.evaluate(() => { + (window as any).monacoEditor.setValue("SELECT 1;"); + }); + + await page.waitForTimeout(500); + + // Verify error is gone + diagnostics = await page.evaluate(() => { + return (window as any).lastDiagnostics; + }); + expect(diagnostics.length).toBe(0); + }); + + test("LSP initialize request returns capabilities", async ({ page }) => { + const response = await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + return await client.sendMessage({ + jsonrpc: "2.0", + id: 100, + method: "initialize", + params: { capabilities: {} }, + }); + }); + + expect(response.length).toBeGreaterThanOrEqual(1); + const initResponse = response.find((r: any) => r.id === 100); + expect(initResponse).toBeDefined(); + expect(initResponse.result).toBeDefined(); + expect(initResponse.result.capabilities).toBeDefined(); + }); + + test("LSP shutdown request works", async ({ page }) => { + const response = await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + return await client.sendMessage({ + jsonrpc: "2.0", + id: 101, + method: "shutdown", + params: null, + }); + }); + + expect(response.length).toBeGreaterThanOrEqual(1); + const shutdownResponse = response.find((r: any) => r.id === 101); + expect(shutdownResponse).toBeDefined(); + expect(shutdownResponse.result).toBeNull(); + }); + + test("LSP didOpen returns publishDiagnostics", async ({ page }) => { + const response = await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + return await client.sendMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///worker-test.sql", + languageId: "sql", + version: 1, + text: "SELEC 1;", + }, + }, + }); + }); + + const diagNotification = response.find( + (m: any) => m.method === "textDocument/publishDiagnostics", + ); + expect(diagNotification).toBeDefined(); + expect(diagNotification?.params?.diagnostics?.length).toBeGreaterThan(0); + }); + + test("LSP completion request returns results", async ({ page }) => { + // Open a document first + await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + await client.sendMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///completion-test.sql", + languageId: "sql", + version: 1, + text: "SELECT ", + }, + }, + }); + }); + + // Request completions + const response = await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + return await client.sendMessage({ + jsonrpc: "2.0", + id: 102, + method: "textDocument/completion", + params: { + textDocument: { uri: "file:///completion-test.sql" }, + position: { line: 0, character: 7 }, + }, + }); + }); + + expect(response.length).toBeGreaterThanOrEqual(1); + const completionResponse = response.find((r: any) => r.id === 102); + expect(completionResponse).toBeDefined(); + expect(completionResponse.result).toBeDefined(); + expect(Array.isArray(completionResponse.result)).toBe(true); + }); + + test("LSP hover request works", async ({ page }) => { + // Open a document first + await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + await client.sendMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///hover-test.sql", + languageId: "sql", + version: 1, + text: "SELECT * FROM users;", + }, + }, + }); + }); + + // Request hover + const response = await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + return await client.sendMessage({ + jsonrpc: "2.0", + id: 103, + method: "textDocument/hover", + params: { + textDocument: { uri: "file:///hover-test.sql" }, + position: { line: 0, character: 14 }, + }, + }); + }); + + expect(response.length).toBeGreaterThanOrEqual(1); + const hoverResponse = response.find((r: any) => r.id === 103); + expect(hoverResponse).toBeDefined(); + expect(hoverResponse.jsonrpc).toBe("2.0"); + }); + + test("LSP didChange triggers publishDiagnostics", async ({ page }) => { + // Open a document + await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + await client.sendMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///change-test.sql", + languageId: "sql", + version: 1, + text: "SELECT 1;", + }, + }, + }); + }); + + // Change the document to have an error + const response = await page.evaluate(async () => { + const client = (window as any).pglsLspClient; + return await client.sendMessage({ + jsonrpc: "2.0", + method: "textDocument/didChange", + params: { + textDocument: { + uri: "file:///change-test.sql", + version: 2, + }, + contentChanges: [{ text: "SELEC 1;" }], + }, + }); + }); + + const diagNotification = response.find( + (m: any) => m.method === "textDocument/publishDiagnostics", + ); + expect(diagNotification).toBeDefined(); + expect(diagNotification.params.diagnostics.length).toBeGreaterThan(0); + }); + + test("setSchema via LSP notification enables schema-aware completions", async ({ page }) => { + const schema = TEST_SCHEMA; + + const result = await page.evaluate(async (schema) => { + const client = (window as any).pglsLspClient; + + // Set schema via LSP notification + await client.sendMessage({ + jsonrpc: "2.0", + method: "pgls/setSchema", + params: { schema: JSON.stringify(schema) }, + }); + + // Open a document + await client.sendMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///schema-completion.sql", + languageId: "sql", + version: 1, + text: "SELECT * FROM ", + }, + }, + }); + + // Request completions + const response = await client.sendMessage({ + jsonrpc: "2.0", + id: 200, + method: "textDocument/completion", + params: { + textDocument: { uri: "file:///schema-completion.sql" }, + position: { line: 0, character: 14 }, + }, + }); + + const completionResponse = response.find((r: any) => r.id === 200); + return completionResponse?.result; + }, schema); + + expect(Array.isArray(result)).toBe(true); + const labels = result.map((item: any) => item.label); + expect(labels).toContain("users"); + expect(labels).toContain("orders"); + }); + + test("hover with schema returns column type info", async ({ page }) => { + const schema = TEST_SCHEMA; + + const response = await page.evaluate(async (schema) => { + const client = (window as any).pglsLspClient; + + // Set schema + await client.sendMessage({ + jsonrpc: "2.0", + method: "pgls/setSchema", + params: { schema: JSON.stringify(schema) }, + }); + + // Open a document + await client.sendMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///schema-hover.sql", + languageId: "sql", + version: 1, + text: "SELECT username FROM users;", + }, + }, + }); + + // Request hover over "username" + return await client.sendMessage({ + jsonrpc: "2.0", + id: 201, + method: "textDocument/hover", + params: { + textDocument: { uri: "file:///schema-hover.sql" }, + position: { line: 0, character: 10 }, + }, + }); + }, schema); + + const hoverResponse = response.find((r: any) => r.id === 201); + expect(hoverResponse).toBeDefined(); + // With schema, hover should return content + if (hoverResponse.result !== null) { + expect(hoverResponse.result.contents).toBeDefined(); + } + }); }); diff --git a/packages/@postgres-language-server/wasm/e2e/server.ts b/packages/@postgres-language-server/wasm/e2e/server.ts index 8364f7d24..69a3c1faa 100644 --- a/packages/@postgres-language-server/wasm/e2e/server.ts +++ b/packages/@postgres-language-server/wasm/e2e/server.ts @@ -10,60 +10,60 @@ const PORT = 3000; const ROOT = join(import.meta.dir, ".."); const MIME_TYPES: Record = { - ".html": "text/html", - ".js": "application/javascript", - ".mjs": "application/javascript", - ".ts": "application/javascript", - ".css": "text/css", - ".wasm": "application/wasm", - ".json": "application/json", + ".html": "text/html", + ".js": "application/javascript", + ".mjs": "application/javascript", + ".ts": "application/javascript", + ".css": "text/css", + ".wasm": "application/wasm", + ".json": "application/json", }; function getMimeType(path: string): string { - const ext = extname(path); - return MIME_TYPES[ext] || "application/octet-stream"; + const ext = extname(path); + return MIME_TYPES[ext] || "application/octet-stream"; } -const server = Bun.serve({ - port: PORT, - fetch(req) { - const url = new URL(req.url); - let pathname = url.pathname; +const _server = Bun.serve({ + port: PORT, + fetch(req) { + const url = new URL(req.url); + let pathname = url.pathname; - // Default to index.html - if (pathname === "/") { - pathname = "/e2e/index.html"; - } + // Default to index.html + if (pathname === "/") { + pathname = "/e2e/index.html"; + } - // Map paths - let filePath: string; - if (pathname.startsWith("/wasm/")) { - filePath = join(ROOT, pathname); - } else if (pathname.startsWith("/dist/")) { - filePath = join(ROOT, pathname); - } else if (pathname.startsWith("/e2e/")) { - filePath = join(ROOT, pathname); - } else { - filePath = join(ROOT, "e2e", pathname); - } + // Map paths + let filePath: string; + if (pathname.startsWith("/wasm/")) { + filePath = join(ROOT, pathname); + } else if (pathname.startsWith("/dist/")) { + filePath = join(ROOT, pathname); + } else if (pathname.startsWith("/e2e/")) { + filePath = join(ROOT, pathname); + } else { + filePath = join(ROOT, "e2e", pathname); + } - if (!existsSync(filePath)) { - console.log(`404: ${pathname} (${filePath})`); - return new Response("Not Found", { status: 404 }); - } + if (!existsSync(filePath)) { + console.log(`404: ${pathname} (${filePath})`); + return new Response("Not Found", { status: 404 }); + } - const content = readFileSync(filePath); - const mimeType = getMimeType(filePath); + const content = readFileSync(filePath); + const mimeType = getMimeType(filePath); - console.log(`200: ${pathname}`); - return new Response(content, { - headers: { - "Content-Type": mimeType, - "Cross-Origin-Opener-Policy": "same-origin", - "Cross-Origin-Embedder-Policy": "require-corp", - }, - }); - }, + console.log(`200: ${pathname}`); + return new Response(content, { + headers: { + "Content-Type": mimeType, + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + }, + }); + }, }); console.log(`E2E test server running at http://localhost:${PORT}`); diff --git a/packages/@postgres-language-server/wasm/package.json b/packages/@postgres-language-server/wasm/package.json index 86b87886a..bcae8f1b4 100644 --- a/packages/@postgres-language-server/wasm/package.json +++ b/packages/@postgres-language-server/wasm/package.json @@ -1,50 +1,60 @@ { - "name": "@postgres-language-server/wasm", - "version": "0.0.0", - "description": "WebAssembly bindings for the Postgres Language Server", - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./workspace": { - "types": "./dist/workspace.d.ts", - "import": "./dist/workspace.js" - }, - "./lsp": { - "types": "./dist/lsp.d.ts", - "import": "./dist/lsp.js" - } - }, - "scripts": { - "build": "bun run build:wasm && bun run build:ts", - "build:wasm": "../../../crates/pgls_wasm/build-wasm.sh --release && mkdir -p wasm && cp ../../../crates/pgls_wasm/dist/pgls.js ../../../crates/pgls_wasm/dist/pgls.wasm ./wasm/", - "build:ts": "tsc -p tsconfig.build.json && cp wasm/pgls.wasm wasm/pgls.js dist/", - "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist", - "test": "bun test", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:install": "playwright install chromium" - }, - "files": ["dist/", "wasm/", "README.md"], - "repository": { - "type": "git", - "url": "git+https://github.com/supabase-community/postgres-language-server.git", - "directory": "packages/@postgres-language-server/wasm" - }, - "author": "Supabase Community", - "bugs": "https://github.com/supabase-community/postgres-language-server/issues", - "keywords": ["postgres", "sql", "linter", "wasm", "language-server"], - "license": "MIT", - "publishConfig": { - "provenance": true - }, - "devDependencies": { - "@playwright/test": "^1.50.0", - "typescript": "^5.0.0" - } + "name": "@postgres-language-server/wasm", + "version": "0.0.0", + "description": "WebAssembly bindings for the Postgres Language Server", + "keywords": [ + "language-server", + "linter", + "postgres", + "sql", + "wasm" + ], + "bugs": "https://github.com/supabase-community/postgres-language-server/issues", + "license": "MIT", + "author": "Supabase Community", + "repository": { + "type": "git", + "url": "git+https://github.com/supabase-community/postgres-language-server.git", + "directory": "packages/@postgres-language-server/wasm" + }, + "files": [ + "dist/", + "wasm/", + "README.md" + ], + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./workspace": { + "types": "./dist/workspace.d.ts", + "import": "./dist/workspace.js" + }, + "./lsp": { + "types": "./dist/lsp.d.ts", + "import": "./dist/lsp.js" + } + }, + "publishConfig": { + "provenance": true + }, + "scripts": { + "build": "bun run build:wasm && bun run build:ts", + "build:wasm": "../../../crates/pgls_wasm/build-wasm.sh --release && mkdir -p wasm && cp ../../../crates/pgls_wasm/dist/pgls.js ../../../crates/pgls_wasm/dist/pgls.wasm ./wasm/", + "build:ts": "tsc -p tsconfig.build.json && cp wasm/pgls.wasm wasm/pgls.js dist/", + "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist", + "test": "bun test", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:install": "playwright install chromium" + }, + "devDependencies": { + "@playwright/test": "^1.50.0", + "typescript": "^5.0.0" + } } diff --git a/packages/@postgres-language-server/wasm/playwright.config.ts b/packages/@postgres-language-server/wasm/playwright.config.ts index aa6bac074..abd0e7f59 100644 --- a/packages/@postgres-language-server/wasm/playwright.config.ts +++ b/packages/@postgres-language-server/wasm/playwright.config.ts @@ -1,26 +1,26 @@ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ - testDir: "./e2e", - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: "html", - use: { - baseURL: "http://localhost:3000", - trace: "on-first-retry", - }, - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], - webServer: { - command: "bun run e2e/server.ts", - url: "http://localhost:3000", - reuseExistingServer: !process.env.CI, - timeout: 120000, - }, + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: "http://localhost:3000", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], + webServer: { + command: "bun run e2e/server.ts", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, }); diff --git a/packages/@postgres-language-server/wasm/src/common.ts b/packages/@postgres-language-server/wasm/src/common.ts index da9dedc6d..16c3c089a 100644 --- a/packages/@postgres-language-server/wasm/src/common.ts +++ b/packages/@postgres-language-server/wasm/src/common.ts @@ -11,11 +11,11 @@ let wasmModule: PGLSModule | null = null; * Detect if we're running in Node.js/Bun (vs browser) */ function isNode(): boolean { - return ( - typeof process !== "undefined" && - process.versions != null && - (process.versions.node != null || process.versions.bun != null) - ); + return ( + typeof process !== "undefined" && + process.versions != null && + (process.versions.node != null || process.versions.bun != null) + ); } /** @@ -24,87 +24,84 @@ function isNode(): boolean { * but can be called manually for preloading. */ export async function loadWasm(): Promise { - if (wasmModule) { - return wasmModule; - } + if (wasmModule) { + return wasmModule; + } - // Dynamic import of the Emscripten-generated module - // @ts-expect-error - Generated JS file without type declarations - const createPGLS = (await import("../wasm/pgls.js")).default as ( - options?: object, - ) => Promise; + // Dynamic import of the Emscripten-generated module + // @ts-expect-error - Generated JS file without type declarations + const createPGLS = (await import("../wasm/pgls.js")).default as ( + options?: object, + ) => Promise; - // Build options for Emscripten module initialization - const moduleOptions: Record = {}; + // Build options for Emscripten module initialization + const moduleOptions: Record = {}; - if (isNode()) { - // In Node.js/Bun, read the WASM file directly - const { readFileSync } = await import("node:fs"); - const { fileURLToPath } = await import("node:url"); - const { dirname, join } = await import("node:path"); + if (isNode()) { + // In Node.js/Bun, read the WASM file directly + const { readFileSync } = await import("node:fs"); + const { fileURLToPath } = await import("node:url"); + const { dirname, join } = await import("node:path"); - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const wasmPath = join(__dirname, "..", "wasm", "pgls.wasm"); + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const wasmPath = join(__dirname, "..", "wasm", "pgls.wasm"); - moduleOptions.wasmBinary = readFileSync(wasmPath); - } else { - // In browser, use locateFile to help find the .wasm file - moduleOptions.locateFile = (path: string) => { - if (path.endsWith(".wasm")) { - return new URL("./pgls.wasm", import.meta.url).href; - } - return path; - }; - } + moduleOptions.wasmBinary = readFileSync(wasmPath); + } else { + // In browser, use locateFile to help find the .wasm file + moduleOptions.locateFile = (path: string) => { + if (path.endsWith(".wasm")) { + return new URL("./pgls.wasm", import.meta.url).href; + } + return path; + }; + } - // Initialize the Emscripten module - const module = await createPGLS(moduleOptions); + // Initialize the Emscripten module + const module = await createPGLS(moduleOptions); - // Initialize the workspace - const result = module._pgls_init(); - if (result !== 0) { - throw new Error(`Failed to initialize PGLS: error code ${result}`); - } + // Initialize the workspace + const result = module._pgls_init(); + if (result !== 0) { + throw new Error(`Failed to initialize PGLS: error code ${result}`); + } - wasmModule = module; - return module; + wasmModule = module; + return module; } /** * Allocate a string in WASM memory. */ export function allocateString(module: PGLSModule, str: string): number { - const length = module.lengthBytesUTF8(str) + 1; - const ptr = module._malloc(length); - module.stringToUTF8(str, ptr, length); - return ptr; + const length = module.lengthBytesUTF8(str) + 1; + const ptr = module._malloc(length); + module.stringToUTF8(str, ptr, length); + return ptr; } /** * Read and free a string from WASM memory. */ -export function readAndFreeString( - module: PGLSModule, - ptr: number, -): string | null { - if (ptr === 0) { - return null; - } - const str = module.UTF8ToString(ptr); - module._pgls_free_string(ptr); - return str; +export function readAndFreeString(module: PGLSModule, ptr: number): string | null { + if (ptr === 0) { + return null; + } + const str = module.UTF8ToString(ptr); + module._pgls_free_string(ptr); + return str; } /** * Parse a result string that may be an error. */ export function parseResult(str: string | null): T { - if (str === null) { - return null as T; - } - if (str.startsWith("ERROR:")) { - throw new Error(str.substring(7).trim()); - } - return JSON.parse(str) as T; + if (str === null) { + return null as T; + } + if (str.startsWith("ERROR:")) { + throw new Error(str.substring(7).trim()); + } + return JSON.parse(str) as T; } diff --git a/packages/@postgres-language-server/wasm/src/index.ts b/packages/@postgres-language-server/wasm/src/index.ts index 1ffdb2f2a..e10937f70 100644 --- a/packages/@postgres-language-server/wasm/src/index.ts +++ b/packages/@postgres-language-server/wasm/src/index.ts @@ -23,12 +23,12 @@ export { loadWasm } from "./common.js"; // Re-export types export type { - Diagnostic, - CompletionItem, - SchemaCache, - WorkspaceOptions, - PGLSModule, - JsonRpcMessage, + Diagnostic, + CompletionItem, + SchemaCache, + WorkspaceOptions, + PGLSModule, + JsonRpcMessage, } from "./types.js"; // Default export for convenience diff --git a/packages/@postgres-language-server/wasm/src/lsp.ts b/packages/@postgres-language-server/wasm/src/lsp.ts index 7ab986054..fde1683f4 100644 --- a/packages/@postgres-language-server/wasm/src/lsp.ts +++ b/packages/@postgres-language-server/wasm/src/lsp.ts @@ -27,9 +27,8 @@ * ``` */ -import type { JsonRpcMessage, PGLSModule } from "./types.js"; - import { allocateString, loadWasm, readAndFreeString } from "./common.js"; +import type { JsonRpcMessage, PGLSModule } from "./types.js"; export type { PGLSModule, JsonRpcMessage }; @@ -37,41 +36,40 @@ export type { PGLSModule, JsonRpcMessage }; * The LanguageServer class provides a full LSP JSON-RPC message handler. */ export class LanguageServer { - private module: PGLSModule; + private module: PGLSModule; - constructor(module: PGLSModule) { - this.module = module; - } + constructor(module: PGLSModule) { + this.module = module; + } - /** - * Handle an LSP JSON-RPC message. - * - * Processes an incoming LSP message and returns an array of outgoing - * messages (response + any notifications like publishDiagnostics). - * - * @param message - The JSON-RPC message as a string or object - * @returns Array of outgoing JSON-RPC messages - */ - handleMessage(message: string | JsonRpcMessage): JsonRpcMessage[] { - const messageStr = - typeof message === "string" ? message : JSON.stringify(message); - const messagePtr = allocateString(this.module, messageStr); - try { - const resultPtr = this.module._pgls_lsp_handle_message(messagePtr); - const result = readAndFreeString(this.module, resultPtr) ?? "[]"; - return JSON.parse(result) as JsonRpcMessage[]; - } finally { - this.module._free(messagePtr); - } - } + /** + * Handle an LSP JSON-RPC message. + * + * Processes an incoming LSP message and returns an array of outgoing + * messages (response + any notifications like publishDiagnostics). + * + * @param message - The JSON-RPC message as a string or object + * @returns Array of outgoing JSON-RPC messages + */ + handleMessage(message: string | JsonRpcMessage): JsonRpcMessage[] { + const messageStr = typeof message === "string" ? message : JSON.stringify(message); + const messagePtr = allocateString(this.module, messageStr); + try { + const resultPtr = this.module._pgls_lsp_handle_message(messagePtr); + const result = readAndFreeString(this.module, resultPtr) ?? "[]"; + return JSON.parse(result) as JsonRpcMessage[]; + } finally { + this.module._free(messagePtr); + } + } } /** * Create a new LanguageServer instance. */ export async function createLanguageServer(): Promise { - const module = await loadWasm(); - return new LanguageServer(module); + const module = await loadWasm(); + return new LanguageServer(module); } export default createLanguageServer; diff --git a/packages/@postgres-language-server/wasm/src/types.ts b/packages/@postgres-language-server/wasm/src/types.ts index 763afb365..411041e8a 100644 --- a/packages/@postgres-language-server/wasm/src/types.ts +++ b/packages/@postgres-language-server/wasm/src/types.ts @@ -6,32 +6,32 @@ * A diagnostic message from the linter. */ export interface Diagnostic { - /** The category/rule name (e.g., "lint/safety/banDropColumn") */ - category: string; - /** Start byte offset in the file */ - start: number; - /** End byte offset in the file */ - end: number; - /** The diagnostic message */ - message: string; - /** Severity: "error", "warning", "info", "hint", or "fatal" */ - severity: "error" | "warning" | "info" | "hint" | "fatal"; + /** The category/rule name (e.g., "lint/safety/banDropColumn") */ + category: string; + /** Start byte offset in the file */ + start: number; + /** End byte offset in the file */ + end: number; + /** The diagnostic message */ + message: string; + /** Severity: "error", "warning", "info", "hint", or "fatal" */ + severity: "error" | "warning" | "info" | "hint" | "fatal"; } /** * A completion item suggestion. */ export interface CompletionItem { - /** The label of the completion item */ - label: string; - /** The kind of completion (e.g., "table", "column", "function") */ - kind: string; - /** Optional detail text */ - detail?: string; - /** Optional documentation */ - documentation?: string; - /** The text to insert */ - insertText?: string; + /** The label of the completion item */ + label: string; + /** The kind of completion (e.g., "table", "column", "function") */ + kind: string; + /** Optional detail text */ + detail?: string; + /** Optional documentation */ + documentation?: string; + /** The text to insert */ + insertText?: string; } /** @@ -39,50 +39,50 @@ export interface CompletionItem { * This structure mirrors the Rust SchemaCache type. */ export interface SchemaCache { - schemas: Schema[]; - tables: Table[]; - functions: Function[]; - types: Type[]; - // Add more as needed based on pgls_schema_cache + schemas: Schema[]; + tables: Table[]; + functions: Function[]; + types: Type[]; + // Add more as needed based on pgls_schema_cache } export interface Schema { - name: string; - owner?: string; + name: string; + owner?: string; } export interface Table { - name: string; - schema: string; - columns: Column[]; + name: string; + schema: string; + columns: Column[]; } export interface Column { - name: string; - dataType: string; - isNullable: boolean; - defaultValue?: string; + name: string; + dataType: string; + isNullable: boolean; + defaultValue?: string; } export interface Function { - name: string; - schema: string; - returnType: string; - arguments: string[]; + name: string; + schema: string; + returnType: string; + arguments: string[]; } export interface Type { - name: string; - schema: string; - kind: string; + name: string; + schema: string; + kind: string; } /** * Options for initializing the workspace. */ export interface WorkspaceOptions { - /** Optional schema cache to preload */ - schema?: SchemaCache | string; + /** Optional schema cache to preload */ + schema?: SchemaCache | string; } /** @@ -90,16 +90,16 @@ export interface WorkspaceOptions { * This is the standard LSP message format. */ export interface JsonRpcMessage { - jsonrpc: "2.0"; - id?: number | string | null; - method?: string; - params?: unknown; - result?: unknown; - error?: { - code: number; - message: string; - data?: unknown; - }; + jsonrpc: "2.0"; + id?: number | string | null; + method?: string; + params?: unknown; + result?: unknown; + error?: { + code: number; + message: string; + data?: unknown; + }; } /** @@ -107,28 +107,28 @@ export interface JsonRpcMessage { * This is the raw interface exposed by the compiled WASM. */ export interface PGLSModule { - // Memory management - _malloc(size: number): number; - _free(ptr: number): void; + // Memory management + _malloc(size: number): number; + _free(ptr: number): void; - // FFI functions - _pgls_init(): number; - _pgls_free_string(ptr: number): void; - _pgls_set_schema(jsonPtr: number): number; - _pgls_clear_schema(): void; - _pgls_insert_file(pathPtr: number, contentPtr: number): number; - _pgls_remove_file(pathPtr: number): void; - _pgls_lint(pathPtr: number): number; - _pgls_complete(pathPtr: number, offset: number): number; - _pgls_hover(pathPtr: number, offset: number): number; - _pgls_parse(sqlPtr: number): number; - _pgls_version(): number; + // FFI functions + _pgls_init(): number; + _pgls_free_string(ptr: number): void; + _pgls_set_schema(jsonPtr: number): number; + _pgls_clear_schema(): void; + _pgls_insert_file(pathPtr: number, contentPtr: number): number; + _pgls_remove_file(pathPtr: number): void; + _pgls_lint(pathPtr: number): number; + _pgls_complete(pathPtr: number, offset: number): number; + _pgls_hover(pathPtr: number, offset: number): number; + _pgls_parse(sqlPtr: number): number; + _pgls_version(): number; - // Language Server API - _pgls_lsp_handle_message(messagePtr: number): number; + // Language Server API + _pgls_lsp_handle_message(messagePtr: number): number; - // Emscripten runtime methods - UTF8ToString(ptr: number): string; - stringToUTF8(str: string, ptr: number, maxLength: number): void; - lengthBytesUTF8(str: string): number; + // Emscripten runtime methods + UTF8ToString(ptr: number): string; + stringToUTF8(str: string, ptr: number, maxLength: number): void; + lengthBytesUTF8(str: string): number; } diff --git a/packages/@postgres-language-server/wasm/src/worker.ts b/packages/@postgres-language-server/wasm/src/worker.ts index e10996092..6fbc6a444 100644 --- a/packages/@postgres-language-server/wasm/src/worker.ts +++ b/packages/@postgres-language-server/wasm/src/worker.ts @@ -36,11 +36,7 @@ * ``` */ -import { - type JsonRpcMessage, - type LanguageServer, - createLanguageServer, -} from "./lsp.js"; +import { type JsonRpcMessage, type LanguageServer, createLanguageServer } from "./lsp.js"; let languageServer: LanguageServer | null = null; @@ -48,40 +44,40 @@ let languageServer: LanguageServer | null = null; * Initialize the language server. */ async function initialize(): Promise { - if (!languageServer) { - languageServer = await createLanguageServer(); - } + if (!languageServer) { + languageServer = await createLanguageServer(); + } } /** * Handle incoming messages from the main thread. */ self.onmessage = async (event: MessageEvent) => { - // Ensure language server is initialized - if (!languageServer) { - await initialize(); - } + // Ensure language server is initialized + if (!languageServer) { + await initialize(); + } - const data = event.data; + const data = event.data; - // Handle LSP JSON-RPC messages - // The message can be a string (raw JSON) or an object - const message: string | JsonRpcMessage = data; + // Handle LSP JSON-RPC messages + // The message can be a string (raw JSON) or an object + const message: string | JsonRpcMessage = data; - // Process the message and get array of outgoing messages - const outgoing = languageServer?.handleMessage(message); + // Process the message and get array of outgoing messages + const outgoing = languageServer?.handleMessage(message); - // Send EACH message separately via postMessage - // This is required by BrowserMessageReader which expects - // individual messages, not arrays - if (outgoing) { - for (const msg of outgoing) { - self.postMessage(msg); - } - } + // Send EACH message separately via postMessage + // This is required by BrowserMessageReader which expects + // individual messages, not arrays + if (outgoing) { + for (const msg of outgoing) { + self.postMessage(msg); + } + } }; // Initialize immediately and signal readiness initialize().then(() => { - self.postMessage({ type: "ready" }); + self.postMessage({ type: "ready" }); }); diff --git a/packages/@postgres-language-server/wasm/src/workspace.ts b/packages/@postgres-language-server/wasm/src/workspace.ts index 4b6e0dd9e..d185d5c9c 100644 --- a/packages/@postgres-language-server/wasm/src/workspace.ts +++ b/packages/@postgres-language-server/wasm/src/workspace.ts @@ -16,179 +16,165 @@ * ``` */ +import { allocateString, loadWasm, parseResult, readAndFreeString } from "./common.js"; import type { - CompletionItem, - Diagnostic, - PGLSModule, - SchemaCache, - WorkspaceOptions, + CompletionItem, + Diagnostic, + PGLSModule, + SchemaCache, + WorkspaceOptions, } from "./types.js"; -import { - allocateString, - loadWasm, - parseResult, - readAndFreeString, -} from "./common.js"; - -export type { - Diagnostic, - CompletionItem, - SchemaCache, - WorkspaceOptions, - PGLSModule, -}; +export type { Diagnostic, CompletionItem, SchemaCache, WorkspaceOptions, PGLSModule }; /** * The Workspace class provides a direct API for SQL parsing, linting, * completions, and hover information. */ export class Workspace { - private module: PGLSModule; - - constructor(module: PGLSModule) { - this.module = module; - } - - /** - * Set the database schema from a SchemaCache object or JSON string. - */ - setSchema(schema: SchemaCache | string): void { - const json = typeof schema === "string" ? schema : JSON.stringify(schema); - const jsonPtr = allocateString(this.module, json); - try { - const resultPtr = this.module._pgls_set_schema(jsonPtr); - const result = readAndFreeString(this.module, resultPtr); - if (result !== null) { - throw new Error(result); - } - } finally { - this.module._free(jsonPtr); - } - } - - /** - * Clear the current schema. - */ - clearSchema(): void { - this.module._pgls_clear_schema(); - } - - /** - * Insert or update a file in the workspace. - */ - insertFile(path: string, content: string): void { - const pathPtr = allocateString(this.module, path); - const contentPtr = allocateString(this.module, content); - try { - const resultPtr = this.module._pgls_insert_file(pathPtr, contentPtr); - const result = readAndFreeString(this.module, resultPtr); - if (result !== null) { - throw new Error(result); - } - } finally { - this.module._free(pathPtr); - this.module._free(contentPtr); - } - } - - /** - * Remove a file from the workspace. - */ - removeFile(path: string): void { - const pathPtr = allocateString(this.module, path); - try { - this.module._pgls_remove_file(pathPtr); - } finally { - this.module._free(pathPtr); - } - } - - /** - * Lint a file and return diagnostics. - */ - lint(path: string): Diagnostic[] { - const pathPtr = allocateString(this.module, path); - try { - const resultPtr = this.module._pgls_lint(pathPtr); - const result = readAndFreeString(this.module, resultPtr); - return parseResult(result) ?? []; - } finally { - this.module._free(pathPtr); - } - } - - /** - * Get completions at a position in a file. - */ - complete(path: string, offset: number): CompletionItem[] { - const pathPtr = allocateString(this.module, path); - try { - const resultPtr = this.module._pgls_complete(pathPtr, offset); - const result = readAndFreeString(this.module, resultPtr); - return parseResult(result) ?? []; - } finally { - this.module._free(pathPtr); - } - } - - /** - * Get hover information at a position in a file. - */ - hover(path: string, offset: number): string | null { - const pathPtr = allocateString(this.module, path); - try { - const resultPtr = this.module._pgls_hover(pathPtr, offset); - const result = readAndFreeString(this.module, resultPtr); - if (result === null) { - return null; - } - if (result.startsWith("ERROR:")) { - throw new Error(result.substring(7).trim()); - } - return result; - } finally { - this.module._free(pathPtr); - } - } - - /** - * Parse SQL and return any parse errors. - */ - parse(sql: string): string[] { - const sqlPtr = allocateString(this.module, sql); - try { - const resultPtr = this.module._pgls_parse(sqlPtr); - const result = readAndFreeString(this.module, resultPtr); - return parseResult(result) ?? []; - } finally { - this.module._free(sqlPtr); - } - } - - /** - * Get the version of the library. - */ - version(): string { - const resultPtr = this.module._pgls_version(); - return readAndFreeString(this.module, resultPtr) ?? "unknown"; - } + private module: PGLSModule; + + constructor(module: PGLSModule) { + this.module = module; + } + + /** + * Set the database schema from a SchemaCache object or JSON string. + */ + setSchema(schema: SchemaCache | string): void { + const json = typeof schema === "string" ? schema : JSON.stringify(schema); + const jsonPtr = allocateString(this.module, json); + try { + const resultPtr = this.module._pgls_set_schema(jsonPtr); + const result = readAndFreeString(this.module, resultPtr); + if (result !== null) { + throw new Error(result); + } + } finally { + this.module._free(jsonPtr); + } + } + + /** + * Clear the current schema. + */ + clearSchema(): void { + this.module._pgls_clear_schema(); + } + + /** + * Insert or update a file in the workspace. + */ + insertFile(path: string, content: string): void { + const pathPtr = allocateString(this.module, path); + const contentPtr = allocateString(this.module, content); + try { + const resultPtr = this.module._pgls_insert_file(pathPtr, contentPtr); + const result = readAndFreeString(this.module, resultPtr); + if (result !== null) { + throw new Error(result); + } + } finally { + this.module._free(pathPtr); + this.module._free(contentPtr); + } + } + + /** + * Remove a file from the workspace. + */ + removeFile(path: string): void { + const pathPtr = allocateString(this.module, path); + try { + this.module._pgls_remove_file(pathPtr); + } finally { + this.module._free(pathPtr); + } + } + + /** + * Lint a file and return diagnostics. + */ + lint(path: string): Diagnostic[] { + const pathPtr = allocateString(this.module, path); + try { + const resultPtr = this.module._pgls_lint(pathPtr); + const result = readAndFreeString(this.module, resultPtr); + return parseResult(result) ?? []; + } finally { + this.module._free(pathPtr); + } + } + + /** + * Get completions at a position in a file. + */ + complete(path: string, offset: number): CompletionItem[] { + const pathPtr = allocateString(this.module, path); + try { + const resultPtr = this.module._pgls_complete(pathPtr, offset); + const result = readAndFreeString(this.module, resultPtr); + return parseResult(result) ?? []; + } finally { + this.module._free(pathPtr); + } + } + + /** + * Get hover information at a position in a file. + */ + hover(path: string, offset: number): string | null { + const pathPtr = allocateString(this.module, path); + try { + const resultPtr = this.module._pgls_hover(pathPtr, offset); + const result = readAndFreeString(this.module, resultPtr); + if (result === null) { + return null; + } + if (result.startsWith("ERROR:")) { + throw new Error(result.substring(7).trim()); + } + return result; + } finally { + this.module._free(pathPtr); + } + } + + /** + * Parse SQL and return any parse errors. + */ + parse(sql: string): string[] { + const sqlPtr = allocateString(this.module, sql); + try { + const resultPtr = this.module._pgls_parse(sqlPtr); + const result = readAndFreeString(this.module, resultPtr); + return parseResult(result) ?? []; + } finally { + this.module._free(sqlPtr); + } + } + + /** + * Get the version of the library. + */ + version(): string { + const resultPtr = this.module._pgls_version(); + return readAndFreeString(this.module, resultPtr) ?? "unknown"; + } } /** * Create a new Workspace instance. */ -export async function createWorkspace( - options?: WorkspaceOptions, -): Promise { - const module = await loadWasm(); - const workspace = new Workspace(module); +export async function createWorkspace(options?: WorkspaceOptions): Promise { + const module = await loadWasm(); + const workspace = new Workspace(module); - if (options?.schema) { - workspace.setSchema(options.schema); - } + if (options?.schema) { + workspace.setSchema(options.schema); + } - return workspace; + return workspace; } export default createWorkspace; diff --git a/packages/@postgres-language-server/wasm/tests/index.test.ts b/packages/@postgres-language-server/wasm/tests/index.test.ts index 9e6735fa8..e01855a0c 100644 --- a/packages/@postgres-language-server/wasm/tests/index.test.ts +++ b/packages/@postgres-language-server/wasm/tests/index.test.ts @@ -3,6 +3,7 @@ */ import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; + import { type LanguageServer, createLanguageServer } from "../src/lsp"; import { type Workspace, createWorkspace } from "../src/workspace"; @@ -11,96 +12,96 @@ import { type Workspace, createWorkspace } from "../src/workspace"; // ============================================================================= describe("Workspace API", () => { - let workspace: Workspace; - - beforeAll(async () => { - workspace = await createWorkspace(); - }); - - test("version returns a string", () => { - const version = workspace.version(); - expect(typeof version).toBe("string"); - expect(version).toBe("0.0.0"); - }); - - test("parse valid SQL returns empty array", () => { - const errors = workspace.parse("SELECT 1;"); - expect(errors).toBeArray(); - expect(errors.length).toBe(0); - }); - - test("parse invalid SQL returns errors", () => { - const errors = workspace.parse("SELEC 1;"); - expect(errors).toBeArray(); - expect(errors.length).toBeGreaterThan(0); - }); - - test("parse multiple statements", () => { - const errors = workspace.parse("SELECT 1; SELECT 2;"); - expect(errors).toBeArray(); - expect(errors.length).toBe(0); - }); - - test("insertFile and lint", () => { - workspace.insertFile("/test.sql", "SELECT * FROM users;"); - const diagnostics = workspace.lint("/test.sql"); - expect(diagnostics).toBeArray(); - // Valid SQL should have no parse errors - expect(diagnostics.length).toBe(0); - }); - - test("insertFile with invalid SQL and lint", () => { - workspace.insertFile("/invalid.sql", "SELEC * FROM;"); - const diagnostics = workspace.lint("/invalid.sql"); - expect(diagnostics).toBeArray(); - // Invalid SQL should have at least one error - expect(diagnostics.length).toBeGreaterThan(0); - }); - - test("removeFile", () => { - workspace.insertFile("/to-remove.sql", "SELECT 1;"); - // Should not throw - workspace.removeFile("/to-remove.sql"); - // Linting a removed file should throw or return error - expect(() => workspace.lint("/to-remove.sql")).toThrow(); - }); - - test("complete returns completion items", () => { - workspace.insertFile("/complete.sql", "SELECT "); - const completions = workspace.complete("/complete.sql", 7); - expect(completions).toBeArray(); - // Without schema, may return empty or basic completions - }); - - test("hover returns null without schema", () => { - // Explicitly clear schema to ensure we're testing without schema - workspace.clearSchema(); - workspace.insertFile("/hover.sql", "SELECT * FROM users;"); - const hover = workspace.hover("/hover.sql", 14); // Over "users" - // Without schema loaded, hover should return null - expect(hover).toBeNull(); - }); - - test("clearSchema does not throw", () => { - // clearSchema should work even without a schema set - expect(() => workspace.clearSchema()).not.toThrow(); - }); - - test("setSchema with invalid JSON throws", () => { - expect(() => workspace.setSchema("not valid json")).toThrow(); - }); - - test("parse empty string", () => { - const errors = workspace.parse(""); - expect(errors).toBeArray(); - expect(errors.length).toBe(0); - }); - - test("parse with comments", () => { - const errors = workspace.parse("-- This is a comment\nSELECT 1;"); - expect(errors).toBeArray(); - expect(errors.length).toBe(0); - }); + let workspace: Workspace; + + beforeAll(async () => { + workspace = await createWorkspace(); + }); + + test("version returns a string", () => { + const version = workspace.version(); + expect(typeof version).toBe("string"); + expect(version).toBe("0.0.0"); + }); + + test("parse valid SQL returns empty array", () => { + const errors = workspace.parse("SELECT 1;"); + expect(errors).toBeArray(); + expect(errors.length).toBe(0); + }); + + test("parse invalid SQL returns errors", () => { + const errors = workspace.parse("SELEC 1;"); + expect(errors).toBeArray(); + expect(errors.length).toBeGreaterThan(0); + }); + + test("parse multiple statements", () => { + const errors = workspace.parse("SELECT 1; SELECT 2;"); + expect(errors).toBeArray(); + expect(errors.length).toBe(0); + }); + + test("insertFile and lint", () => { + workspace.insertFile("/test.sql", "SELECT * FROM users;"); + const diagnostics = workspace.lint("/test.sql"); + expect(diagnostics).toBeArray(); + // Valid SQL should have no parse errors + expect(diagnostics.length).toBe(0); + }); + + test("insertFile with invalid SQL and lint", () => { + workspace.insertFile("/invalid.sql", "SELEC * FROM;"); + const diagnostics = workspace.lint("/invalid.sql"); + expect(diagnostics).toBeArray(); + // Invalid SQL should have at least one error + expect(diagnostics.length).toBeGreaterThan(0); + }); + + test("removeFile", () => { + workspace.insertFile("/to-remove.sql", "SELECT 1;"); + // Should not throw + workspace.removeFile("/to-remove.sql"); + // Linting a removed file should throw or return error + expect(() => workspace.lint("/to-remove.sql")).toThrow(); + }); + + test("complete returns completion items", () => { + workspace.insertFile("/complete.sql", "SELECT "); + const completions = workspace.complete("/complete.sql", 7); + expect(completions).toBeArray(); + // Without schema, may return empty or basic completions + }); + + test("hover returns null without schema", () => { + // Explicitly clear schema to ensure we're testing without schema + workspace.clearSchema(); + workspace.insertFile("/hover.sql", "SELECT * FROM users;"); + const hover = workspace.hover("/hover.sql", 14); // Over "users" + // Without schema loaded, hover should return null + expect(hover).toBeNull(); + }); + + test("clearSchema does not throw", () => { + // clearSchema should work even without a schema set + expect(() => workspace.clearSchema()).not.toThrow(); + }); + + test("setSchema with invalid JSON throws", () => { + expect(() => workspace.setSchema("not valid json")).toThrow(); + }); + + test("parse empty string", () => { + const errors = workspace.parse(""); + expect(errors).toBeArray(); + expect(errors.length).toBe(0); + }); + + test("parse with comments", () => { + const errors = workspace.parse("-- This is a comment\nSELECT 1;"); + expect(errors).toBeArray(); + expect(errors.length).toBe(0); + }); }); // ============================================================================= @@ -108,135 +109,131 @@ describe("Workspace API", () => { // ============================================================================= describe("LanguageServer API", () => { - let lsp: LanguageServer; - - beforeAll(async () => { - lsp = await createLanguageServer(); - }); - - test("handleMessage returns array", () => { - const messages = lsp.handleMessage({ - jsonrpc: "2.0", - id: 1, - method: "initialize", - params: {}, - }); - expect(messages).toBeArray(); - expect(messages.length).toBeGreaterThan(0); - }); - - test("initialize returns capabilities", () => { - const messages = lsp.handleMessage({ - jsonrpc: "2.0", - id: 1, - method: "initialize", - params: {}, - }); - - expect(messages.length).toBe(1); - const response = messages[0]; - expect(response.jsonrpc).toBe("2.0"); - expect(response.id).toBe(1); - expect(response.result).toBeDefined(); - // @ts-expect-error - result is unknown type - expect(response.result.capabilities).toBeDefined(); - }); - - test("shutdown returns null", () => { - const messages = lsp.handleMessage({ - jsonrpc: "2.0", - id: 2, - method: "shutdown", - params: null, - }); - - expect(messages.length).toBe(1); - const response = messages[0]; - expect(response.id).toBe(2); - expect(response.result).toBeNull(); - }); - - test("handleMessage accepts string input", () => { - const messages = lsp.handleMessage( - JSON.stringify({ - jsonrpc: "2.0", - id: 3, - method: "shutdown", - params: null, - }), - ); - - expect(messages.length).toBe(1); - expect(messages[0].id).toBe(3); - }); - - test("didOpen returns publishDiagnostics notification", () => { - const messages = lsp.handleMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///test-lsp.sql", - languageId: "sql", - version: 1, - text: "SELECT * FROM users;", - }, - }, - }); - - // Should return at least one publishDiagnostics notification - expect(messages.length).toBeGreaterThanOrEqual(1); - const notification = messages.find( - (m) => m.method === "textDocument/publishDiagnostics", - ); - expect(notification).toBeDefined(); - expect(notification?.params).toBeDefined(); - }); - - test("didOpen with invalid SQL returns diagnostics", () => { - const messages = lsp.handleMessage({ - jsonrpc: "2.0", - method: "textDocument/didOpen", - params: { - textDocument: { - uri: "file:///invalid-lsp.sql", - languageId: "sql", - version: 1, - text: "SELEC * FROM;", - }, - }, - }); - - const notification = messages.find( - (m) => m.method === "textDocument/publishDiagnostics", - ); - expect(notification).toBeDefined(); - // @ts-expect-error - params is unknown type - expect(notification?.params?.diagnostics?.length).toBeGreaterThan(0); - }); - - test("unknown method returns error", () => { - const messages = lsp.handleMessage({ - jsonrpc: "2.0", - id: 99, - method: "unknownMethod", - params: {}, - }); - - expect(messages.length).toBe(1); - const response = messages[0]; - expect(response.error).toBeDefined(); - expect(response.error?.code).toBe(-32601); // Method not found - }); - - test("invalid JSON returns parse error", () => { - const messages = lsp.handleMessage("not valid json"); - - expect(messages.length).toBe(1); - const response = messages[0]; - expect(response.error).toBeDefined(); - expect(response.error?.code).toBe(-32700); // Parse error - }); + let lsp: LanguageServer; + + beforeAll(async () => { + lsp = await createLanguageServer(); + }); + + test("handleMessage returns array", () => { + const messages = lsp.handleMessage({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: {}, + }); + expect(messages).toBeArray(); + expect(messages.length).toBeGreaterThan(0); + }); + + test("initialize returns capabilities", () => { + const messages = lsp.handleMessage({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: {}, + }); + + expect(messages.length).toBe(1); + const response = messages[0]; + expect(response.jsonrpc).toBe("2.0"); + expect(response.id).toBe(1); + expect(response.result).toBeDefined(); + // @ts-expect-error - result is unknown type + expect(response.result.capabilities).toBeDefined(); + }); + + test("shutdown returns null", () => { + const messages = lsp.handleMessage({ + jsonrpc: "2.0", + id: 2, + method: "shutdown", + params: null, + }); + + expect(messages.length).toBe(1); + const response = messages[0]; + expect(response.id).toBe(2); + expect(response.result).toBeNull(); + }); + + test("handleMessage accepts string input", () => { + const messages = lsp.handleMessage( + JSON.stringify({ + jsonrpc: "2.0", + id: 3, + method: "shutdown", + params: null, + }), + ); + + expect(messages.length).toBe(1); + expect(messages[0].id).toBe(3); + }); + + test("didOpen returns publishDiagnostics notification", () => { + const messages = lsp.handleMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///test-lsp.sql", + languageId: "sql", + version: 1, + text: "SELECT * FROM users;", + }, + }, + }); + + // Should return at least one publishDiagnostics notification + expect(messages.length).toBeGreaterThanOrEqual(1); + const notification = messages.find((m) => m.method === "textDocument/publishDiagnostics"); + expect(notification).toBeDefined(); + expect(notification?.params).toBeDefined(); + }); + + test("didOpen with invalid SQL returns diagnostics", () => { + const messages = lsp.handleMessage({ + jsonrpc: "2.0", + method: "textDocument/didOpen", + params: { + textDocument: { + uri: "file:///invalid-lsp.sql", + languageId: "sql", + version: 1, + text: "SELEC * FROM;", + }, + }, + }); + + const notification = messages.find((m) => m.method === "textDocument/publishDiagnostics"); + expect(notification).toBeDefined(); + // @ts-expect-error - params is unknown type + expect(notification?.params?.diagnostics?.length).toBeGreaterThan(0); + }); + + test("unknown method returns error", () => { + const messages = lsp.handleMessage({ + jsonrpc: "2.0", + id: 99, + method: "unknownMethod", + params: {}, + }); + + expect(messages.length).toBe(1); + const response = messages[0]; + expect(response.error).toBeDefined(); + expect(response.error?.code).toBe(-32601); // Method not found + }); + + test("invalid JSON returns parse error", () => { + const messages = lsp.handleMessage("not valid json"); + + expect(messages.length).toBe(1); + const response = messages[0]; + expect(response.error).toBeDefined(); + expect(response.error?.code).toBe(-32700); // Parse error + }); }); // ============================================================================= @@ -248,234 +245,234 @@ describe("LanguageServer API", () => { * This matches the Rust SchemaCache struct format. */ const TEST_SCHEMA = { - schemas: [ - { - id: 1, - name: "public", - owner: "postgres", - allowed_users: [], - allowed_creators: [], - table_count: 1, - view_count: 0, - function_count: 0, - total_size: "0 bytes", - comment: null, - }, - ], - tables: [ - { - id: 1, - schema: "public", - name: "users", - rls_enabled: false, - rls_forced: false, - replica_identity: "Default", - table_kind: "Ordinary", - bytes: 0, - size: "0 bytes", - live_rows_estimate: 0, - dead_rows_estimate: 0, - comment: "User accounts table", - }, - { - id: 2, - schema: "public", - name: "posts", - rls_enabled: false, - rls_forced: false, - replica_identity: "Default", - table_kind: "Ordinary", - bytes: 0, - size: "0 bytes", - live_rows_estimate: 0, - dead_rows_estimate: 0, - comment: null, - }, - ], - columns: [ - { - name: "id", - table_name: "users", - table_oid: 1, - class_kind: "OrdinaryTable", - number: 1, - schema_name: "public", - type_id: 23, - type_name: "integer", - is_nullable: false, - is_primary_key: true, - is_unique: true, - default_expr: "nextval('users_id_seq'::regclass)", - varchar_length: null, - comment: null, - }, - { - name: "email", - table_name: "users", - table_oid: 1, - class_kind: "OrdinaryTable", - number: 2, - schema_name: "public", - type_id: 25, - type_name: "text", - is_nullable: false, - is_primary_key: false, - is_unique: true, - default_expr: null, - varchar_length: null, - comment: "User email address", - }, - { - name: "name", - table_name: "users", - table_oid: 1, - class_kind: "OrdinaryTable", - number: 3, - schema_name: "public", - type_id: 1043, - type_name: "character varying", - is_nullable: true, - is_primary_key: false, - is_unique: false, - default_expr: null, - varchar_length: 255, - comment: null, - }, - { - name: "id", - table_name: "posts", - table_oid: 2, - class_kind: "OrdinaryTable", - number: 1, - schema_name: "public", - type_id: 23, - type_name: "integer", - is_nullable: false, - is_primary_key: true, - is_unique: true, - default_expr: null, - varchar_length: null, - comment: null, - }, - { - name: "user_id", - table_name: "posts", - table_oid: 2, - class_kind: "OrdinaryTable", - number: 2, - schema_name: "public", - type_id: 23, - type_name: "integer", - is_nullable: false, - is_primary_key: false, - is_unique: false, - default_expr: null, - varchar_length: null, - comment: null, - }, - { - name: "title", - table_name: "posts", - table_oid: 2, - class_kind: "OrdinaryTable", - number: 3, - schema_name: "public", - type_id: 25, - type_name: "text", - is_nullable: false, - is_primary_key: false, - is_unique: false, - default_expr: null, - varchar_length: null, - comment: null, - }, - ], - functions: [], - types: [], - version: { - version: "16.0", - version_num: 160000, - major_version: 16, - active_connections: 1, - max_connections: 100, - }, - policies: [], - extensions: [], - triggers: [], - roles: [], + schemas: [ + { + id: 1, + name: "public", + owner: "postgres", + allowed_users: [], + allowed_creators: [], + table_count: 1, + view_count: 0, + function_count: 0, + total_size: "0 bytes", + comment: null, + }, + ], + tables: [ + { + id: 1, + schema: "public", + name: "users", + rls_enabled: false, + rls_forced: false, + replica_identity: "Default", + table_kind: "Ordinary", + bytes: 0, + size: "0 bytes", + live_rows_estimate: 0, + dead_rows_estimate: 0, + comment: "User accounts table", + }, + { + id: 2, + schema: "public", + name: "posts", + rls_enabled: false, + rls_forced: false, + replica_identity: "Default", + table_kind: "Ordinary", + bytes: 0, + size: "0 bytes", + live_rows_estimate: 0, + dead_rows_estimate: 0, + comment: null, + }, + ], + columns: [ + { + name: "id", + table_name: "users", + table_oid: 1, + class_kind: "OrdinaryTable", + number: 1, + schema_name: "public", + type_id: 23, + type_name: "integer", + is_nullable: false, + is_primary_key: true, + is_unique: true, + default_expr: "nextval('users_id_seq'::regclass)", + varchar_length: null, + comment: null, + }, + { + name: "email", + table_name: "users", + table_oid: 1, + class_kind: "OrdinaryTable", + number: 2, + schema_name: "public", + type_id: 25, + type_name: "text", + is_nullable: false, + is_primary_key: false, + is_unique: true, + default_expr: null, + varchar_length: null, + comment: "User email address", + }, + { + name: "name", + table_name: "users", + table_oid: 1, + class_kind: "OrdinaryTable", + number: 3, + schema_name: "public", + type_id: 1043, + type_name: "character varying", + is_nullable: true, + is_primary_key: false, + is_unique: false, + default_expr: null, + varchar_length: 255, + comment: null, + }, + { + name: "id", + table_name: "posts", + table_oid: 2, + class_kind: "OrdinaryTable", + number: 1, + schema_name: "public", + type_id: 23, + type_name: "integer", + is_nullable: false, + is_primary_key: true, + is_unique: true, + default_expr: null, + varchar_length: null, + comment: null, + }, + { + name: "user_id", + table_name: "posts", + table_oid: 2, + class_kind: "OrdinaryTable", + number: 2, + schema_name: "public", + type_id: 23, + type_name: "integer", + is_nullable: false, + is_primary_key: false, + is_unique: false, + default_expr: null, + varchar_length: null, + comment: null, + }, + { + name: "title", + table_name: "posts", + table_oid: 2, + class_kind: "OrdinaryTable", + number: 3, + schema_name: "public", + type_id: 25, + type_name: "text", + is_nullable: false, + is_primary_key: false, + is_unique: false, + default_expr: null, + varchar_length: null, + comment: null, + }, + ], + functions: [], + types: [], + version: { + version: "16.0", + version_num: 160000, + major_version: 16, + active_connections: 1, + max_connections: 100, + }, + policies: [], + extensions: [], + triggers: [], + roles: [], }; describe("Schema-based Workspace completions and hover", () => { - let workspace: Workspace; - - beforeAll(async () => { - workspace = await createWorkspace(); - }); - - // Ensure schema is loaded before each test - beforeEach(() => { - workspace.setSchema(JSON.stringify(TEST_SCHEMA)); - }); - - test("setSchema works with valid schema", () => { - // Schema was set in beforeEach - expect(true).toBe(true); - }); - - test("complete returns table names in FROM clause", () => { - workspace.insertFile("/from-complete.sql", "SELECT * FROM "); - const completions = workspace.complete("/from-complete.sql", 14); - expect(completions).toBeArray(); - // Should contain table names from schema - const tableNames = completions.map((c: any) => c.label); - expect(tableNames).toContain("users"); - expect(tableNames).toContain("posts"); - }); - - test("complete returns column names after table reference", () => { - workspace.insertFile("/col-complete.sql", "SELECT FROM users"); - // Position cursor after SELECT (position 7) - const completions = workspace.complete("/col-complete.sql", 7); - expect(completions).toBeArray(); - // Should contain column names from users table - const columnNames = completions.map((c: any) => c.label); - expect(columnNames).toContain("id"); - expect(columnNames).toContain("email"); - expect(columnNames).toContain("name"); - }); - - test("hover on table name shows table info", () => { - workspace.insertFile("/hover-table.sql", "SELECT * FROM users;"); - // Position over "users" (around character 14) - const hover = workspace.hover("/hover-table.sql", 14); - // With schema loaded, hover should return info (a markdown string) - expect(hover).not.toBeNull(); - expect(typeof hover).toBe("string"); - // The hover text should mention the table - expect(hover?.toLowerCase()).toContain("users"); - }); - - test("hover on column name shows column type", () => { - workspace.insertFile("/hover-col.sql", "SELECT email FROM users;"); - // Position over "email" (around character 7) - const hover = workspace.hover("/hover-col.sql", 8); - // With schema loaded, hover should return type info (a markdown string) - expect(hover).not.toBeNull(); - expect(typeof hover).toBe("string"); - // The hover text should mention the type - expect(hover?.toLowerCase()).toContain("text"); - }); - - test("clearSchema removes schema and hover returns null", () => { - // First verify hover works with schema (set by beforeEach) - workspace.insertFile("/with-schema.sql", "SELECT * FROM users;"); - const hoverWithSchema = workspace.hover("/with-schema.sql", 14); - expect(hoverWithSchema).not.toBeNull(); - - // Now clear schema and verify hover returns null - workspace.clearSchema(); - workspace.insertFile("/no-schema.sql", "SELECT * FROM users;"); - const hoverWithoutSchema = workspace.hover("/no-schema.sql", 14); - expect(hoverWithoutSchema).toBeNull(); - }); + let workspace: Workspace; + + beforeAll(async () => { + workspace = await createWorkspace(); + }); + + // Ensure schema is loaded before each test + beforeEach(() => { + workspace.setSchema(JSON.stringify(TEST_SCHEMA)); + }); + + test("setSchema works with valid schema", () => { + // Schema was set in beforeEach + expect(true).toBe(true); + }); + + test("complete returns table names in FROM clause", () => { + workspace.insertFile("/from-complete.sql", "SELECT * FROM "); + const completions = workspace.complete("/from-complete.sql", 14); + expect(completions).toBeArray(); + // Should contain table names from schema + const tableNames = completions.map((c: any) => c.label); + expect(tableNames).toContain("users"); + expect(tableNames).toContain("posts"); + }); + + test("complete returns column names after table reference", () => { + workspace.insertFile("/col-complete.sql", "SELECT FROM users"); + // Position cursor after SELECT (position 7) + const completions = workspace.complete("/col-complete.sql", 7); + expect(completions).toBeArray(); + // Should contain column names from users table + const columnNames = completions.map((c: any) => c.label); + expect(columnNames).toContain("id"); + expect(columnNames).toContain("email"); + expect(columnNames).toContain("name"); + }); + + test("hover on table name shows table info", () => { + workspace.insertFile("/hover-table.sql", "SELECT * FROM users;"); + // Position over "users" (around character 14) + const hover = workspace.hover("/hover-table.sql", 14); + // With schema loaded, hover should return info (a markdown string) + expect(hover).not.toBeNull(); + expect(typeof hover).toBe("string"); + // The hover text should mention the table + expect(hover?.toLowerCase()).toContain("users"); + }); + + test("hover on column name shows column type", () => { + workspace.insertFile("/hover-col.sql", "SELECT email FROM users;"); + // Position over "email" (around character 7) + const hover = workspace.hover("/hover-col.sql", 8); + // With schema loaded, hover should return type info (a markdown string) + expect(hover).not.toBeNull(); + expect(typeof hover).toBe("string"); + // The hover text should mention the type + expect(hover?.toLowerCase()).toContain("text"); + }); + + test("clearSchema removes schema and hover returns null", () => { + // First verify hover works with schema (set by beforeEach) + workspace.insertFile("/with-schema.sql", "SELECT * FROM users;"); + const hoverWithSchema = workspace.hover("/with-schema.sql", 14); + expect(hoverWithSchema).not.toBeNull(); + + // Now clear schema and verify hover returns null + workspace.clearSchema(); + workspace.insertFile("/no-schema.sql", "SELECT * FROM users;"); + const hoverWithoutSchema = workspace.hover("/no-schema.sql", 14); + expect(hoverWithoutSchema).toBeNull(); + }); }); diff --git a/packages/@postgres-language-server/wasm/tsconfig.build.json b/packages/@postgres-language-server/wasm/tsconfig.build.json index 2d8c92077..3e8ab89b1 100644 --- a/packages/@postgres-language-server/wasm/tsconfig.build.json +++ b/packages/@postgres-language-server/wasm/tsconfig.build.json @@ -1,18 +1,18 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "node", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./src", - "lib": ["ES2020", "DOM"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "e2e"] + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "lib": ["ES2020", "DOM"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "e2e"] } diff --git a/packages/@postgres-language-server/wasm/tsconfig.json b/packages/@postgres-language-server/wasm/tsconfig.json index c895b0888..51db058dc 100644 --- a/packages/@postgres-language-server/wasm/tsconfig.json +++ b/packages/@postgres-language-server/wasm/tsconfig.json @@ -1,18 +1,18 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "outDir": "./dist", - "rootDir": "./src", - "lib": ["ES2020", "DOM"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "lib": ["ES2020", "DOM"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/@postgrestools/backend-jsonrpc/package.json b/packages/@postgrestools/backend-jsonrpc/package.json index d8e0db2cc..187e0f0c5 100644 --- a/packages/@postgrestools/backend-jsonrpc/package.json +++ b/packages/@postgrestools/backend-jsonrpc/package.json @@ -1,32 +1,38 @@ { - "name": "@postgrestools/backend-jsonrpc", - "version": "", - "main": "dist/index.js", - "scripts": { - "test": "bun test", - "test:ci": "bun build && bun test", - "build": "bun build ./src/index.ts --outdir ./dist --target node" - }, - "files": ["dist/", "README.md"], - "repository": { - "type": "git", - "url": "git+https://github.com/supabase-community/postgres-language-server.git", - "directory": "packages/@postgrestools/backend-jsonrpc" - }, - "author": "Supabase Community", - "bugs": "ttps://github.com/supabase-community/postgres-language-server/issues", - "description": "Bindings to the JSON-RPC Workspace API of the Postgres Language Tools daemon", - "keywords": ["TypeScript", "Postgres"], - "license": "MIT", - "publishConfig": { - "provenance": true - }, - "optionalDependencies": { - "@postgrestools/cli-win32-x64": "", - "@postgrestools/cli-win32-arm64": "", - "@postgrestools/cli-darwin-x64": "", - "@postgrestools/cli-darwin-arm64": "", - "@postgrestools/cli-linux-x64": "", - "@postgrestools/cli-linux-arm64": "" - } + "name": "@postgrestools/backend-jsonrpc", + "version": "", + "description": "Bindings to the JSON-RPC Workspace API of the Postgres Language Tools daemon", + "keywords": [ + "Postgres", + "TypeScript" + ], + "bugs": "ttps://github.com/supabase-community/postgres-language-server/issues", + "license": "MIT", + "author": "Supabase Community", + "repository": { + "type": "git", + "url": "git+https://github.com/supabase-community/postgres-language-server.git", + "directory": "packages/@postgrestools/backend-jsonrpc" + }, + "files": [ + "dist/", + "README.md" + ], + "main": "dist/index.js", + "publishConfig": { + "provenance": true + }, + "scripts": { + "test": "bun test", + "test:ci": "bun build && bun test", + "build": "bun build ./src/index.ts --outdir ./dist --target node" + }, + "optionalDependencies": { + "@postgrestools/cli-darwin-arm64": "", + "@postgrestools/cli-darwin-x64": "", + "@postgrestools/cli-linux-arm64": "", + "@postgrestools/cli-linux-x64": "", + "@postgrestools/cli-win32-arm64": "", + "@postgrestools/cli-win32-x64": "" + } } diff --git a/packages/@postgrestools/backend-jsonrpc/src/command.ts b/packages/@postgrestools/backend-jsonrpc/src/command.ts index 94f19b721..59f43213e 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/command.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/command.ts @@ -6,69 +6,66 @@ import { execSync } from "node:child_process"; * @returns Filesystem path to the binary, or null if no prebuilt distribution exists for the current platform */ export function getCommand(): string | null { - const { platform, arch } = process; + const { platform, arch } = process; - const PLATFORMS: Partial< - Record< - NodeJS.Platform | "linux-musl", - Partial> - > - > = { - win32: { - x64: "@postgrestools/cli-x86_64-windows-msvc/postgrestools.exe", - arm64: "@postgrestools/cli-aarch64-windows-msvc/postgrestools.exe", - }, - darwin: { - x64: "@postgrestools/cli-x86_64-apple-darwin/postgrestools", - arm64: "@postgrestools/cli-aarch64-apple-darwin/postgrestools", - }, - linux: { - x64: "@postgrestools/cli-x86_64-linux-gnu/postgrestools", - arm64: "@postgrestools/cli-aarch64-linux-gnu/postgrestools", - }, - "linux-musl": { - x64: "@postgrestools/cli-x86_64-linux-musl/postgrestools", - // no arm64 build for musl - }, - }; + const PLATFORMS: Partial< + Record>> + > = { + win32: { + x64: "@postgrestools/cli-x86_64-windows-msvc/postgrestools.exe", + arm64: "@postgrestools/cli-aarch64-windows-msvc/postgrestools.exe", + }, + darwin: { + x64: "@postgrestools/cli-x86_64-apple-darwin/postgrestools", + arm64: "@postgrestools/cli-aarch64-apple-darwin/postgrestools", + }, + linux: { + x64: "@postgrestools/cli-x86_64-linux-gnu/postgrestools", + arm64: "@postgrestools/cli-aarch64-linux-gnu/postgrestools", + }, + "linux-musl": { + x64: "@postgrestools/cli-x86_64-linux-musl/postgrestools", + // no arm64 build for musl + }, + }; - function isMusl() { - let stderr = ""; - try { - stderr = execSync("ldd --version", { - stdio: [ - "ignore", // stdin - "pipe", // stdout – glibc systems print here - "pipe", // stderr – musl systems print here - ], - }).toString(); - } catch (err: unknown) { - if (hasStdErr(err)) { - stderr = err.stderr; - } - } - if (stderr.indexOf("musl") > -1) { - return true; - } - return false; - } + function isMusl() { + let stderr = ""; + try { + stderr = execSync("ldd --version", { + stdio: [ + "ignore", // stdin + "pipe", // stdout – glibc systems print here + "pipe", // stderr – musl systems print here + ], + }).toString(); + } catch (err: unknown) { + if (hasStdErr(err)) { + stderr = err.stderr; + } + } + if (stderr.indexOf("musl") > -1) { + return true; + } + return false; + } - function getPlatform(): NodeJS.Platform | "linux-musl" { - if (platform === "linux") { - return isMusl() ? "linux-musl" : "linux"; - } + function getPlatform(): NodeJS.Platform | "linux-musl" { + if (platform === "linux") { + return isMusl() ? "linux-musl" : "linux"; + } - return platform; - } + return platform; + } - const binPath = PLATFORMS?.[getPlatform()]?.[arch]; - if (!binPath) { - return null; - } + const binPath = PLATFORMS?.[getPlatform()]?.[arch]; + if (!binPath) { + return null; + } - return require.resolve(binPath); + return require.resolve(binPath); } function hasStdErr(err: unknown): err is { stderr: string } { - return typeof err === "object" && err !== null && "stderr" in err; + return typeof err === "object" && err !== null && "stderr" in err; } diff --git a/packages/@postgrestools/backend-jsonrpc/src/index.ts b/packages/@postgrestools/backend-jsonrpc/src/index.ts index 92ed6660e..35694e54b 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/index.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/index.ts @@ -10,12 +10,12 @@ import { type Workspace, createWorkspace as wrapTransport } from "./workspace"; * @returns A Workspace client, or null if the underlying platform is not supported */ export async function createWorkspace(): Promise { - const command = getCommand(); - if (!command) { - return null; - } + const command = getCommand(); + if (!command) { + return null; + } - return createWorkspaceWithBinary(command); + return createWorkspaceWithBinary(command); } /** @@ -26,21 +26,19 @@ export async function createWorkspace(): Promise { * @param command Path to the binary * @returns A Workspace client, or null if the underlying platform is not supported */ -export async function createWorkspaceWithBinary( - command: string, -): Promise { - const socket = await createSocket(command); - const transport = new Transport(socket); +export async function createWorkspaceWithBinary(command: string): Promise { + const socket = await createSocket(command); + const transport = new Transport(socket); - await transport.request("initialize", { - capabilities: {}, - client_info: { - name: "@postgrestools/backend-jsonrpc", - version: "0.0.0", - }, - }); + await transport.request("initialize", { + capabilities: {}, + client_info: { + name: "@postgrestools/backend-jsonrpc", + version: "0.0.0", + }, + }); - return wrapTransport(transport); + return wrapTransport(transport); } export * from "./workspace"; diff --git a/packages/@postgrestools/backend-jsonrpc/src/socket.ts b/packages/@postgrestools/backend-jsonrpc/src/socket.ts index 6fd2902f9..dda919b0e 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/socket.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/socket.ts @@ -2,30 +2,26 @@ import { spawn } from "node:child_process"; import { type Socket, connect } from "node:net"; function getSocket(command: string): Promise { - return new Promise((resolve, reject) => { - const process = spawn(command, ["__print_socket"], { - stdio: "pipe", - }); + return new Promise((resolve, reject) => { + const process = spawn(command, ["__print_socket"], { + stdio: "pipe", + }); - process.on("error", reject); + process.on("error", reject); - let pipeName = ""; - process.stdout.on("data", (data) => { - pipeName += data.toString("utf-8"); - }); + let pipeName = ""; + process.stdout.on("data", (data) => { + pipeName += data.toString("utf-8"); + }); - process.on("exit", (code) => { - if (code === 0) { - resolve(pipeName.trimEnd()); - } else { - reject( - new Error( - `Command '${command} __print_socket' exited with code ${code}`, - ), - ); - } - }); - }); + process.on("exit", (code) => { + if (code === 0) { + resolve(pipeName.trimEnd()); + } else { + reject(new Error(`Command '${command} __print_socket' exited with code ${code}`)); + } + }); + }); } /** @@ -35,13 +31,13 @@ function getSocket(command: string): Promise { * @returns Socket instance connected to the daemon */ export async function createSocket(command: string): Promise { - const path = await getSocket(command); - const socket = connect(path); + const path = await getSocket(command); + const socket = connect(path); - await new Promise((resolve, reject) => { - socket.once("error", reject); - socket.once("ready", resolve); - }); + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.once("ready", resolve); + }); - return socket; + return socket; } diff --git a/packages/@postgrestools/backend-jsonrpc/src/transport.ts b/packages/@postgrestools/backend-jsonrpc/src/transport.ts index b1cdad445..7d2e111a6 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/transport.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/transport.ts @@ -1,99 +1,95 @@ interface Socket { - on(event: "data", fn: (data: Buffer) => void): void; - write(data: Buffer): void; - destroy(): void; + on(event: "data", fn: (data: Buffer) => void): void; + write(data: Buffer): void; + destroy(): void; } enum ReaderStateKind { - Header = 0, - Body = 1, + Header = 0, + Body = 1, } interface ReaderStateHeader { - readonly kind: ReaderStateKind.Header; - contentLength?: number; - contentType?: string; + readonly kind: ReaderStateKind.Header; + contentLength?: number; + contentType?: string; } interface ReaderStateBody { - readonly kind: ReaderStateKind.Body; - readonly contentLength: number; - readonly contentType?: string; + readonly kind: ReaderStateKind.Body; + readonly contentLength: number; + readonly contentType?: string; } type ReaderState = ReaderStateHeader | ReaderStateBody; interface JsonRpcRequest { - jsonrpc: "2.0"; - id: number; - method: string; - params: unknown; + jsonrpc: "2.0"; + id: number; + method: string; + params: unknown; } function isJsonRpcRequest(message: JsonRpcMessage): message is JsonRpcRequest { - return ( - "id" in message && - typeof message.id === "number" && - "method" in message && - typeof message.method === "string" && - "params" in message - ); + return ( + "id" in message && + typeof message.id === "number" && + "method" in message && + typeof message.method === "string" && + "params" in message + ); } interface JsonRpcNotification { - jsonrpc: "2.0"; - method: string; - params: unknown; + jsonrpc: "2.0"; + method: string; + params: unknown; } -function isJsonRpcNotification( - message: JsonRpcMessage, -): message is JsonRpcNotification { - return ( - !("id" in message) && - "method" in message && - typeof message.method === "string" && - "params" in message - ); +function isJsonRpcNotification(message: JsonRpcMessage): message is JsonRpcNotification { + return ( + !("id" in message) && + "method" in message && + typeof message.method === "string" && + "params" in message + ); } type JsonRpcResponse = - | { - jsonrpc: "2.0"; - id: number; - result: unknown; - } - | { - jsonrpc: "2.0"; - id: number; - error: unknown; - }; - -function isJsonRpcResponse( - message: JsonRpcMessage, -): message is JsonRpcResponse { - return ( - "id" in message && - typeof message.id === "number" && - !("method" in message) && - ("result" in message || "error" in message) - ); + | { + jsonrpc: "2.0"; + id: number; + result: unknown; + } + | { + jsonrpc: "2.0"; + id: number; + error: unknown; + }; + +function isJsonRpcResponse(message: JsonRpcMessage): message is JsonRpcResponse { + return ( + "id" in message && + typeof message.id === "number" && + !("method" in message) && + ("result" in message || "error" in message) + ); } type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse; function isJsonRpcMessage(message: unknown): message is JsonRpcMessage { - return ( - typeof message === "object" && - message !== null && - "jsonrpc" in message && - message.jsonrpc === "2.0" - ); + return ( + typeof message === "object" && + message !== null && + "jsonrpc" in message && + message.jsonrpc === "2.0" + ); } interface PendingRequest { - resolve(result: unknown): void; - reject(error: unknown): void; + resolve(result: unknown): void; + reject(error: unknown): void; } const MIME_JSONRPC = "application/vscode-jsonrpc"; @@ -102,192 +98,185 @@ const MIME_JSONRPC = "application/vscode-jsonrpc"; * Implements the daemon server JSON-RPC protocol over a Socket instance */ export class Transport { - /** - * Counter incremented for each outgoing request to generate a unique ID - */ - private nextRequestId = 0; - - /** - * Storage for the promise resolver functions of pending requests, - * keyed by ID of the request - */ - private pendingRequests: Map = new Map(); - - constructor(private socket: Socket) { - socket.on("data", (data) => { - this.processIncoming(data); - }); - } - - /** - * Send a request to the remote server - * - * @param method Name of the remote method to call - * @param params Parameters object the remote method should be called with - * @return Promise resolving with the value returned by the remote method, or rejecting with an RPC error if the remote call failed - */ - // biome-ignore lint/suspicious/noExplicitAny: if i change it to Promise typescript breaks - request(method: string, params: unknown): Promise { - return new Promise((resolve, reject) => { - const id = this.nextRequestId++; - this.pendingRequests.set(id, { resolve, reject }); - this.sendMessage({ - jsonrpc: "2.0", - id, - method, - params, - }); - }); - } - - /** - * Send a notification message to the remote server - * - * @param method Name of the remote method to call - * @param params Parameters object the remote method should be called with - */ - notify(method: string, params: unknown) { - this.sendMessage({ - jsonrpc: "2.0", - method, - params, - }); - } - - /** - * Destroy the internal socket instance for this Transport - */ - destroy() { - this.socket.destroy(); - } - - private sendMessage(message: JsonRpcMessage) { - const body = Buffer.from(JSON.stringify(message)); - const headers = Buffer.from( - `Content-Length: ${body.length}\r\nContent-Type: ${MIME_JSONRPC};charset=utf-8\r\n\r\n`, - ); - this.socket.write(Buffer.concat([headers, body])); - } - - private pendingData = Buffer.from(""); - private readerState: ReaderState = { - kind: ReaderStateKind.Header, - }; - - private processIncoming(data: Buffer) { - this.pendingData = Buffer.concat([this.pendingData, data]); - - while (this.pendingData.length > 0) { - if (this.readerState.kind === ReaderStateKind.Header) { - const lineBreakIndex = this.pendingData.indexOf("\n"); - if (lineBreakIndex < 0) { - break; - } - - const header = this.pendingData.subarray(0, lineBreakIndex + 1); - this.pendingData = this.pendingData.subarray(lineBreakIndex + 1); - this.processIncomingHeader(this.readerState, header.toString("utf-8")); - } else if (this.pendingData.length >= this.readerState.contentLength) { - const body = this.pendingData.subarray( - 0, - this.readerState.contentLength, - ); - this.pendingData = this.pendingData.subarray( - this.readerState.contentLength, - ); - this.processIncomingBody(body); - - this.readerState = { - kind: ReaderStateKind.Header, - }; - } else { - break; - } - } - } - - private processIncomingHeader(readerState: ReaderStateHeader, line: string) { - if (line === "\r\n") { - const { contentLength, contentType } = readerState; - if (typeof contentLength !== "number") { - throw new Error( - "incoming message from the remote workspace is missing the Content-Length header", - ); - } - - this.readerState = { - kind: ReaderStateKind.Body, - contentLength, - contentType, - }; - return; - } - - const colonIndex = line.indexOf(":"); - if (colonIndex < 0) { - throw new Error(`could not find colon token in "${line}"`); - } - - const headerName = line.substring(0, colonIndex); - const headerValue = line.substring(colonIndex + 1).trim(); - - switch (headerName) { - case "Content-Length": { - const value = Number.parseInt(headerValue); - readerState.contentLength = value; - break; - } - case "Content-Type": { - if (!headerValue.startsWith(MIME_JSONRPC)) { - throw new Error( - `invalid value for Content-Type expected "${MIME_JSONRPC}", got "${headerValue}"`, - ); - } - - readerState.contentType = headerValue; - break; - } - default: - console.warn(`ignoring unknown header "${headerName}"`); - } - } - - private processIncomingBody(buffer: Buffer) { - const data = buffer.toString("utf-8"); - const body = JSON.parse(data); - - if (isJsonRpcMessage(body)) { - if (isJsonRpcRequest(body)) { - // TODO: Not implemented at the moment - return; - } - - if (isJsonRpcNotification(body)) { - // TODO: Not implemented at the moment - return; - } - - if (isJsonRpcResponse(body)) { - const pendingRequest = this.pendingRequests.get(body.id); - if (pendingRequest) { - this.pendingRequests.delete(body.id); - const { resolve, reject } = pendingRequest; - if ("result" in body) { - resolve(body.result); - } else { - reject(body.error); - } - } else { - throw new Error( - `could not find any pending request matching RPC response ID ${body.id}`, - ); - } - return; - } - } - - throw new Error( - `failed to deserialize incoming message from remote workspace, "${data}" is not a valid JSON-RPC message body`, - ); - } + /** + * Counter incremented for each outgoing request to generate a unique ID + */ + private nextRequestId = 0; + + /** + * Storage for the promise resolver functions of pending requests, + * keyed by ID of the request + */ + private pendingRequests: Map = new Map(); + + constructor(private socket: Socket) { + socket.on("data", (data) => { + this.processIncoming(data); + }); + } + + /** + * Send a request to the remote server + * + * @param method Name of the remote method to call + * @param params Parameters object the remote method should be called with + * @return Promise resolving with the value returned by the remote method, or rejecting with an RPC error if the remote call failed + */ + // oxlint-disable-next-line typescript/no-explicit-any -- if i change it to Promise typescript breaks + request(method: string, params: unknown): Promise { + return new Promise((resolve, reject) => { + const id = this.nextRequestId++; + this.pendingRequests.set(id, { resolve, reject }); + this.sendMessage({ + jsonrpc: "2.0", + id, + method, + params, + }); + }); + } + + /** + * Send a notification message to the remote server + * + * @param method Name of the remote method to call + * @param params Parameters object the remote method should be called with + */ + notify(method: string, params: unknown) { + this.sendMessage({ + jsonrpc: "2.0", + method, + params, + }); + } + + /** + * Destroy the internal socket instance for this Transport + */ + destroy() { + this.socket.destroy(); + } + + private sendMessage(message: JsonRpcMessage) { + const body = Buffer.from(JSON.stringify(message)); + const headers = Buffer.from( + `Content-Length: ${body.length}\r\nContent-Type: ${MIME_JSONRPC};charset=utf-8\r\n\r\n`, + ); + this.socket.write(Buffer.concat([headers, body])); + } + + private pendingData = Buffer.from(""); + private readerState: ReaderState = { + kind: ReaderStateKind.Header, + }; + + private processIncoming(data: Buffer) { + this.pendingData = Buffer.concat([this.pendingData, data]); + + while (this.pendingData.length > 0) { + if (this.readerState.kind === ReaderStateKind.Header) { + const lineBreakIndex = this.pendingData.indexOf("\n"); + if (lineBreakIndex < 0) { + break; + } + + const header = this.pendingData.subarray(0, lineBreakIndex + 1); + this.pendingData = this.pendingData.subarray(lineBreakIndex + 1); + this.processIncomingHeader(this.readerState, header.toString("utf-8")); + } else if (this.pendingData.length >= this.readerState.contentLength) { + const body = this.pendingData.subarray(0, this.readerState.contentLength); + this.pendingData = this.pendingData.subarray(this.readerState.contentLength); + this.processIncomingBody(body); + + this.readerState = { + kind: ReaderStateKind.Header, + }; + } else { + break; + } + } + } + + private processIncomingHeader(readerState: ReaderStateHeader, line: string) { + if (line === "\r\n") { + const { contentLength, contentType } = readerState; + if (typeof contentLength !== "number") { + throw new Error( + "incoming message from the remote workspace is missing the Content-Length header", + ); + } + + this.readerState = { + kind: ReaderStateKind.Body, + contentLength, + contentType, + }; + return; + } + + const colonIndex = line.indexOf(":"); + if (colonIndex < 0) { + throw new Error(`could not find colon token in "${line}"`); + } + + const headerName = line.substring(0, colonIndex); + const headerValue = line.substring(colonIndex + 1).trim(); + + switch (headerName) { + case "Content-Length": { + const value = Number.parseInt(headerValue); + readerState.contentLength = value; + break; + } + case "Content-Type": { + if (!headerValue.startsWith(MIME_JSONRPC)) { + throw new Error( + `invalid value for Content-Type expected "${MIME_JSONRPC}", got "${headerValue}"`, + ); + } + + readerState.contentType = headerValue; + break; + } + default: + console.warn(`ignoring unknown header "${headerName}"`); + } + } + + private processIncomingBody(buffer: Buffer) { + const data = buffer.toString("utf-8"); + const body = JSON.parse(data); + + if (isJsonRpcMessage(body)) { + if (isJsonRpcRequest(body)) { + // TODO: Not implemented at the moment + return; + } + + if (isJsonRpcNotification(body)) { + // TODO: Not implemented at the moment + return; + } + + if (isJsonRpcResponse(body)) { + const pendingRequest = this.pendingRequests.get(body.id); + if (pendingRequest) { + this.pendingRequests.delete(body.id); + const { resolve, reject } = pendingRequest; + if ("result" in body) { + resolve(body.result); + } else { + reject(body.error); + } + } else { + throw new Error(`could not find any pending request matching RPC response ID ${body.id}`); + } + return; + } + } + + throw new Error( + `failed to deserialize incoming message from remote workspace, "${data}" is not a valid JSON-RPC message body`, + ); + } } diff --git a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts index 4d4a25d4d..3d4383427 100644 --- a/packages/@postgrestools/backend-jsonrpc/src/workspace.ts +++ b/packages/@postgrestools/backend-jsonrpc/src/workspace.ts @@ -1,18 +1,18 @@ // Generated file, do not edit by hand, see `xtask/codegen` import type { Transport } from "./transport"; export interface IsPathIgnoredParams { - pgls_path: PgLSPath; + pgls_path: PgLSPath; } export interface PgLSPath { - /** - * Determines the kind of the file inside Postgres Language Server. Some files are considered as configuration files, others as manifest files, and others as files to handle - */ - kind: FileKind; - path: string; - /** - * Whether this path (usually a file) was fixed as a result of a format/lint/check command with the `--write` filag. - */ - was_written: boolean; + /** + * Determines the kind of the file inside Postgres Language Server. Some files are considered as configuration files, others as manifest files, and others as files to handle + */ + kind: FileKind; + path: string; + /** + * Whether this path (usually a file) was fixed as a result of a format/lint/check command with the `--write` filag. + */ + was_written: boolean; } export type FileKind = FileKind2[]; /** @@ -20,155 +20,155 @@ export type FileKind = FileKind2[]; */ export type FileKind2 = "Config" | "Ignore" | "Inspectable" | "Handleable"; export interface RegisterProjectFolderParams { - path?: string; - setAsCurrentWorkspace: boolean; + path?: string; + setAsCurrentWorkspace: boolean; } export type ProjectKey = string; export interface GetFileContentParams { - path: PgLSPath; + path: PgLSPath; } export interface PullFileDiagnosticsParams { - categories: RuleCategories; - max_diagnostics: number; - only: RuleCode[]; - path: PgLSPath; - skip: RuleCode[]; + categories: RuleCategories; + max_diagnostics: number; + only: RuleCode[]; + path: PgLSPath; + skip: RuleCode[]; } export type RuleCategories = RuleCategory[]; export type RuleCode = string; export type RuleCategory = "Lint" | "Action" | "Transformation"; export interface PullDiagnosticsResult { - diagnostics: Diagnostic[]; - skipped_diagnostics: number; + diagnostics: Diagnostic[]; + skipped_diagnostics: number; } /** * Serializable representation for a [Diagnostic](super::Diagnostic). */ export interface Diagnostic { - advices: Advices; - category?: Category; - description: string; - location: Location; - message: MarkupBuf; - severity: Severity; - source?: Diagnostic; - tags: DiagnosticTags; - verboseAdvices: Advices; + advices: Advices; + category?: Category; + description: string; + location: Location; + message: MarkupBuf; + severity: Severity; + source?: Diagnostic; + tags: DiagnosticTags; + verboseAdvices: Advices; } /** * Implementation of [Visitor] collecting serializable [Advice] into a vector. */ export interface Advices { - advices: Advice[]; + advices: Advice[]; } export type Category = - | "lint/safety/addSerialColumn" - | "lint/safety/addingFieldWithDefault" - | "lint/safety/addingForeignKeyConstraint" - | "lint/safety/addingNotNullField" - | "lint/safety/addingPrimaryKeyConstraint" - | "lint/safety/addingRequiredField" - | "lint/safety/banCharField" - | "lint/safety/banConcurrentIndexCreationInTransaction" - | "lint/safety/banDropColumn" - | "lint/safety/banDropDatabase" - | "lint/safety/banDropNotNull" - | "lint/safety/banDropTable" - | "lint/safety/banTruncateCascade" - | "lint/safety/changingColumnType" - | "lint/safety/constraintMissingNotValid" - | "lint/safety/creatingEnum" - | "lint/safety/disallowUniqueConstraint" - | "lint/safety/lockTimeoutWarning" - | "lint/safety/multipleAlterTable" - | "lint/safety/preferBigInt" - | "lint/safety/preferBigintOverInt" - | "lint/safety/preferBigintOverSmallint" - | "lint/safety/preferIdentity" - | "lint/safety/preferJsonb" - | "lint/safety/preferRobustStmts" - | "lint/safety/preferTextField" - | "lint/safety/preferTimestamptz" - | "lint/safety/renamingColumn" - | "lint/safety/renamingTable" - | "lint/safety/requireConcurrentIndexCreation" - | "lint/safety/requireConcurrentIndexDeletion" - | "lint/safety/runningStatementWhileHoldingAccessExclusive" - | "lint/safety/transactionNesting" - | "pglinter/extensionNotInstalled" - | "pglinter/ruleDisabledInExtension" - | "pglinter/base/compositePrimaryKeyTooManyColumns" - | "pglinter/base/howManyObjectsWithUppercase" - | "pglinter/base/howManyRedudantIndex" - | "pglinter/base/howManyTableWithoutIndexOnFk" - | "pglinter/base/howManyTableWithoutPrimaryKey" - | "pglinter/base/howManyTablesNeverSelected" - | "pglinter/base/howManyTablesWithFkMismatch" - | "pglinter/base/howManyTablesWithFkOutsideSchema" - | "pglinter/base/howManyTablesWithReservedKeywords" - | "pglinter/base/howManyTablesWithSameTrigger" - | "pglinter/base/howManyUnusedIndex" - | "pglinter/base/severalTableOwnerInSchema" - | "pglinter/cluster/passwordEncryptionIsMd5" - | "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" - | "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists" - | "pglinter/schema/ownerSchemaIsInternalRole" - | "pglinter/schema/schemaOwnerDoNotMatchTableOwner" - | "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt" - | "pglinter/schema/schemaWithDefaultRoleNotGranted" - | "pglinter/schema/unsecuredPublicSchema" - | "splinter/performance/authRlsInitplan" - | "splinter/performance/duplicateIndex" - | "splinter/performance/multiplePermissivePolicies" - | "splinter/performance/noPrimaryKey" - | "splinter/performance/tableBloat" - | "splinter/performance/unindexedForeignKeys" - | "splinter/performance/unusedIndex" - | "splinter/security/authUsersExposed" - | "splinter/security/extensionInPublic" - | "splinter/security/extensionVersionsOutdated" - | "splinter/security/fkeyToAuthUnique" - | "splinter/security/foreignTableInApi" - | "splinter/security/functionSearchPathMutable" - | "splinter/security/insecureQueueExposedInApi" - | "splinter/security/materializedViewInApi" - | "splinter/security/policyExistsRlsDisabled" - | "splinter/security/rlsDisabledInPublic" - | "splinter/security/rlsEnabledNoPolicy" - | "splinter/security/rlsPolicyAlwaysTrue" - | "splinter/security/rlsReferencesUserMetadata" - | "splinter/security/securityDefinerView" - | "splinter/security/sensitiveColumnsExposed" - | "splinter/security/unsupportedRegTypes" - | "stdin" - | "check" - | "format" - | "configuration" - | "database/connection" - | "internalError/io" - | "internalError/runtime" - | "internalError/fs" - | "flags/invalid" - | "project" - | "typecheck" - | "plpgsql_check" - | "internalError/panic" - | "syntax" - | "dummy" - | "lint" - | "lint/performance" - | "lint/safety" - | "splinter" - | "splinter/performance" - | "splinter/security" - | "pglinter" - | "pglinter/base" - | "pglinter/cluster" - | "pglinter/schema"; + | "lint/safety/addSerialColumn" + | "lint/safety/addingFieldWithDefault" + | "lint/safety/addingForeignKeyConstraint" + | "lint/safety/addingNotNullField" + | "lint/safety/addingPrimaryKeyConstraint" + | "lint/safety/addingRequiredField" + | "lint/safety/banCharField" + | "lint/safety/banConcurrentIndexCreationInTransaction" + | "lint/safety/banDropColumn" + | "lint/safety/banDropDatabase" + | "lint/safety/banDropNotNull" + | "lint/safety/banDropTable" + | "lint/safety/banTruncateCascade" + | "lint/safety/changingColumnType" + | "lint/safety/constraintMissingNotValid" + | "lint/safety/creatingEnum" + | "lint/safety/disallowUniqueConstraint" + | "lint/safety/lockTimeoutWarning" + | "lint/safety/multipleAlterTable" + | "lint/safety/preferBigInt" + | "lint/safety/preferBigintOverInt" + | "lint/safety/preferBigintOverSmallint" + | "lint/safety/preferIdentity" + | "lint/safety/preferJsonb" + | "lint/safety/preferRobustStmts" + | "lint/safety/preferTextField" + | "lint/safety/preferTimestamptz" + | "lint/safety/renamingColumn" + | "lint/safety/renamingTable" + | "lint/safety/requireConcurrentIndexCreation" + | "lint/safety/requireConcurrentIndexDeletion" + | "lint/safety/runningStatementWhileHoldingAccessExclusive" + | "lint/safety/transactionNesting" + | "pglinter/extensionNotInstalled" + | "pglinter/ruleDisabledInExtension" + | "pglinter/base/compositePrimaryKeyTooManyColumns" + | "pglinter/base/howManyObjectsWithUppercase" + | "pglinter/base/howManyRedudantIndex" + | "pglinter/base/howManyTableWithoutIndexOnFk" + | "pglinter/base/howManyTableWithoutPrimaryKey" + | "pglinter/base/howManyTablesNeverSelected" + | "pglinter/base/howManyTablesWithFkMismatch" + | "pglinter/base/howManyTablesWithFkOutsideSchema" + | "pglinter/base/howManyTablesWithReservedKeywords" + | "pglinter/base/howManyTablesWithSameTrigger" + | "pglinter/base/howManyUnusedIndex" + | "pglinter/base/severalTableOwnerInSchema" + | "pglinter/cluster/passwordEncryptionIsMd5" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists" + | "pglinter/cluster/pgHbaEntriesWithMethodTrustShouldNotExists" + | "pglinter/schema/ownerSchemaIsInternalRole" + | "pglinter/schema/schemaOwnerDoNotMatchTableOwner" + | "pglinter/schema/schemaPrefixedOrSuffixedWithEnvt" + | "pglinter/schema/schemaWithDefaultRoleNotGranted" + | "pglinter/schema/unsecuredPublicSchema" + | "splinter/performance/authRlsInitplan" + | "splinter/performance/duplicateIndex" + | "splinter/performance/multiplePermissivePolicies" + | "splinter/performance/noPrimaryKey" + | "splinter/performance/tableBloat" + | "splinter/performance/unindexedForeignKeys" + | "splinter/performance/unusedIndex" + | "splinter/security/authUsersExposed" + | "splinter/security/extensionInPublic" + | "splinter/security/extensionVersionsOutdated" + | "splinter/security/fkeyToAuthUnique" + | "splinter/security/foreignTableInApi" + | "splinter/security/functionSearchPathMutable" + | "splinter/security/insecureQueueExposedInApi" + | "splinter/security/materializedViewInApi" + | "splinter/security/policyExistsRlsDisabled" + | "splinter/security/rlsDisabledInPublic" + | "splinter/security/rlsEnabledNoPolicy" + | "splinter/security/rlsPolicyAlwaysTrue" + | "splinter/security/rlsReferencesUserMetadata" + | "splinter/security/securityDefinerView" + | "splinter/security/sensitiveColumnsExposed" + | "splinter/security/unsupportedRegTypes" + | "stdin" + | "check" + | "format" + | "configuration" + | "database/connection" + | "internalError/io" + | "internalError/runtime" + | "internalError/fs" + | "flags/invalid" + | "project" + | "typecheck" + | "plpgsql_check" + | "internalError/panic" + | "syntax" + | "dummy" + | "lint" + | "lint/performance" + | "lint/safety" + | "splinter" + | "splinter/performance" + | "splinter/security" + | "pglinter" + | "pglinter/base" + | "pglinter/cluster" + | "pglinter/schema"; export interface Location { - path?: Resource_for_String; - sourceCode?: string; - span?: TextRange; + path?: Resource_for_String; + sourceCode?: string; + span?: TextRange; } export type MarkupBuf = MarkupNodeBuf[]; /** @@ -182,43 +182,39 @@ export type DiagnosticTags = DiagnosticTag[]; See the [Visitor] trait for additional documentation on all the supported advice types. */ export type Advice = - | { log: [LogCategory, MarkupBuf] } - | { list: MarkupBuf[] } - | { frame: Location } - | { diff: TextEdit } - | { diffWithOffset: [TextEdit, number] } - | { backtrace: [MarkupBuf, Backtrace] } - | { command: string } - | { group: [MarkupBuf, Advices] }; + | { log: [LogCategory, MarkupBuf] } + | { list: MarkupBuf[] } + | { frame: Location } + | { diff: TextEdit } + | { diffWithOffset: [TextEdit, number] } + | { backtrace: [MarkupBuf, Backtrace] } + | { command: string } + | { group: [MarkupBuf, Advices] }; /** * Represents the resource a diagnostic is associated with. */ -export type Resource_for_String = - | "database" - | "argv" - | "memory" - | { file: string }; +export type Resource_for_String = "database" | "argv" | "memory" | { file: string }; export type TextRange = [TextSize, TextSize]; export interface MarkupNodeBuf { - content: string; - elements: MarkupElement[]; + content: string; + elements: MarkupElement[]; } /** * Internal enum used to automatically generate bit offsets for [DiagnosticTags] and help with the implementation of `serde` and `schemars` for tags. */ export type DiagnosticTag = - | "fixable" - | "internal" - | "unnecessaryCode" - | "deprecatedCode" - | "verbose"; + | "fixable" + | "internal" + | "unnecessaryCode" + | "deprecatedCode" + | "verbose"; /** * The category for a log advice, defines how the message should be presented to the user. */ export type LogCategory = "none" | "info" | "warn" | "error"; export interface TextEdit { - dictionary: string; - ops: CompressedOp[]; + dictionary: string; + ops: CompressedOp[]; } export type Backtrace = BacktraceFrame[]; export type TextSize = number; @@ -226,65 +222,63 @@ export type TextSize = number; * Enumeration of all the supported markup elements */ export type MarkupElement = - | "Emphasis" - | "Dim" - | "Italic" - | "Underline" - | "Error" - | "Success" - | "Warn" - | "Info" - | "Debug" - | "Trace" - | "Inverse" - | { Hyperlink: { href: string } }; -export type CompressedOp = - | { diffOp: DiffOp } - | { equalLines: { line_count: number } }; + | "Emphasis" + | "Dim" + | "Italic" + | "Underline" + | "Error" + | "Success" + | "Warn" + | "Info" + | "Debug" + | "Trace" + | "Inverse" + | { Hyperlink: { href: string } }; +export type CompressedOp = { diffOp: DiffOp } | { equalLines: { line_count: number } }; /** * Serializable representation of a backtrace frame. */ export interface BacktraceFrame { - ip: number; - symbols: BacktraceSymbol[]; + ip: number; + symbols: BacktraceSymbol[]; } export type DiffOp = - | { equal: { range: TextRange } } - | { insert: { range: TextRange } } - | { delete: { range: TextRange } }; + | { equal: { range: TextRange } } + | { insert: { range: TextRange } } + | { delete: { range: TextRange } }; /** * Serializable representation of a backtrace frame symbol. */ export interface BacktraceSymbol { - colno?: number; - filename?: string; - lineno?: number; - name?: string; + colno?: number; + filename?: string; + lineno?: number; + name?: string; } export interface GetCompletionsParams { - /** - * The File for which a completion is requested. - */ - path: PgLSPath; - /** - * The Cursor position in the file for which a completion is requested. - */ - position: TextSize; + /** + * The File for which a completion is requested. + */ + path: PgLSPath; + /** + * The Cursor position in the file for which a completion is requested. + */ + position: TextSize; } export interface CompletionsResult { - items: CompletionItem[]; + items: CompletionItem[]; } export interface CompletionItem { - completion_text?: CompletionText; - description: string; - detail?: string; - kind: CompletionItemKind; - label: string; - preselected: boolean; - /** - * String used for sorting by LSP clients. - */ - sort_text: string; + completion_text?: CompletionText; + description: string; + detail?: string; + kind: CompletionItemKind; + label: string; + preselected: boolean; + /** + * String used for sorting by LSP clients. + */ + sort_text: string; } /** * The text that the editor should fill in. If `None`, the `label` should be used. Tables, for example, might have different completion_texts: @@ -292,280 +286,280 @@ export interface CompletionItem { label: "users", description: "Schema: auth", completion_text: "auth.users". */ export interface CompletionText { - is_snippet: boolean; - /** - * A `range` is required because some editors replace the current token, others naively insert the text. Having a range where start == end makes it an insertion. - */ - range: TextRange; - text: string; + is_snippet: boolean; + /** + * A `range` is required because some editors replace the current token, others naively insert the text. Having a range where start == end makes it an insertion. + */ + range: TextRange; + text: string; } export type CompletionItemKind = - | "table" - | "function" - | "column" - | "schema" - | "policy" - | "role" - | "keyword"; + | "table" + | "function" + | "column" + | "schema" + | "policy" + | "role" + | "keyword"; export interface UpdateSettingsParams { - configuration: PartialConfiguration; - gitignore_matches: string[]; - vcs_base_path?: string; - workspace_directory?: string; + configuration: PartialConfiguration; + gitignore_matches: string[]; + vcs_base_path?: string; + workspace_directory?: string; } /** * The configuration that is contained inside the configuration file. */ export interface PartialConfiguration { - /** - * A field for the [JSON schema](https://json-schema.org/) specification - */ - $schema?: string; - /** - * The configuration of the database connection - */ - db?: PartialDatabaseConfiguration; - /** - * A list of paths to other JSON files, used to extends the current configuration. - */ - extends?: StringSet; - /** - * The configuration of the filesystem - */ - files?: PartialFilesConfiguration; - /** - * The configuration for the SQL formatter - */ - format?: PartialFormatConfiguration; - /** - * The configuration for the linter - */ - linter?: PartialLinterConfiguration; - /** - * Configure migrations - */ - migrations?: PartialMigrationsConfiguration; - /** - * The configuration for pglinter - */ - pglinter?: PartialPglinterConfiguration; - /** - * The configuration for type checking - */ - plpgsqlCheck?: PartialPlPgSqlCheckConfiguration; - /** - * The configuration for splinter - */ - splinter?: PartialSplinterConfiguration; - /** - * The configuration for type checking - */ - typecheck?: PartialTypecheckConfiguration; - /** - * The configuration of the VCS integration - */ - vcs?: PartialVcsConfiguration; + /** + * A field for the [JSON schema](https://json-schema.org/) specification + */ + $schema?: string; + /** + * The configuration of the database connection + */ + db?: PartialDatabaseConfiguration; + /** + * A list of paths to other JSON files, used to extends the current configuration. + */ + extends?: StringSet; + /** + * The configuration of the filesystem + */ + files?: PartialFilesConfiguration; + /** + * The configuration for the SQL formatter + */ + format?: PartialFormatConfiguration; + /** + * The configuration for the linter + */ + linter?: PartialLinterConfiguration; + /** + * Configure migrations + */ + migrations?: PartialMigrationsConfiguration; + /** + * The configuration for pglinter + */ + pglinter?: PartialPglinterConfiguration; + /** + * The configuration for type checking + */ + plpgsqlCheck?: PartialPlPgSqlCheckConfiguration; + /** + * The configuration for splinter + */ + splinter?: PartialSplinterConfiguration; + /** + * The configuration for type checking + */ + typecheck?: PartialTypecheckConfiguration; + /** + * The configuration of the VCS integration + */ + vcs?: PartialVcsConfiguration; } /** * The configuration of the database connection. */ export interface PartialDatabaseConfiguration { - allowStatementExecutionsAgainst?: StringSet; - /** - * The connection timeout in seconds. - */ - connTimeoutSecs?: number; - /** - * A connection string that encodes the full connection setup. When provided, it takes precedence over the individual fields. Can also be set via the `DATABASE_URL` environment variable. - */ - connectionString?: string; - /** - * The name of the database. Can also be set via the `PGDATABASE` environment variable. - */ - database?: string; - /** - * The host of the database. Required if you want database-related features. All else falls back to sensible defaults. Can also be set via the `PGHOST` environment variable. - */ - host?: string; - /** - * The password to connect to the database. Can also be set via the `PGPASSWORD` environment variable. - */ - password?: string; - /** - * The port of the database. Can also be set via the `PGPORT` environment variable. - */ - port?: number; - /** - * The username to connect to the database. Can also be set via the `PGUSER` environment variable. - */ - username?: string; + allowStatementExecutionsAgainst?: StringSet; + /** + * The connection timeout in seconds. + */ + connTimeoutSecs?: number; + /** + * A connection string that encodes the full connection setup. When provided, it takes precedence over the individual fields. Can also be set via the `DATABASE_URL` environment variable. + */ + connectionString?: string; + /** + * The name of the database. Can also be set via the `PGDATABASE` environment variable. + */ + database?: string; + /** + * The host of the database. Required if you want database-related features. All else falls back to sensible defaults. Can also be set via the `PGHOST` environment variable. + */ + host?: string; + /** + * The password to connect to the database. Can also be set via the `PGPASSWORD` environment variable. + */ + password?: string; + /** + * The port of the database. Can also be set via the `PGPORT` environment variable. + */ + port?: number; + /** + * The username to connect to the database. Can also be set via the `PGUSER` environment variable. + */ + username?: string; } export type StringSet = string[]; /** * The configuration of the filesystem */ export interface PartialFilesConfiguration { - /** - * A list of Unix shell style patterns. Will ignore files/folders that will match these patterns. - */ - ignore?: StringSet; - /** - * A list of Unix shell style patterns. Will handle only those files/folders that will match these patterns. - */ - include?: StringSet; - /** - * The maximum allowed size for source code files in bytes. Files above this limit will be ignored for performance reasons. Defaults to 1 MiB - */ - maxSize?: number; + /** + * A list of Unix shell style patterns. Will ignore files/folders that will match these patterns. + */ + ignore?: StringSet; + /** + * A list of Unix shell style patterns. Will handle only those files/folders that will match these patterns. + */ + include?: StringSet; + /** + * The maximum allowed size for source code files in bytes. Files above this limit will be ignored for performance reasons. Defaults to 1 MiB + */ + maxSize?: number; } /** * The configuration for SQL formatting. */ export interface PartialFormatConfiguration { - /** - * Constant casing (NULL, TRUE, FALSE): "upper" or "lower". Default: "lower". - */ - constantCase?: KeywordCase; - /** - * If `false`, it disables the formatter. `true` by default. - */ - enabled?: boolean; - /** - * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. - */ - ignore?: StringSet; - /** - * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. - */ - include?: StringSet; - /** - * Number of spaces (or tab width) for indentation. Default: 2. - */ - indentSize?: number; - /** - * Indentation style: "spaces" or "tabs". Default: "spaces". - */ - indentStyle?: IndentStyle; - /** - * Keyword casing: "upper" or "lower". Default: "lower". - */ - keywordCase?: KeywordCase; - /** - * Maximum line width before breaking. Default: 100. - */ - lineWidth?: number; - /** - * If `true`, skip formatting of SQL function bodies (keep them verbatim). Default: `false`. - */ - skipFnBodies?: boolean; - /** - * Data type casing (text, varchar, int): "upper" or "lower". Default: "lower". - */ - typeCase?: KeywordCase; + /** + * Constant casing (NULL, TRUE, FALSE): "upper" or "lower". Default: "lower". + */ + constantCase?: KeywordCase; + /** + * If `false`, it disables the formatter. `true` by default. + */ + enabled?: boolean; + /** + * A list of Unix shell style patterns. The formatter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet; + /** + * A list of Unix shell style patterns. The formatter will include files/folders that will match these patterns. + */ + include?: StringSet; + /** + * Number of spaces (or tab width) for indentation. Default: 2. + */ + indentSize?: number; + /** + * Indentation style: "spaces" or "tabs". Default: "spaces". + */ + indentStyle?: IndentStyle; + /** + * Keyword casing: "upper" or "lower". Default: "lower". + */ + keywordCase?: KeywordCase; + /** + * Maximum line width before breaking. Default: 100. + */ + lineWidth?: number; + /** + * If `true`, skip formatting of SQL function bodies (keep them verbatim). Default: `false`. + */ + skipFnBodies?: boolean; + /** + * Data type casing (text, varchar, int): "upper" or "lower". Default: "lower". + */ + typeCase?: KeywordCase; } export interface PartialLinterConfiguration { - /** - * if `false`, it disables the feature and the linter won't be executed. `true` by default - */ - enabled?: boolean; - /** - * A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns. - */ - ignore?: StringSet; - /** - * A list of Unix shell style patterns. The linter will include files/folders that will match these patterns. - */ - include?: StringSet; - /** - * List of rules - */ - rules?: LinterRules; + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * A list of Unix shell style patterns. The linter will ignore files/folders that will match these patterns. + */ + ignore?: StringSet; + /** + * A list of Unix shell style patterns. The linter will include files/folders that will match these patterns. + */ + include?: StringSet; + /** + * List of rules + */ + rules?: LinterRules; } /** * The configuration of the filesystem */ export interface PartialMigrationsConfiguration { - /** - * Ignore any migrations before this timestamp - */ - after?: number; - /** - * The directory where the migration files are stored - */ - migrationsDir?: string; + /** + * Ignore any migrations before this timestamp + */ + after?: number; + /** + * The directory where the migration files are stored + */ + migrationsDir?: string; } export interface PartialPglinterConfiguration { - /** - * if `false`, it disables the feature and the linter won't be executed. `true` by default - */ - enabled?: boolean; - /** - * List of rules - */ - rules?: PglinterRules; + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * List of rules + */ + rules?: PglinterRules; } /** * The configuration for type checking. */ export interface PartialPlPgSqlCheckConfiguration { - /** - * if `false`, it disables the feature and pglpgsql_check won't be executed. `true` by default - */ - enabled?: boolean; + /** + * if `false`, it disables the feature and pglpgsql_check won't be executed. `true` by default + */ + enabled?: boolean; } export interface PartialSplinterConfiguration { - /** - * if `false`, it disables the feature and the linter won't be executed. `true` by default - */ - enabled?: boolean; - /** - * A list of glob patterns for database objects to ignore across all rules. Patterns use Unix-style globs where `*` matches any sequence of characters. Format: `schema.object_name`, e.g., "public.my_table", "audit.*" - */ - ignore?: StringSet; - /** - * List of rules - */ - rules?: SplinterRules; + /** + * if `false`, it disables the feature and the linter won't be executed. `true` by default + */ + enabled?: boolean; + /** + * A list of glob patterns for database objects to ignore across all rules. Patterns use Unix-style globs where `*` matches any sequence of characters. Format: `schema.object_name`, e.g., "public.my_table", "audit.*" + */ + ignore?: StringSet; + /** + * List of rules + */ + rules?: SplinterRules; } /** * The configuration for type checking. */ export interface PartialTypecheckConfiguration { - /** - * if `false`, it disables the feature and the typechecker won't be executed. `true` by default - */ - enabled?: boolean; - /** - * Default search path schemas for type checking. Can be a list of schema names or glob patterns like ["public", "app_*"]. If not specified, defaults to ["public"]. - */ - searchPath?: StringSet; + /** + * if `false`, it disables the feature and the typechecker won't be executed. `true` by default + */ + enabled?: boolean; + /** + * Default search path schemas for type checking. Can be a list of schema names or glob patterns like ["public", "app_*"]. If not specified, defaults to ["public"]. + */ + searchPath?: StringSet; } /** * Set of properties to integrate with a VCS software. */ export interface PartialVcsConfiguration { - /** - * The kind of client. - */ - clientKind?: VcsClientKind; - /** - * The main branch of the project - */ - defaultBranch?: string; - /** - * Whether we should integrate itself with the VCS client - */ - enabled?: boolean; - /** + /** + * The kind of client. + */ + clientKind?: VcsClientKind; + /** + * The main branch of the project + */ + defaultBranch?: string; + /** + * Whether we should integrate itself with the VCS client + */ + enabled?: boolean; + /** * The folder where we should check for VCS files. By default, we will use the same folder where `postgres-language-server.jsonc` was found. If we can't find the configuration, it will attempt to use the current working directory. If no current working directory can't be found, we won't use the VCS integration, and a diagnostic will be emitted */ - root?: string; - /** - * Whether we should use the VCS ignore file. When [true], we will ignore the files specified in the ignore file. - */ - useIgnoreFile?: boolean; + root?: string; + /** + * Whether we should use the VCS ignore file. When [true], we will ignore the files specified in the ignore file. + */ + useIgnoreFile?: boolean; } /** * Keyword casing style for the formatter. @@ -576,450 +570,448 @@ export type KeywordCase = "upper" | "lower"; */ export type IndentStyle = "spaces" | "tabs"; export interface LinterRules { - /** - * It enables ALL rules. The rules that belong to `nursery` won't be enabled. - */ - all?: boolean; - /** - * It enables the lint rules recommended by Postgres Language Server. `true` by default. - */ - recommended?: boolean; - safety?: Safety; + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + safety?: Safety; } export interface PglinterRules { - /** - * It enables ALL rules. The rules that belong to `nursery` won't be enabled. - */ - all?: boolean; - base?: Base; - cluster?: Cluster; - /** - * It enables the lint rules recommended by Postgres Language Server. `true` by default. - */ - recommended?: boolean; - schema?: Schema; + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + base?: Base; + cluster?: Cluster; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + schema?: Schema; } export interface SplinterRules { - /** - * It enables ALL rules. The rules that belong to `nursery` won't be enabled. - */ - all?: boolean; - performance?: Performance; - /** - * It enables the lint rules recommended by Postgres Language Server. `true` by default. - */ - recommended?: boolean; - security?: Security; + /** + * It enables ALL rules. The rules that belong to `nursery` won't be enabled. + */ + all?: boolean; + performance?: Performance; + /** + * It enables the lint rules recommended by Postgres Language Server. `true` by default. + */ + recommended?: boolean; + security?: Security; } export type VcsClientKind = "git"; /** * A list of rules that belong to this group */ export interface Safety { - /** - * Adding a column with a SERIAL type or GENERATED ALWAYS AS ... STORED causes a full table rewrite. - */ - addSerialColumn?: RuleConfiguration_for_Null; - /** - * Adding a column with a DEFAULT value may lead to a table rewrite while holding an ACCESS EXCLUSIVE lock. - */ - addingFieldWithDefault?: RuleConfiguration_for_Null; - /** - * Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes. - */ - addingForeignKeyConstraint?: RuleConfiguration_for_Null; - /** - * Setting a column NOT NULL blocks reads while the table is scanned. - */ - addingNotNullField?: RuleConfiguration_for_Null; - /** - * Adding a primary key constraint results in locks and table rewrites. - */ - addingPrimaryKeyConstraint?: RuleConfiguration_for_Null; - /** - * Adding a new column that is NOT NULL and has no default value to an existing table effectively makes it required. - */ - addingRequiredField?: RuleConfiguration_for_Null; - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * Using CHAR(n) or CHARACTER(n) types is discouraged. - */ - banCharField?: RuleConfiguration_for_Null; - /** - * Concurrent index creation is not allowed within a transaction. - */ - banConcurrentIndexCreationInTransaction?: RuleConfiguration_for_Null; - /** - * Dropping a column may break existing clients. - */ - banDropColumn?: RuleConfiguration_for_Null; - /** - * Dropping a database may break existing clients (and everything else, really). - */ - banDropDatabase?: RuleConfiguration_for_Null; - /** - * Dropping a NOT NULL constraint may break existing clients. - */ - banDropNotNull?: RuleConfiguration_for_Null; - /** - * Dropping a table may break existing clients. - */ - banDropTable?: RuleConfiguration_for_Null; - /** - * Using TRUNCATE's CASCADE option will truncate any tables that are also foreign-keyed to the specified tables. - */ - banTruncateCascade?: RuleConfiguration_for_Null; - /** - * Changing a column type may break existing clients. - */ - changingColumnType?: RuleConfiguration_for_Null; - /** - * Adding constraints without NOT VALID blocks all reads and writes. - */ - constraintMissingNotValid?: RuleConfiguration_for_Null; - /** - * Creating enum types is not recommended for new applications. - */ - creatingEnum?: RuleConfiguration_for_Null; - /** - * Disallow adding a UNIQUE constraint without using an existing index. - */ - disallowUniqueConstraint?: RuleConfiguration_for_Null; - /** - * Taking a dangerous lock without setting a lock timeout can cause indefinite blocking. - */ - lockTimeoutWarning?: RuleConfiguration_for_Null; - /** - * Multiple ALTER TABLE statements on the same table should be combined into a single statement. - */ - multipleAlterTable?: RuleConfiguration_for_Null; - /** - * Prefer BIGINT over smaller integer types. - */ - preferBigInt?: RuleConfiguration_for_Null; - /** - * Prefer BIGINT over INT/INTEGER types. - */ - preferBigintOverInt?: RuleConfiguration_for_Null; - /** - * Prefer BIGINT over SMALLINT types. - */ - preferBigintOverSmallint?: RuleConfiguration_for_Null; - /** - * Prefer using IDENTITY columns over serial columns. - */ - preferIdentity?: RuleConfiguration_for_Null; - /** - * Prefer JSONB over JSON types. - */ - preferJsonb?: RuleConfiguration_for_Null; - /** - * Prefer statements with guards for robustness in migrations. - */ - preferRobustStmts?: RuleConfiguration_for_Null; - /** - * Prefer using TEXT over VARCHAR(n) types. - */ - preferTextField?: RuleConfiguration_for_Null; - /** - * Prefer TIMESTAMPTZ over TIMESTAMP types. - */ - preferTimestamptz?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * Renaming columns may break existing queries and application code. - */ - renamingColumn?: RuleConfiguration_for_Null; - /** - * Renaming tables may break existing queries and application code. - */ - renamingTable?: RuleConfiguration_for_Null; - /** - * Creating indexes non-concurrently can lock the table for writes. - */ - requireConcurrentIndexCreation?: RuleConfiguration_for_Null; - /** - * Dropping indexes non-concurrently can lock the table for reads. - */ - requireConcurrentIndexDeletion?: RuleConfiguration_for_Null; - /** - * Running additional statements while holding an ACCESS EXCLUSIVE lock blocks all table access. - */ - runningStatementWhileHoldingAccessExclusive?: RuleConfiguration_for_Null; - /** - * Detects problematic transaction nesting that could lead to unexpected behavior. - */ - transactionNesting?: RuleConfiguration_for_Null; + /** + * Adding a column with a SERIAL type or GENERATED ALWAYS AS ... STORED causes a full table rewrite. + */ + addSerialColumn?: RuleConfiguration_for_Null; + /** + * Adding a column with a DEFAULT value may lead to a table rewrite while holding an ACCESS EXCLUSIVE lock. + */ + addingFieldWithDefault?: RuleConfiguration_for_Null; + /** + * Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes. + */ + addingForeignKeyConstraint?: RuleConfiguration_for_Null; + /** + * Setting a column NOT NULL blocks reads while the table is scanned. + */ + addingNotNullField?: RuleConfiguration_for_Null; + /** + * Adding a primary key constraint results in locks and table rewrites. + */ + addingPrimaryKeyConstraint?: RuleConfiguration_for_Null; + /** + * Adding a new column that is NOT NULL and has no default value to an existing table effectively makes it required. + */ + addingRequiredField?: RuleConfiguration_for_Null; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Using CHAR(n) or CHARACTER(n) types is discouraged. + */ + banCharField?: RuleConfiguration_for_Null; + /** + * Concurrent index creation is not allowed within a transaction. + */ + banConcurrentIndexCreationInTransaction?: RuleConfiguration_for_Null; + /** + * Dropping a column may break existing clients. + */ + banDropColumn?: RuleConfiguration_for_Null; + /** + * Dropping a database may break existing clients (and everything else, really). + */ + banDropDatabase?: RuleConfiguration_for_Null; + /** + * Dropping a NOT NULL constraint may break existing clients. + */ + banDropNotNull?: RuleConfiguration_for_Null; + /** + * Dropping a table may break existing clients. + */ + banDropTable?: RuleConfiguration_for_Null; + /** + * Using TRUNCATE's CASCADE option will truncate any tables that are also foreign-keyed to the specified tables. + */ + banTruncateCascade?: RuleConfiguration_for_Null; + /** + * Changing a column type may break existing clients. + */ + changingColumnType?: RuleConfiguration_for_Null; + /** + * Adding constraints without NOT VALID blocks all reads and writes. + */ + constraintMissingNotValid?: RuleConfiguration_for_Null; + /** + * Creating enum types is not recommended for new applications. + */ + creatingEnum?: RuleConfiguration_for_Null; + /** + * Disallow adding a UNIQUE constraint without using an existing index. + */ + disallowUniqueConstraint?: RuleConfiguration_for_Null; + /** + * Taking a dangerous lock without setting a lock timeout can cause indefinite blocking. + */ + lockTimeoutWarning?: RuleConfiguration_for_Null; + /** + * Multiple ALTER TABLE statements on the same table should be combined into a single statement. + */ + multipleAlterTable?: RuleConfiguration_for_Null; + /** + * Prefer BIGINT over smaller integer types. + */ + preferBigInt?: RuleConfiguration_for_Null; + /** + * Prefer BIGINT over INT/INTEGER types. + */ + preferBigintOverInt?: RuleConfiguration_for_Null; + /** + * Prefer BIGINT over SMALLINT types. + */ + preferBigintOverSmallint?: RuleConfiguration_for_Null; + /** + * Prefer using IDENTITY columns over serial columns. + */ + preferIdentity?: RuleConfiguration_for_Null; + /** + * Prefer JSONB over JSON types. + */ + preferJsonb?: RuleConfiguration_for_Null; + /** + * Prefer statements with guards for robustness in migrations. + */ + preferRobustStmts?: RuleConfiguration_for_Null; + /** + * Prefer using TEXT over VARCHAR(n) types. + */ + preferTextField?: RuleConfiguration_for_Null; + /** + * Prefer TIMESTAMPTZ over TIMESTAMP types. + */ + preferTimestamptz?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * Renaming columns may break existing queries and application code. + */ + renamingColumn?: RuleConfiguration_for_Null; + /** + * Renaming tables may break existing queries and application code. + */ + renamingTable?: RuleConfiguration_for_Null; + /** + * Creating indexes non-concurrently can lock the table for writes. + */ + requireConcurrentIndexCreation?: RuleConfiguration_for_Null; + /** + * Dropping indexes non-concurrently can lock the table for reads. + */ + requireConcurrentIndexDeletion?: RuleConfiguration_for_Null; + /** + * Running additional statements while holding an ACCESS EXCLUSIVE lock blocks all table access. + */ + runningStatementWhileHoldingAccessExclusive?: RuleConfiguration_for_Null; + /** + * Detects problematic transaction nesting that could lead to unexpected behavior. + */ + transactionNesting?: RuleConfiguration_for_Null; } /** * A list of rules that belong to this group */ export interface Base { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns - */ - compositePrimaryKeyTooManyColumns?: RuleConfiguration_for_Null; - /** - * HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns. - */ - howManyObjectsWithUppercase?: RuleConfiguration_for_Null; - /** - * HowManyRedudantIndex (B002): Count number of redundant index vs nb index. - */ - howManyRedudantIndex?: RuleConfiguration_for_Null; - /** - * HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key. - */ - howManyTableWithoutIndexOnFk?: RuleConfiguration_for_Null; - /** - * HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key. - */ - howManyTableWithoutPrimaryKey?: RuleConfiguration_for_Null; - /** - * HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected. - */ - howManyTablesNeverSelected?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type. - */ - howManyTablesWithFkMismatch?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema. - */ - howManyTablesWithFkOutsideSchema?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names. - */ - howManyTablesWithReservedKeywords?: RuleConfiguration_for_Null; - /** - * HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers. - */ - howManyTablesWithSameTrigger?: RuleConfiguration_for_Null; - /** - * HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.) - */ - howManyUnusedIndex?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners. - */ - severalTableOwnerInSchema?: RuleConfiguration_for_Null; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * CompositePrimaryKeyTooManyColumns (B012): Detect tables with composite primary keys involving more than 4 columns + */ + compositePrimaryKeyTooManyColumns?: RuleConfiguration_for_Null; + /** + * HowManyObjectsWithUppercase (B005): Count number of objects with uppercase in name or in columns. + */ + howManyObjectsWithUppercase?: RuleConfiguration_for_Null; + /** + * HowManyRedudantIndex (B002): Count number of redundant index vs nb index. + */ + howManyRedudantIndex?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutIndexOnFk (B003): Count number of tables without index on foreign key. + */ + howManyTableWithoutIndexOnFk?: RuleConfiguration_for_Null; + /** + * HowManyTableWithoutPrimaryKey (B001): Count number of tables without primary key. + */ + howManyTableWithoutPrimaryKey?: RuleConfiguration_for_Null; + /** + * HowManyTablesNeverSelected (B006): Count number of table(s) that has never been selected. + */ + howManyTablesNeverSelected?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkMismatch (B008): Count number of tables with foreign keys that do not match the key reference type. + */ + howManyTablesWithFkMismatch?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithFkOutsideSchema (B007): Count number of tables with foreign keys outside their schema. + */ + howManyTablesWithFkOutsideSchema?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithReservedKeywords (B010): Count number of database objects using reserved keywords in their names. + */ + howManyTablesWithReservedKeywords?: RuleConfiguration_for_Null; + /** + * HowManyTablesWithSameTrigger (B009): Count number of tables using the same trigger vs nb table with their own triggers. + */ + howManyTablesWithSameTrigger?: RuleConfiguration_for_Null; + /** + * HowManyUnusedIndex (B004): Count number of unused index vs nb index (base on pg_stat_user_indexes, indexes associated to unique constraints are discard.) + */ + howManyUnusedIndex?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SeveralTableOwnerInSchema (B011): In a schema there are several tables owned by different owners. + */ + severalTableOwnerInSchema?: RuleConfiguration_for_Null; } /** * A list of rules that belong to this group */ export interface Cluster { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256. - */ - passwordEncryptionIsMd5?: RuleConfiguration_for_Null; - /** - * PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. - */ - pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists?: RuleConfiguration_for_Null; - /** - * PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. - */ - pgHbaEntriesWithMethodTrustShouldNotExists?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * PasswordEncryptionIsMd5 (C003): This configuration is not secure anymore and will prevent an upgrade to Postgres 18. Warning, you will need to reset all passwords after this is changed to scram-sha-256. + */ + passwordEncryptionIsMd5?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustOrPasswordShouldNotExists (C002): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustOrPasswordShouldNotExists?: RuleConfiguration_for_Null; + /** + * PgHbaEntriesWithMethodTrustShouldNotExists (C001): This configuration is extremely insecure and should only be used in a controlled, non-production environment for testing purposes. In a production environment, you should use more secure authentication methods such as md5, scram-sha-256, or cert, and restrict access to trusted IP addresses only. + */ + pgHbaEntriesWithMethodTrustShouldNotExists?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; } /** * A list of rules that belong to this group */ export interface Schema { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary). - */ - ownerSchemaIsInternalRole?: RuleConfiguration_for_Null; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match. - */ - schemaOwnerDoNotMatchTableOwner?: RuleConfiguration_for_Null; - /** - * SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy. - */ - schemaPrefixedOrSuffixedWithEnvt?: RuleConfiguration_for_Null; - /** - * SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it. - */ - schemaWithDefaultRoleNotGranted?: RuleConfiguration_for_Null; - /** - * UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects. - */ - unsecuredPublicSchema?: RuleConfiguration_for_Null; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * OwnerSchemaIsInternalRole (S004): Owner of schema should not be any internal pg roles, or owner is a superuser (not sure it is necesary). + */ + ownerSchemaIsInternalRole?: RuleConfiguration_for_Null; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * SchemaOwnerDoNotMatchTableOwner (S005): The schema owner and tables in the schema do not match. + */ + schemaOwnerDoNotMatchTableOwner?: RuleConfiguration_for_Null; + /** + * SchemaPrefixedOrSuffixedWithEnvt (S002): The schema is prefixed with one of staging,stg,preprod,prod,sandbox,sbox string. Means that when you refresh your preprod, staging environments from production, you have to rename the target schema from prod_ to stg_ or something like. It is possible, but it is never easy. + */ + schemaPrefixedOrSuffixedWithEnvt?: RuleConfiguration_for_Null; + /** + * SchemaWithDefaultRoleNotGranted (S001): The schema has no default role. Means that futur table will not be granted through a role. So you will have to re-execute grants on it. + */ + schemaWithDefaultRoleNotGranted?: RuleConfiguration_for_Null; + /** + * UnsecuredPublicSchema (S003): Only authorized users should be allowed to create objects. + */ + unsecuredPublicSchema?: RuleConfiguration_for_Null; } /** * A list of rules that belong to this group */ export interface Performance { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row - */ - authRlsInitplan?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Duplicate Index: Detects cases where two ore more identical indexes exist. - */ - duplicateIndex?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. - */ - multiplePermissivePolicies?: RuleConfiguration_for_SplinterRuleOptions; - /** - * No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. - */ - noPrimaryKey?: RuleConfiguration_for_SplinterRuleOptions; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. - */ - tableBloat?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance. - */ - unindexedForeignKeys?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Unused Index: Detects if an index has never been used and may be a candidate for removal. - */ - unusedIndex?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Auth RLS Initialization Plan: Detects if calls to `current_setting()` and `auth.()` in RLS policies are being unnecessarily re-evaluated for each row + */ + authRlsInitplan?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Duplicate Index: Detects cases where two ore more identical indexes exist. + */ + duplicateIndex?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Multiple Permissive Policies: Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query. + */ + multiplePermissivePolicies?: RuleConfiguration_for_SplinterRuleOptions; + /** + * No Primary Key: Detects if a table does not have a primary key. Tables without a primary key can be inefficient to interact with at scale. + */ + noPrimaryKey?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * Table Bloat: Detects if a table has excess bloat and may benefit from maintenance operations like vacuum full or cluster. + */ + tableBloat?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unindexed foreign keys: Identifies foreign key constraints without a covering index, which can impact database performance. + */ + unindexedForeignKeys?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unused Index: Detects if an index has never been used and may be a candidate for removal. + */ + unusedIndex?: RuleConfiguration_for_SplinterRuleOptions; } /** * A list of rules that belong to this group */ export interface Security { - /** - * It enables ALL rules for this group. - */ - all?: boolean; - /** - * Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. - */ - authUsersExposed?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Extension in Public: Detects extensions installed in the `public` schema. - */ - extensionInPublic?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Extension Versions Outdated: Detects extensions that are not using the default (recommended) version. - */ - extensionVersionsOutdated?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema. - */ - fkeyToAuthUnique?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. - */ - foreignTableInApi?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Function Search Path Mutable: Detects functions where the search_path parameter is not set. - */ - functionSearchPathMutable?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs - */ - insecureQueueExposedInApi?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Materialized View in API: Detects materialized views that are accessible over the Data APIs. - */ - materializedViewInApi?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. - */ - policyExistsRlsDisabled?: RuleConfiguration_for_SplinterRuleOptions; - /** - * It enables the recommended rules for this group - */ - recommended?: boolean; - /** - * RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST - */ - rlsDisabledInPublic?: RuleConfiguration_for_SplinterRuleOptions; - /** - * RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. - */ - rlsEnabledNoPolicy?: RuleConfiguration_for_SplinterRuleOptions; - /** - * RLS Policy Always True: Detects RLS policies that use overly permissive expressions like USING (true) or WITH CHECK (true) for UPDATE, DELETE, or INSERT operations. SELECT policies with USING (true) are intentionally excluded as this pattern is often used deliberately for public read access. - */ - rlsPolicyAlwaysTrue?: RuleConfiguration_for_SplinterRuleOptions; - /** - * RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. - */ - rlsReferencesUserMetadata?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user - */ - securityDefinerView?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Sensitive Columns Exposed: Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection. - */ - sensitiveColumnsExposed?: RuleConfiguration_for_SplinterRuleOptions; - /** - * Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. - */ - unsupportedRegTypes?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables ALL rules for this group. + */ + all?: boolean; + /** + * Exposed Auth Users: Detects if auth.users is exposed to anon or authenticated roles via a view or materialized view in schemas exposed to PostgREST, potentially compromising user data security. + */ + authUsersExposed?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension in Public: Detects extensions installed in the `public` schema. + */ + extensionInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Extension Versions Outdated: Detects extensions that are not using the default (recommended) version. + */ + extensionVersionsOutdated?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Key to Auth Unique Constraint: Detects user defined foreign keys to unique constraints in the auth schema. + */ + fkeyToAuthUnique?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Foreign Table in API: Detects foreign tables that are accessible over APIs. Foreign tables do not respect row level security policies. + */ + foreignTableInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Function Search Path Mutable: Detects functions where the search_path parameter is not set. + */ + functionSearchPathMutable?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Insecure Queue Exposed in API: Detects cases where an insecure Queue is exposed over Data APIs + */ + insecureQueueExposedInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Materialized View in API: Detects materialized views that are accessible over the Data APIs. + */ + materializedViewInApi?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Policy Exists RLS Disabled: Detects cases where row level security (RLS) policies have been created, but RLS has not been enabled for the underlying table. + */ + policyExistsRlsDisabled?: RuleConfiguration_for_SplinterRuleOptions; + /** + * It enables the recommended rules for this group + */ + recommended?: boolean; + /** + * RLS Disabled in Public: Detects cases where row level security (RLS) has not been enabled on tables in schemas exposed to PostgREST + */ + rlsDisabledInPublic?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS Enabled No Policy: Detects cases where row level security (RLS) has been enabled on a table but no RLS policies have been created. + */ + rlsEnabledNoPolicy?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS Policy Always True: Detects RLS policies that use overly permissive expressions like USING (true) or WITH CHECK (true) for UPDATE, DELETE, or INSERT operations. SELECT policies with USING (true) are intentionally excluded as this pattern is often used deliberately for public read access. + */ + rlsPolicyAlwaysTrue?: RuleConfiguration_for_SplinterRuleOptions; + /** + * RLS references user metadata: Detects when Supabase Auth user_metadata is referenced insecurely in a row level security (RLS) policy. + */ + rlsReferencesUserMetadata?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Security Definer View: Detects views defined with the SECURITY DEFINER property. These views enforce Postgres permissions and row level security policies (RLS) of the view creator, rather than that of the querying user + */ + securityDefinerView?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Sensitive Columns Exposed: Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection. + */ + sensitiveColumnsExposed?: RuleConfiguration_for_SplinterRuleOptions; + /** + * Unsupported reg types: Identifies columns using unsupported reg* types outside pg_catalog schema, which prevents database upgrades using pg_upgrade. + */ + unsupportedRegTypes?: RuleConfiguration_for_SplinterRuleOptions; } -export type RuleConfiguration_for_Null = - | RulePlainConfiguration - | RuleWithOptions_for_Null; +export type RuleConfiguration_for_Null = RulePlainConfiguration | RuleWithOptions_for_Null; export type RuleConfiguration_for_SplinterRuleOptions = - | RulePlainConfiguration - | RuleWithOptions_for_SplinterRuleOptions; + | RulePlainConfiguration + | RuleWithOptions_for_SplinterRuleOptions; export type RulePlainConfiguration = "warn" | "error" | "info" | "off"; export interface RuleWithOptions_for_Null { - /** - * The severity of the emitted diagnostics by the rule - */ - level: RulePlainConfiguration; - /** - * Rule's options - */ - options: null; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: null; } export interface RuleWithOptions_for_SplinterRuleOptions { - /** - * The severity of the emitted diagnostics by the rule - */ - level: RulePlainConfiguration; - /** - * Rule's options - */ - options: SplinterRuleOptions; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: SplinterRuleOptions; } /** * Shared options for all splinter rules. @@ -1027,76 +1019,72 @@ export interface RuleWithOptions_for_SplinterRuleOptions { These options allow configuring per-rule filtering of database objects. */ export interface SplinterRuleOptions { - /** + /** * A list of glob patterns for database objects to ignore. Patterns use Unix-style globs where: - `*` matches any sequence of characters - `?` matches any single character Each pattern should be in the format `schema.object_name`, for example: - `"public.my_table"` - ignores a specific table - `"audit.*"` - ignores all objects in the audit schema - `"*.audit_*"` - ignores objects with audit_ prefix in any schema */ - ignore?: string[]; + ignore?: string[]; } export interface OpenFileParams { - content: string; - path: PgLSPath; - version: number; + content: string; + path: PgLSPath; + version: number; } export interface ChangeFileParams { - content: string; - path: PgLSPath; - version: number; + content: string; + path: PgLSPath; + version: number; } export interface CloseFileParams { - path: PgLSPath; + path: PgLSPath; } export type Configuration = PartialConfiguration; export interface Workspace { - isPathIgnored(params: IsPathIgnoredParams): Promise; - registerProjectFolder( - params: RegisterProjectFolderParams, - ): Promise; - getFileContent(params: GetFileContentParams): Promise; - pullFileDiagnostics( - params: PullFileDiagnosticsParams, - ): Promise; - getCompletions(params: GetCompletionsParams): Promise; - updateSettings(params: UpdateSettingsParams): Promise; - openFile(params: OpenFileParams): Promise; - changeFile(params: ChangeFileParams): Promise; - closeFile(params: CloseFileParams): Promise; - destroy(): void; + isPathIgnored(params: IsPathIgnoredParams): Promise; + registerProjectFolder(params: RegisterProjectFolderParams): Promise; + getFileContent(params: GetFileContentParams): Promise; + pullFileDiagnostics(params: PullFileDiagnosticsParams): Promise; + getCompletions(params: GetCompletionsParams): Promise; + updateSettings(params: UpdateSettingsParams): Promise; + openFile(params: OpenFileParams): Promise; + changeFile(params: ChangeFileParams): Promise; + closeFile(params: CloseFileParams): Promise; + destroy(): void; } export function createWorkspace(transport: Transport): Workspace { - return { - isPathIgnored(params) { - return transport.request("pgls/is_path_ignored", params); - }, - registerProjectFolder(params) { - return transport.request("pgls/register_project_folder", params); - }, - getFileContent(params) { - return transport.request("pgls/get_file_content", params); - }, - pullFileDiagnostics(params) { - return transport.request("pgls/pull_file_diagnostics", params); - }, - getCompletions(params) { - return transport.request("pgls/get_completions", params); - }, - updateSettings(params) { - return transport.request("pgls/update_settings", params); - }, - openFile(params) { - return transport.request("pgls/open_file", params); - }, - changeFile(params) { - return transport.request("pgls/change_file", params); - }, - closeFile(params) { - return transport.request("pgls/close_file", params); - }, - destroy() { - transport.destroy(); - }, - }; + return { + isPathIgnored(params) { + return transport.request("pgls/is_path_ignored", params); + }, + registerProjectFolder(params) { + return transport.request("pgls/register_project_folder", params); + }, + getFileContent(params) { + return transport.request("pgls/get_file_content", params); + }, + pullFileDiagnostics(params) { + return transport.request("pgls/pull_file_diagnostics", params); + }, + getCompletions(params) { + return transport.request("pgls/get_completions", params); + }, + updateSettings(params) { + return transport.request("pgls/update_settings", params); + }, + openFile(params) { + return transport.request("pgls/open_file", params); + }, + changeFile(params) { + return transport.request("pgls/change_file", params); + }, + closeFile(params) { + return transport.request("pgls/close_file", params); + }, + destroy() { + transport.destroy(); + }, + }; } diff --git a/packages/@postgrestools/backend-jsonrpc/tests/transport.test.mjs b/packages/@postgrestools/backend-jsonrpc/tests/transport.test.mjs index 32a103eea..91df2e128 100644 --- a/packages/@postgrestools/backend-jsonrpc/tests/transport.test.mjs +++ b/packages/@postgrestools/backend-jsonrpc/tests/transport.test.mjs @@ -3,158 +3,154 @@ import { describe, expect, it, mock } from "bun:test"; import { Transport } from "../src/transport"; function makeMessage(body) { - const content = JSON.stringify(body); - return Buffer.from( - `Content-Length: ${content.length}\r\nContent-Type: application/vscode-jsonrpc;charset=utf-8\r\n\r\n${content}`, - ); + const content = JSON.stringify(body); + return Buffer.from( + `Content-Length: ${content.length}\r\nContent-Type: application/vscode-jsonrpc;charset=utf-8\r\n\r\n${content}`, + ); } describe("Transport Layer", () => { - it("should encode requests into the socket", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - const result = transport.request("method", "params"); - - expect(socket.write).toHaveBeenCalledWith( - makeMessage({ - jsonrpc: "2.0", - id: 0, - method: "method", - params: "params", - }), - ); - - onData( - makeMessage({ - jsonrpc: "2.0", - id: 0, - result: "result", - }), - ); - - const response = await result; - expect(response).toBe("result"); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on missing Content-Length headers", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => onData(Buffer.from("\r\n"))).toThrowError( - "incoming message from the remote workspace is missing the Content-Length header", - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on missing colon token", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => onData(Buffer.from("Content-Length\r\n"))).toThrowError( - 'could not find colon token in "Content-Length\r\n"', - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on invalid Content-Type", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => - onData(Buffer.from("Content-Type: text/plain\r\n")), - ).toThrowError( - 'invalid value for Content-Type expected "application/vscode-jsonrpc", got "text/plain"', - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on unknown request ID", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => - onData(makeMessage({ jsonrpc: "2.0", id: 0, result: "result" })), - ).toThrowError( - "could not find any pending request matching RPC response ID 0", - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); - - it("should throw on invalid messages", async () => { - let onData = null; - const socket = { - on(event, fn) { - expect(event).toBe("data"); - onData = fn; - }, - write: mock(), - destroy: mock(), - }; - - const transport = new Transport(socket); - - expect(() => onData(makeMessage({}))).toThrowError( - 'failed to deserialize incoming message from remote workspace, "{}" is not a valid JSON-RPC message body', - ); - - transport.destroy(); - expect(socket.destroy).toHaveBeenCalledOnce(); - }); + it("should encode requests into the socket", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + const result = transport.request("method", "params"); + + expect(socket.write).toHaveBeenCalledWith( + makeMessage({ + jsonrpc: "2.0", + id: 0, + method: "method", + params: "params", + }), + ); + + onData( + makeMessage({ + jsonrpc: "2.0", + id: 0, + result: "result", + }), + ); + + const response = await result; + expect(response).toBe("result"); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on missing Content-Length headers", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(Buffer.from("\r\n"))).toThrowError( + "incoming message from the remote workspace is missing the Content-Length header", + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on missing colon token", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(Buffer.from("Content-Length\r\n"))).toThrowError( + 'could not find colon token in "Content-Length\r\n"', + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on invalid Content-Type", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(Buffer.from("Content-Type: text/plain\r\n"))).toThrowError( + 'invalid value for Content-Type expected "application/vscode-jsonrpc", got "text/plain"', + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on unknown request ID", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(makeMessage({ jsonrpc: "2.0", id: 0, result: "result" }))).toThrowError( + "could not find any pending request matching RPC response ID 0", + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); + + it("should throw on invalid messages", async () => { + let onData = null; + const socket = { + on(event, fn) { + expect(event).toBe("data"); + onData = fn; + }, + write: mock(), + destroy: mock(), + }; + + const transport = new Transport(socket); + + expect(() => onData(makeMessage({}))).toThrowError( + 'failed to deserialize incoming message from remote workspace, "{}" is not a valid JSON-RPC message body', + ); + + transport.destroy(); + expect(socket.destroy).toHaveBeenCalledOnce(); + }); }); diff --git a/packages/@postgrestools/backend-jsonrpc/tests/workspace.test.mjs b/packages/@postgrestools/backend-jsonrpc/tests/workspace.test.mjs index 3c32d574c..1f816e41c 100644 --- a/packages/@postgrestools/backend-jsonrpc/tests/workspace.test.mjs +++ b/packages/@postgrestools/backend-jsonrpc/tests/workspace.test.mjs @@ -1,57 +1,56 @@ import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; + import { describe, expect, it } from "vitest"; import { createWorkspaceWithBinary } from "../src"; describe("Workspace API", () => { - it("should process remote requests", async () => { - const extension = process.platform === "win32" ? ".exe" : ""; - const command = resolve( - fileURLToPath(import.meta.url), - "../../../../..", - `target/release/postgres-language-server${extension}`, - ); - - const workspace = await createWorkspaceWithBinary(command); - workspace.registerProjectFolder({ - setAsCurrentWorkspace: true, - }); - await workspace.openFile({ - path: { - path: "test.sql", - was_written: false, - kind: ["Handleable"], - }, - content: "select 1 from", - version: 0, - }); - - const { diagnostics } = await workspace.pullFileDiagnostics({ - only: [], - skip: [], - max_diagnostics: 100, - categories: [], - path: { - path: "test.sql", - was_written: false, - kind: ["Handleable"], - }, - }); - - expect(diagnostics).toHaveLength(1); - expect(diagnostics[0].description).toBe( - "Invalid statement: syntax error at end of input", - ); - - await workspace.closeFile({ - path: { - path: "test.sql", - was_written: false, - kind: ["Handleable"], - }, - }); - - workspace.destroy(); - }); + it("should process remote requests", async () => { + const extension = process.platform === "win32" ? ".exe" : ""; + const command = resolve( + fileURLToPath(import.meta.url), + "../../../../..", + `target/release/postgres-language-server${extension}`, + ); + + const workspace = await createWorkspaceWithBinary(command); + workspace.registerProjectFolder({ + setAsCurrentWorkspace: true, + }); + await workspace.openFile({ + path: { + path: "test.sql", + was_written: false, + kind: ["Handleable"], + }, + content: "select 1 from", + version: 0, + }); + + const { diagnostics } = await workspace.pullFileDiagnostics({ + only: [], + skip: [], + max_diagnostics: 100, + categories: [], + path: { + path: "test.sql", + was_written: false, + kind: ["Handleable"], + }, + }); + + expect(diagnostics).toHaveLength(1); + expect(diagnostics[0].description).toBe("Invalid statement: syntax error at end of input"); + + await workspace.closeFile({ + path: { + path: "test.sql", + was_written: false, + kind: ["Handleable"], + }, + }); + + workspace.destroy(); + }); }); diff --git a/packages/@postgrestools/postgrestools/package.json b/packages/@postgrestools/postgrestools/package.json index df94730c3..0d6af15ba 100644 --- a/packages/@postgrestools/postgrestools/package.json +++ b/packages/@postgrestools/postgrestools/package.json @@ -1,44 +1,48 @@ { - "name": "@postgrestools/postgrestools", - "version": "", - "bin": { - "postgrestools": "bin/postgrestools" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/supabase-community/postgres-language-server.git", - "directory": "packages/@postgrestools/postgrestools" - }, - "author": "Supabase Community", - "contributors": [ - { - "name": "Philipp Steinrötter", - "url": "https://github.com/psteinroe" - }, - { - "name": "Julian Domke", - "url": "https://github.com/juleswritescode" - } - ], - "license": "MIT or Apache-2.0", - "description": "A collection of language tools and a Language Server Protocol (LSP) implementation for Postgres, focusing on developer experience and reliable SQL tooling.", - "files": ["bin/postgrestools", "schema.json", "README.md"], - "engines": { - "node": ">=20" - }, - "publishConfig": { - "provenance": true - }, - "optionalDependencies": { - "@postgrestools/cli-x86_64-windows-msvc": "", - "@postgrestools/cli-aarch64-windows-msvc": "", - "@postgrestools/cli-x86_64-apple-darwin": "", - "@postgrestools/cli-aarch64-apple-darwin": "", - "@postgrestools/cli-x86_64-linux-gnu": "", - "@postgrestools/cli-aarch64-linux-gnu": "", - "@postgrestools/cli-x86_64-linux-musl": "" - }, - "scripts": { - "test": "bun test" - } + "name": "@postgrestools/postgrestools", + "version": "", + "description": "A collection of language tools and a Language Server Protocol (LSP) implementation for Postgres, focusing on developer experience and reliable SQL tooling.", + "license": "MIT or Apache-2.0", + "author": "Supabase Community", + "contributors": [ + { + "name": "Philipp Steinrötter", + "url": "https://github.com/psteinroe" + }, + { + "name": "Julian Domke", + "url": "https://github.com/juleswritescode" + } + ], + "repository": { + "type": "git", + "url": "git+https://github.com/supabase-community/postgres-language-server.git", + "directory": "packages/@postgrestools/postgrestools" + }, + "bin": { + "postgrestools": "bin/postgrestools" + }, + "files": [ + "bin/postgrestools", + "schema.json", + "README.md" + ], + "publishConfig": { + "provenance": true + }, + "scripts": { + "test": "bun test" + }, + "optionalDependencies": { + "@postgrestools/cli-aarch64-apple-darwin": "", + "@postgrestools/cli-aarch64-linux-gnu": "", + "@postgrestools/cli-aarch64-windows-msvc": "", + "@postgrestools/cli-x86_64-apple-darwin": "", + "@postgrestools/cli-x86_64-linux-gnu": "", + "@postgrestools/cli-x86_64-linux-musl": "", + "@postgrestools/cli-x86_64-windows-msvc": "" + }, + "engines": { + "node": ">=20" + } } diff --git a/packages/@postgrestools/postgrestools/scripts/generate-packages.mjs b/packages/@postgrestools/postgrestools/scripts/generate-packages.mjs index 004206e29..0fd1a50af 100644 --- a/packages/@postgrestools/postgrestools/scripts/generate-packages.mjs +++ b/packages/@postgrestools/postgrestools/scripts/generate-packages.mjs @@ -12,295 +12,283 @@ const POSTGRESTOOLS_ROOT = resolve(PACKAGES_POSTGRESTOOLS_ROOT, "../.."); const MANIFEST_PATH = resolve(CLI_ROOT, "package.json"); function platformArchCombinations() { - const SUPPORTED_PLATFORMS = [ - "pc-windows-msvc", - "apple-darwin", - "unknown-linux-gnu", - "unknown-linux-musl", - ]; - - const SUPPORTED_ARCHITECTURES = ["x86_64", "aarch64"]; - - return SUPPORTED_PLATFORMS.flatMap((platform) => { - return SUPPORTED_ARCHITECTURES.flatMap((arch) => { - // we do not support MUSL builds on aarch64, as this would - // require difficult cross compilation and most aarch64 users should - // have sufficiently modern glibc versions - if (platform.endsWith("musl") && arch === "aarch64") { - return []; - } - - return { - platform, - arch, - }; - }); - }); + const SUPPORTED_PLATFORMS = [ + "pc-windows-msvc", + "apple-darwin", + "unknown-linux-gnu", + "unknown-linux-musl", + ]; + + const SUPPORTED_ARCHITECTURES = ["x86_64", "aarch64"]; + + return SUPPORTED_PLATFORMS.flatMap((platform) => { + return SUPPORTED_ARCHITECTURES.flatMap((arch) => { + // we do not support MUSL builds on aarch64, as this would + // require difficult cross compilation and most aarch64 users should + // have sufficiently modern glibc versions + if (platform.endsWith("musl") && arch === "aarch64") { + return []; + } + + return { + platform, + arch, + }; + }); + }); } async function downloadSchema(releaseTag, githubToken) { - const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/schema.json`; + const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/schema.json`; - const response = await fetch(assetUrl.trim(), { - headers: { - Authorization: `token ${githubToken}`, - Accept: "application/octet-stream", - }, - }); + const response = await fetch(assetUrl.trim(), { + headers: { + Authorization: `token ${githubToken}`, + Accept: "application/octet-stream", + }, + }); - if (!response.ok) { - throw new Error(`Failed to Fetch Asset from ${assetUrl}`); - } + if (!response.ok) { + throw new Error(`Failed to Fetch Asset from ${assetUrl}`); + } - // download to root. - const fileStream = fs.createWriteStream( - resolve(POSTGRESTOOLS_ROOT, "schema.json"), - ); + // download to root. + const fileStream = fs.createWriteStream(resolve(POSTGRESTOOLS_ROOT, "schema.json")); - await streamPipeline(response.body, fileStream); + await streamPipeline(response.body, fileStream); - console.log(`Downloaded schema for ${releaseTag}`); + console.log(`Downloaded schema for ${releaseTag}`); } async function downloadBinary(platform, arch, os, releaseTag, githubToken) { - const buildName = getBuildName(platform, arch); - const ext = getBinaryExt(os); + const buildName = getBuildName(platform, arch); + const ext = getBinaryExt(os); - const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/${buildName}${ext}`; + const assetUrl = `https://github.com/supabase-community/postgres-language-server/releases/download/${releaseTag}/${buildName}${ext}`; - const response = await fetch(assetUrl.trim(), { - headers: { - Authorization: `token ${githubToken}`, - Accept: "application/octet-stream", - }, - }); + const response = await fetch(assetUrl.trim(), { + headers: { + Authorization: `token ${githubToken}`, + Accept: "application/octet-stream", + }, + }); - if (!response.ok) { - const error = await response.text(); - throw new Error( - `Failed to Fetch Asset from ${assetUrl} (Reason: ${error})`, - ); - } + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to Fetch Asset from ${assetUrl} (Reason: ${error})`); + } - // just download to root. - const fileStream = fs.createWriteStream(getBinarySource(platform, arch, os)); + // just download to root. + const fileStream = fs.createWriteStream(getBinarySource(platform, arch, os)); - await streamPipeline(response.body, fileStream); + await streamPipeline(response.body, fileStream); - console.log(`Downloaded asset for ${buildName} (v${releaseTag})`); + console.log(`Downloaded asset for ${buildName} (v${releaseTag})`); } async function writeManifest(packagePath, version) { - const manifestPath = resolve( - PACKAGES_POSTGRESTOOLS_ROOT, - packagePath, - "package.json", - ); - - const manifestData = JSON.parse( - fs.readFileSync(manifestPath).toString("utf-8"), - ); - - const nativePackages = platformArchCombinations().map( - ({ platform, arch }) => [getPackageName(platform, arch), version], - ); - - manifestData.version = version; - manifestData.optionalDependencies = Object.fromEntries(nativePackages); - - console.log(`Update manifest ${manifestPath}`); - const content = JSON.stringify(manifestData, null, 2); - - /** - * writeFileSync seemed to not work reliably? - */ - await new Promise((res, rej) => { - fs.writeFile(manifestPath, content, (e) => (e ? rej(e) : res())); - }); + const manifestPath = resolve(PACKAGES_POSTGRESTOOLS_ROOT, packagePath, "package.json"); + + const manifestData = JSON.parse(fs.readFileSync(manifestPath).toString("utf-8")); + + const nativePackages = platformArchCombinations().map(({ platform, arch }) => [ + getPackageName(platform, arch), + version, + ]); + + manifestData.version = version; + manifestData.optionalDependencies = Object.fromEntries(nativePackages); + + console.log(`Update manifest ${manifestPath}`); + const content = JSON.stringify(manifestData, null, 2); + + /** + * writeFileSync seemed to not work reliably? + */ + await new Promise((res, rej) => { + fs.writeFile(manifestPath, content, (e) => (e ? rej(e) : res())); + }); } async function makePackageDir(platform, arch) { - const buildName = getBuildName(platform, arch); - const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, buildName); + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, buildName); - await new Promise((res, rej) => { - fs.mkdir(packageRoot, {}, (e) => (e ? rej(e) : res())); - }); + await new Promise((res, rej) => { + fs.mkdir(packageRoot, {}, (e) => (e ? rej(e) : res())); + }); } function copyBinaryToNativePackage(platform, arch, os) { - // Update the package.json manifest - const buildName = getBuildName(platform, arch); - const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, buildName); - const packageName = getPackageName(platform, arch); - - const { version, license, repository, engines } = rootManifest(); - - /** - * We need to map rust triplets to NPM-known values. - * Otherwise, npm will abort the package installation. - */ - const npm_arch = arch === "aarch64" ? "arm64" : "x64"; - let libc = undefined; - let npm_os = undefined; - - switch (os) { - case "linux": { - libc = platform.endsWith("musl") ? "musl" : "gnu"; - npm_os = "linux"; - break; - } - case "windows": { - libc = "msvc"; - npm_os = "win32"; - break; - } - case "darwin": { - libc = undefined; - npm_os = "darwin"; - break; - } - default: { - throw new Error(`Unsupported os: ${os}`); - } - } - - const manifest = JSON.stringify( - { - name: packageName, - version, - license, - repository, - engines, - os: [npm_os], - cpu: [npm_arch], - libc, - }, - null, - 2, - ); - - const ext = getBinaryExt(os); - const manifestPath = resolve(packageRoot, "package.json"); - console.info(`Update manifest ${manifestPath}`); - fs.writeFileSync(manifestPath, manifest); - - // Copy the CLI binary - const binarySource = getBinarySource(platform, arch, os); - const binaryTarget = resolve(packageRoot, `postgrestools${ext}`); - - if (!fs.existsSync(binarySource)) { - console.error( - `Source for binary for ${buildName} not found at: ${binarySource}`, - ); - process.exit(1); - } - - console.info(`Copy binary ${binaryTarget}`); - fs.copyFileSync(binarySource, binaryTarget); - fs.chmodSync(binaryTarget, 0o755); + // Update the package.json manifest + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, buildName); + const packageName = getPackageName(platform, arch); + + const { version, license, repository, engines } = rootManifest(); + + /** + * We need to map rust triplets to NPM-known values. + * Otherwise, npm will abort the package installation. + */ + const npm_arch = arch === "aarch64" ? "arm64" : "x64"; + let libc = undefined; + let npm_os = undefined; + + switch (os) { + case "linux": { + libc = platform.endsWith("musl") ? "musl" : "gnu"; + npm_os = "linux"; + break; + } + case "windows": { + libc = "msvc"; + npm_os = "win32"; + break; + } + case "darwin": { + libc = undefined; + npm_os = "darwin"; + break; + } + default: { + throw new Error(`Unsupported os: ${os}`); + } + } + + const manifest = JSON.stringify( + { + name: packageName, + version, + license, + repository, + engines, + os: [npm_os], + cpu: [npm_arch], + libc, + }, + null, + 2, + ); + + const ext = getBinaryExt(os); + const manifestPath = resolve(packageRoot, "package.json"); + console.info(`Update manifest ${manifestPath}`); + fs.writeFileSync(manifestPath, manifest); + + // Copy the CLI binary + const binarySource = getBinarySource(platform, arch, os); + const binaryTarget = resolve(packageRoot, `postgrestools${ext}`); + + if (!fs.existsSync(binarySource)) { + console.error(`Source for binary for ${buildName} not found at: ${binarySource}`); + process.exit(1); + } + + console.info(`Copy binary ${binaryTarget}`); + fs.copyFileSync(binarySource, binaryTarget); + fs.chmodSync(binaryTarget, 0o755); } function copySchemaToNativePackage(platform, arch) { - const buildName = getBuildName(platform, arch); - const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, buildName); + const buildName = getBuildName(platform, arch); + const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, buildName); - const schemaSrc = resolve(POSTGRESTOOLS_ROOT, "schema.json"); - const schemaTarget = resolve(packageRoot, "schema.json"); + const schemaSrc = resolve(POSTGRESTOOLS_ROOT, "schema.json"); + const schemaTarget = resolve(packageRoot, "schema.json"); - if (!fs.existsSync(schemaSrc)) { - console.error(`schema.json not found at: ${schemaSrc}`); - process.exit(1); - } + if (!fs.existsSync(schemaSrc)) { + console.error(`schema.json not found at: ${schemaSrc}`); + process.exit(1); + } - console.info("Copying schema.json"); - fs.copyFileSync(schemaSrc, schemaTarget); - fs.chmodSync(schemaTarget, 0o666); + console.info("Copying schema.json"); + fs.copyFileSync(schemaSrc, schemaTarget); + fs.chmodSync(schemaTarget, 0o666); } function copyReadmeToPackage(packagePath) { - const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, packagePath); - const readmeSrc = resolve(POSTGRESTOOLS_ROOT, "README.md"); - const readmeTarget = resolve(packageRoot, "README.md"); + const packageRoot = resolve(PACKAGES_POSTGRESTOOLS_ROOT, packagePath); + const readmeSrc = resolve(POSTGRESTOOLS_ROOT, "README.md"); + const readmeTarget = resolve(packageRoot, "README.md"); - if (!fs.existsSync(readmeSrc)) { - console.error(`README.md not found at: ${readmeSrc}`); - process.exit(1); - } + if (!fs.existsSync(readmeSrc)) { + console.error(`README.md not found at: ${readmeSrc}`); + process.exit(1); + } - console.info(`Copying README.md to ${packagePath}`); + console.info(`Copying README.md to ${packagePath}`); - // Read the original README content - const originalReadme = fs.readFileSync(readmeSrc, "utf-8"); + // Read the original README content + const originalReadme = fs.readFileSync(readmeSrc, "utf-8"); - // Add deprecation notice for @postgrestools packages - const deprecationNotice = `> [!WARNING] + // Add deprecation notice for @postgrestools packages + const deprecationNotice = `> [!WARNING] > **This package is deprecated.** Please use [\`@postgres-language-server/cli\`](https://www.npmjs.com/package/@postgres-language-server/cli) instead. > > The \`@postgrestools\` namespace is being phased out in favor of \`@postgres-language-server\`. All future updates and development will happen in the new package. `; - const modifiedReadme = deprecationNotice + originalReadme; + const modifiedReadme = deprecationNotice + originalReadme; - fs.writeFileSync(readmeTarget, modifiedReadme, "utf-8"); - fs.chmodSync(readmeTarget, 0o666); + fs.writeFileSync(readmeTarget, modifiedReadme, "utf-8"); + fs.chmodSync(readmeTarget, 0o666); } -const rootManifest = () => - JSON.parse(fs.readFileSync(MANIFEST_PATH).toString("utf-8")); +const rootManifest = () => JSON.parse(fs.readFileSync(MANIFEST_PATH).toString("utf-8")); function getBinaryExt(os) { - return os === "windows" ? ".exe" : ""; + return os === "windows" ? ".exe" : ""; } function getBinarySource(platform, arch, os) { - const ext = getBinaryExt(os); - return resolve(POSTGRESTOOLS_ROOT, `${getBuildName(platform, arch)}${ext}`); + const ext = getBinaryExt(os); + return resolve(POSTGRESTOOLS_ROOT, `${getBuildName(platform, arch)}${ext}`); } function getBuildName(platform, arch) { - return `postgrestools_${arch}-${platform}`; + return `postgrestools_${arch}-${platform}`; } function getPackageName(platform, arch) { - // trim the "unknown" from linux and the "pc" from windows - const platformName = platform.split("-").slice(-2).join("-"); - return `@postgrestools/cli-${arch}-${platformName}`; + // trim the "unknown" from linux and the "pc" from windows + const platformName = platform.split("-").slice(-2).join("-"); + return `@postgrestools/cli-${arch}-${platformName}`; } function getOs(platform) { - return platform.split("-").find((_, idx) => idx === 1); + return platform.split("-").find((_, idx) => idx === 1); } function getVersion(releaseTag, isPrerelease) { - return releaseTag + (isPrerelease ? "-rc" : ""); + return releaseTag + (isPrerelease ? "-rc" : ""); } (async function main() { - const githubToken = process.env.GITHUB_TOKEN; - const releaseTag = process.env.RELEASE_TAG; - assert(githubToken, "GITHUB_TOKEN not defined!"); - assert(releaseTag, "RELEASE_TAG not defined!"); - - const isPrerelease = process.env.PRERELEASE === "true"; - - await downloadSchema(releaseTag, githubToken); - const version = getVersion(releaseTag, isPrerelease); - await writeManifest("postgrestools", version); - await writeManifest("backend-jsonrpc", version); - - // Copy README to main packages - copyReadmeToPackage("postgrestools"); - copyReadmeToPackage("backend-jsonrpc"); - - for (const { platform, arch } of platformArchCombinations()) { - const os = getOs(platform); - await makePackageDir(platform, arch); - await downloadBinary(platform, arch, os, releaseTag, githubToken); - copyBinaryToNativePackage(platform, arch, os); - copySchemaToNativePackage(platform, arch); - } - - process.exit(0); + const githubToken = process.env.GITHUB_TOKEN; + const releaseTag = process.env.RELEASE_TAG; + assert(githubToken, "GITHUB_TOKEN not defined!"); + assert(releaseTag, "RELEASE_TAG not defined!"); + + const isPrerelease = process.env.PRERELEASE === "true"; + + await downloadSchema(releaseTag, githubToken); + const version = getVersion(releaseTag, isPrerelease); + await writeManifest("postgrestools", version); + await writeManifest("backend-jsonrpc", version); + + // Copy README to main packages + copyReadmeToPackage("postgrestools"); + copyReadmeToPackage("backend-jsonrpc"); + + for (const { platform, arch } of platformArchCombinations()) { + const os = getOs(platform); + await makePackageDir(platform, arch); + await downloadBinary(platform, arch, os, releaseTag, githubToken); + copyBinaryToNativePackage(platform, arch, os); + copySchemaToNativePackage(platform, arch); + } + + process.exit(0); })(); diff --git a/packages/@postgrestools/postgrestools/test/bin.test.js b/packages/@postgrestools/postgrestools/test/bin.test.js index 23335a0f8..0e1361615 100644 --- a/packages/@postgrestools/postgrestools/test/bin.test.js +++ b/packages/@postgrestools/postgrestools/test/bin.test.js @@ -8,54 +8,54 @@ const binPath = join(__dirname, "../bin/postgrestools"); const testSqlPath = join(__dirname, "test.sql"); describe("postgrestools bin", () => { - it("should check a SQL file successfully", async () => { - const result = await new Promise((resolve) => { - const proc = spawn("node", [binPath, "check", testSqlPath], { - env: { ...process.env }, - }); - - let stdout = ""; - let stderr = ""; - - proc.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - proc.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - proc.on("close", (code) => { - resolve({ code, stdout, stderr }); - }); - }); - - expect(result.code).toBe(0); - expect(result.stderr).toBe(""); - }); - - it("should fail when file doesn't exist", async () => { - const result = await new Promise((resolve) => { - const proc = spawn("node", [binPath, "check", "nonexistent.sql"], { - env: { ...process.env }, - }); - - let stdout = ""; - let stderr = ""; - - proc.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - proc.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - proc.on("close", (code) => { - resolve({ code, stdout, stderr }); - }); - }); - - expect(result.code).not.toBe(0); - }); + it("should check a SQL file successfully", async () => { + const result = await new Promise((resolve) => { + const proc = spawn("node", [binPath, "check", testSqlPath], { + env: { ...process.env }, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + resolve({ code, stdout, stderr }); + }); + }); + + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + }); + + it("should fail when file doesn't exist", async () => { + const result = await new Promise((resolve) => { + const proc = spawn("node", [binPath, "check", "nonexistent.sql"], { + env: { ...process.env }, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + resolve({ code, stdout, stderr }); + }); + }); + + expect(result.code).not.toBe(0); + }); });