diff --git a/package-lock.json b/package-lock.json index bc83195bf..aaec3860d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1306,6 +1306,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@miniflare/d1": { + "resolved": "packages/d1", + "link": true + }, "node_modules/@miniflare/durable-objects": { "resolved": "packages/durable-objects", "link": true @@ -1574,6 +1578,14 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.0.tgz", + "integrity": "sha512-rnSP9vY+fVsF3iJja5yRGBJV63PNBiezJlYrCkqUmQWFoB16cxAHwOkjsAYEu317miOfKaJpa65cbp0P4XJ/jw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/busboy": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.0.tgz", @@ -1683,8 +1695,7 @@ "node_modules/@types/node": { "version": "18.7.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.15.tgz", - "integrity": "sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ==", - "dev": true + "integrity": "sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ==" }, "node_modules/@types/node-forge": { "version": "0.10.9", @@ -2643,6 +2654,15 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -7292,6 +7312,128 @@ "node": ">=8" } }, + "node_modules/npx-import": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npx-import/-/npx-import-1.1.3.tgz", + "integrity": "sha512-zy6249FJ81OtPsvz2y0+rgis31EN5wbdwBG2umtEh65W/4onYArHuoUSZ+W+T7BQYK7YF+h9G4CuGPusMCcLOw==", + "dev": true, + "dependencies": { + "execa": "^6.1.0", + "parse-package-name": "^1.0.0", + "semver": "^7.3.7", + "validate-npm-package-name": "^4.0.0" + } + }, + "node_modules/npx-import/node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/npx-import/node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/npx-import/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npx-import/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npx-import/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npx-import/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npx-import/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npx-import/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nwsapi": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", @@ -7603,6 +7745,12 @@ "node": ">=6" } }, + "node_modules/parse-package-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-package-name/-/parse-package-name-1.0.0.tgz", + "integrity": "sha512-kBeTUtcj+SkyfaW4+KBe0HtsloBJ/mKTPoxpVdA57GZiPerREsUWJOhVj9anXweFiJkm5y8FG1sxFZkZ0SN6wg==", + "dev": true + }, "node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -8400,9 +8548,9 @@ } }, "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -9343,6 +9491,18 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validate-npm-package-name": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz", + "integrity": "sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/validator": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", @@ -9916,6 +10076,21 @@ "node": ">=16.13" } }, + "packages/d1": { + "name": "@miniflare/d1", + "version": "2.8.2", + "license": "MIT", + "dependencies": { + "@miniflare/core": "2.8.2", + "@miniflare/shared": "2.8.2" + }, + "devDependencies": { + "@miniflare/shared-test": "2.8.2" + }, + "engines": { + "node": ">=16.7" + } + }, "packages/durable-objects": { "name": "@miniflare/durable-objects", "version": "2.8.2", @@ -10055,6 +10230,7 @@ "@miniflare/cache": "2.8.2", "@miniflare/cli-parser": "2.8.2", "@miniflare/core": "2.8.2", + "@miniflare/d1": "2.8.2", "@miniflare/durable-objects": "2.8.2", "@miniflare/html-rewriter": "2.8.2", "@miniflare/http-server": "2.8.2", @@ -10165,12 +10341,14 @@ "version": "2.8.2", "license": "MIT", "dependencies": { + "@types/better-sqlite3": "^7.6.0", "kleur": "^4.1.4", "picomatch": "^2.3.1" }, "devDependencies": { "@miniflare/shared-test": "2.8.2", - "@types/picomatch": "^2.3.0" + "@types/picomatch": "^2.3.0", + "npx-import": "^1.1.3" }, "engines": { "node": ">=16.13" @@ -10200,6 +10378,7 @@ "dependencies": { "@miniflare/cache": "2.8.2", "@miniflare/core": "2.8.2", + "@miniflare/d1": "2.8.2", "@miniflare/durable-objects": "2.8.2", "@miniflare/html-rewriter": "2.8.2", "@miniflare/kv": "2.8.2", @@ -11353,6 +11532,14 @@ "urlpattern-polyfill": "^4.0.3" } }, + "@miniflare/d1": { + "version": "file:packages/d1", + "requires": { + "@miniflare/core": "2.8.2", + "@miniflare/shared": "2.8.2", + "@miniflare/shared-test": "2.8.2" + } + }, "@miniflare/durable-objects": { "version": "file:packages/durable-objects", "requires": { @@ -11432,8 +11619,10 @@ "version": "file:packages/shared", "requires": { "@miniflare/shared-test": "2.8.2", + "@types/better-sqlite3": "^7.6.0", "@types/picomatch": "^2.3.0", "kleur": "^4.1.4", + "npx-import": "^1.1.2", "picomatch": "^2.3.1" } }, @@ -11454,6 +11643,7 @@ "requires": { "@miniflare/cache": "2.8.2", "@miniflare/core": "2.8.2", + "@miniflare/d1": "2.8.2", "@miniflare/durable-objects": "2.8.2", "@miniflare/html-rewriter": "2.8.2", "@miniflare/kv": "2.8.2", @@ -11702,6 +11892,14 @@ "@babel/types": "^7.3.0" } }, + "@types/better-sqlite3": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.0.tgz", + "integrity": "sha512-rnSP9vY+fVsF3iJja5yRGBJV63PNBiezJlYrCkqUmQWFoB16cxAHwOkjsAYEu317miOfKaJpa65cbp0P4XJ/jw==", + "requires": { + "@types/node": "*" + } + }, "@types/busboy": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.0.tgz", @@ -11811,8 +12009,7 @@ "@types/node": { "version": "18.7.15", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.15.tgz", - "integrity": "sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ==", - "dev": true + "integrity": "sha512-XnjpaI8Bgc3eBag2Aw4t2Uj/49lLBSStHWfqKvIuXD7FIrZyMLWp8KuAFHAqxMZYTF9l08N1ctUn9YNybZJVmQ==" }, "@types/node-forge": { "version": "0.10.9", @@ -12510,6 +12707,15 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "requires": { + "semver": "^7.0.0" + } + }, "busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -15822,6 +16028,7 @@ "@miniflare/cache": "2.8.2", "@miniflare/cli-parser": "2.8.2", "@miniflare/core": "2.8.2", + "@miniflare/d1": "2.8.2", "@miniflare/durable-objects": "2.8.2", "@miniflare/html-rewriter": "2.8.2", "@miniflare/http-server": "2.8.2", @@ -15959,6 +16166,85 @@ "path-key": "^3.0.0" } }, + "npx-import": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npx-import/-/npx-import-1.1.3.tgz", + "integrity": "sha512-zy6249FJ81OtPsvz2y0+rgis31EN5wbdwBG2umtEh65W/4onYArHuoUSZ+W+T7BQYK7YF+h9G4CuGPusMCcLOw==", + "dev": true, + "requires": { + "execa": "^6.1.0", + "parse-package-name": "^1.0.0", + "semver": "^7.3.7", + "validate-npm-package-name": "^4.0.0" + }, + "dependencies": { + "execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } + } + }, "nwsapi": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", @@ -16184,6 +16470,12 @@ "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", "dev": true }, + "parse-package-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-package-name/-/parse-package-name-1.0.0.tgz", + "integrity": "sha512-kBeTUtcj+SkyfaW4+KBe0HtsloBJ/mKTPoxpVdA57GZiPerREsUWJOhVj9anXweFiJkm5y8FG1sxFZkZ0SN6wg==", + "dev": true + }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -16760,9 +17052,9 @@ "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==" }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -17472,6 +17764,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "validate-npm-package-name": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz", + "integrity": "sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==", + "dev": true, + "requires": { + "builtins": "^5.0.0" + } + }, "validator": { "version": "13.7.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", diff --git a/packages/d1/README.md b/packages/d1/README.md new file mode 100644 index 000000000..195fa0c42 --- /dev/null +++ b/packages/d1/README.md @@ -0,0 +1,44 @@ +# `@miniflare/d1` + +Workers D1 module for [Miniflare](https://github.com/cloudflare/miniflare): a +fun, full-featured, fully-local simulator for Cloudflare Workers. See +[📦 D1](https://miniflare.dev/storage/d1) for more details. + +## Example + +```js +import { BetaDatabase } from "@miniflare/d1"; +import { MemoryStorage } from "@miniflare/storage-memory"; +const db = new BetaDatabase(new MemoryStorage()); + +// BetaDatabase only supports .fetch(), once D1 is out of beta the full API will be available here: +await db.fetch("/execute", { + method: "POST", + body: JSON.stringify({ + sql: `CREATE TABLE my_table (cid INTEGER PRIMARY KEY, name TEXT NOT NULL);`, + }), +}); +const response = await db.fetch("/query", { + method: "POST", + body: JSON.stringify({ + sql: `SELECT * FROM sqlite_schema`, + }), +}); +console.log(await response.json()); +/* +{ + "success": true, + "result": [ + [ + { + "type": "table", + "name": "my_table", + "tbl_name": "my_table", + "rootpage": 2, + "sql": "CREATE TABLE my_table (cid INTEGER PRIMARY KEY, name TEXT NOT NULL)" + } + ] + ] +} +*/ +``` diff --git a/packages/d1/package.json b/packages/d1/package.json new file mode 100644 index 000000000..b4e21ed86 --- /dev/null +++ b/packages/d1/package.json @@ -0,0 +1,45 @@ +{ + "name": "@miniflare/d1", + "version": "2.8.2", + "description": "Workers D1 module for Miniflare: a fun, full-featured, fully-local simulator for Cloudflare Workers", + "keywords": [ + "cloudflare", + "workers", + "worker", + "local", + "d1", + "sqlite" + ], + "author": "Glen Maddern ", + "license": "MIT", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "files": [ + "dist/src" + ], + "engines": { + "node": ">=16.7" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cloudflare/miniflare.git", + "directory": "packages/d1" + }, + "bugs": { + "url": "https://github.com/cloudflare/miniflare/issues" + }, + "homepage": "https://github.com/cloudflare/miniflare/tree/master/packages/d1#readme", + "volta": { + "extends": "../../package.json" + }, + "dependencies": { + "@miniflare/core": "2.8.2", + "@miniflare/shared": "2.8.2" + }, + "devDependencies": { + "@miniflare/shared-test": "2.8.2" + } +} diff --git a/packages/d1/src/database.ts b/packages/d1/src/database.ts new file mode 100644 index 000000000..d03e18e4a --- /dev/null +++ b/packages/d1/src/database.ts @@ -0,0 +1,38 @@ +import { performance } from "node:perf_hooks"; +import type { SqliteDB } from "@miniflare/shared"; +import { Statement } from "./statement"; + +export class BetaDatabase { + #db: SqliteDB; + + constructor(db: SqliteDB) { + this.#db = db; + } + + prepare(source: string) { + return new Statement(this.#db, source); + } + + async batch(statements: Statement[]) { + return await Promise.all(statements.map((s) => s.all())); + } + + async exec(multiLineStatements: string) { + const statements = multiLineStatements + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); + const start = performance.now(); + for (const statement of statements) { + await new Statement(this.#db, statement).all(); + } + return { + count: statements.length, + duration: performance.now() - start, + }; + } + + async dump() { + throw new Error("DB.dump() not implemented locally!"); + } +} diff --git a/packages/d1/src/index.ts b/packages/d1/src/index.ts new file mode 100644 index 000000000..0c3fb15ee --- /dev/null +++ b/packages/d1/src/index.ts @@ -0,0 +1,3 @@ +export * from "./database"; +export * from "./plugin"; +export * from "./statement"; diff --git a/packages/d1/src/plugin.ts b/packages/d1/src/plugin.ts new file mode 100644 index 000000000..50c15bb45 --- /dev/null +++ b/packages/d1/src/plugin.ts @@ -0,0 +1,70 @@ +import { + Context, + Option, + OptionType, + Plugin, + PluginContext, + SetupResult, + StorageFactory, + resolveStoragePersist, +} from "@miniflare/shared"; +import { BetaDatabase } from "./database"; + +export interface D1Options { + d1Databases?: string[]; + d1Persist?: boolean | string; +} +const D1_BETA_PREFIX = `__D1_BETA__`; + +export class D1Plugin extends Plugin implements D1Options { + @Option({ + type: OptionType.ARRAY, + name: "d1", + description: "D1 namespace to bind", + logName: "D1 Namespaces", + fromWrangler: ({ d1_databases }) => + d1_databases?.map(({ binding }) => binding), + }) + d1Databases?: string[]; + + @Option({ + type: OptionType.BOOLEAN_STRING, + description: "Persist D1 data (to optional path)", + logName: "D1 Persistence", + fromWrangler: ({ miniflare }) => miniflare?.d1_persist, + }) + d1Persist?: boolean | string; + readonly #persist?: boolean | string; + + constructor(ctx: PluginContext, options?: D1Options) { + super(ctx); + this.assignOptions(options); + this.#persist = resolveStoragePersist(ctx.rootPath, this.d1Persist); + } + + async getBetaDatabase( + storageFactory: StorageFactory, + dbName: string + ): Promise { + const storage = await storageFactory.storage(dbName, this.#persist); + return new BetaDatabase(await storage.getSqliteDatabase()); + } + + async setup(storageFactory: StorageFactory): Promise { + const bindings: Context = {}; + for (const dbName of this.d1Databases ?? []) { + if (dbName.startsWith(D1_BETA_PREFIX)) { + bindings[dbName] = await this.getBetaDatabase( + storageFactory, + // Store it locally without the prefix + dbName.slice(D1_BETA_PREFIX.length) + ); + } else { + console.warn( + `Not injecting D1 Database for '${dbName}' as this version of Miniflare only supports D1 beta bindings. Upgrade Wrangler and/or Miniflare and try again.` + ); + } + } + return { bindings }; + } +} diff --git a/packages/d1/src/statement.ts b/packages/d1/src/statement.ts new file mode 100644 index 000000000..ba1e96249 --- /dev/null +++ b/packages/d1/src/statement.ts @@ -0,0 +1,137 @@ +import { performance } from "node:perf_hooks"; +import type { + Database as SqliteDB, + Statement as SqliteStatement, +} from "better-sqlite3"; + +export type BindParams = any[] | [Record]; + +function errorWithCause(message: string, e: unknown) { + // @ts-ignore Errors have causes now, why don't you know this Typescript? + return new Error(message, { cause: e }); +} + +export class Statement { + readonly #db: SqliteDB; + readonly #query: string; + readonly #bindings: BindParams | undefined; + + constructor(db: SqliteDB, query: string, bindings?: BindParams) { + this.#db = db; + this.#query = query; + this.#bindings = bindings; + } + + // Lazily accumulate binding instructions, because ".bind" in better-sqlite3 + // is a real action that means the query must be valid when it's written, + // not when it's about to be executed (i.e. in a batch). + bind(...params: BindParams) { + // Adopting better-sqlite3 behaviour—once bound, a statement cannot be bound again + if (this.#bindings !== undefined) { + throw new TypeError( + "The bind() method can only be invoked once per statement object" + ); + } + return new Statement(this.#db, this.#query, params); + } + + private prepareAndBind() { + const prepared = this.#db.prepare(this.#query); + if (this.#bindings === undefined) return prepared; + try { + return prepared.bind(this.#bindings); + } catch (e) { + // For statements using ?1 ?2, etc, we want to pass them as varargs but + // "better" sqlite3 wants them as an object of {1: params[0], 2: params[1], ...} + if (this.#bindings.length > 0 && typeof this.#bindings[0] !== "object") { + return prepared.bind( + Object.fromEntries(this.#bindings.map((v, i) => [i + 1, v])) + ); + } else { + throw e; + } + } + } + + async all() { + const start = performance.now(); + const statementWithBindings = this.prepareAndBind(); + try { + const results = Statement.#all(statementWithBindings); + return { + results, + duration: performance.now() - start, + lastRowId: null, + changes: null, + success: true, + served_by: "x-miniflare.db3", + }; + } catch (e) { + throw errorWithCause("D1_ALL_ERROR", e); + } + } + + static #all(statementWithBindings: SqliteStatement) { + try { + return statementWithBindings.all(); + } catch (e: unknown) { + // This is the quickest/simplest way I could find to return results by + // default, falling back to .run() + if ( + /This statement does not return data\. Use run\(\) instead/.exec( + (e as Error).message + ) + ) { + return Statement.#run(statementWithBindings); + } + throw e; + } + } + + async first(col?: string) { + const statementWithBindings = this.prepareAndBind(); + try { + const data = Statement.#first(statementWithBindings); + return typeof col === "string" ? data[col] : data; + } catch (e) { + throw errorWithCause("D1_FIRST_ERROR", e); + } + } + + static #first(statementWithBindings: SqliteStatement) { + return statementWithBindings.get(); + } + + async run() { + const start = performance.now(); + const statementWithBindings = this.prepareAndBind(); + try { + const { changes, lastInsertRowid } = Statement.#run( + statementWithBindings + ); + return { + results: null, + duration: performance.now() - start, + lastRowId: lastInsertRowid, + changes, + success: true, + served_by: "x-miniflare.db3", + }; + } catch (e) { + throw errorWithCause("D1_RUN_ERROR", e); + } + } + + static #run(statementWithBindings: SqliteStatement) { + return statementWithBindings.run(); + } + + async raw() { + const statementWithBindings = this.prepareAndBind(); + return Statement.#raw(statementWithBindings); + } + + static #raw(statementWithBindings: SqliteStatement) { + return statementWithBindings.raw() as any; + } +} diff --git a/packages/d1/test/database.spec.ts b/packages/d1/test/database.spec.ts new file mode 100644 index 000000000..cade96f8b --- /dev/null +++ b/packages/d1/test/database.spec.ts @@ -0,0 +1,46 @@ +import { BetaDatabase } from "@miniflare/d1"; +import { Storage } from "@miniflare/shared"; +import { testClock } from "@miniflare/shared-test"; +import { MemoryStorage } from "@miniflare/storage-memory"; +import anyTest, { TestInterface } from "ava"; + +interface Context { + storage: Storage; + db: BetaDatabase; +} + +const test = anyTest as TestInterface; + +test.beforeEach(async (t) => { + const storage = new MemoryStorage(undefined, testClock); + const db = new BetaDatabase(await storage.getSqliteDatabase()); + t.context = { storage, db }; +}); + +test("batch, prepare & all", async (t) => { + const { db } = t.context; + + await db.batch([ + db.prepare( + `CREATE TABLE my_table (cid INTEGER PRIMARY KEY, name TEXT NOT NULL);` + ), + ]); + const response = await db.prepare(`SELECT * FROM sqlite_schema`).all(); + t.deepEqual(Object.keys(response), [ + "results", + "duration", + "lastRowId", + "changes", + "success", + "served_by", + ]); + t.deepEqual(response.results, [ + { + type: "table", + name: "my_table", + tbl_name: "my_table", + rootpage: 2, + sql: "CREATE TABLE my_table (cid INTEGER PRIMARY KEY, name TEXT NOT NULL)", + }, + ]); +}); diff --git a/packages/d1/test/plugin.spec.ts b/packages/d1/test/plugin.spec.ts new file mode 100644 index 000000000..9f21bc622 --- /dev/null +++ b/packages/d1/test/plugin.spec.ts @@ -0,0 +1,53 @@ +import { createCrypto } from "@miniflare/core"; +import { D1Plugin } from "@miniflare/d1"; +import { + logPluginOptions, + parsePluginArgv, + parsePluginWranglerConfig, +} from "@miniflare/shared-test"; +import test from "ava"; + +const crypto = createCrypto(); + +test("D1Plugin: parses options from argv", (t) => { + const options = parsePluginArgv(D1Plugin, [ + "--d1", + "DB1", + "--d1", + "DB2", + "--d1-persist", + "path", + ]); + t.deepEqual(options, { + d1Databases: ["DB1", "DB2"], + d1Persist: "path", + }); +}); +test("D1Plugin: parses options from wrangler config", (t) => { + const options = parsePluginWranglerConfig(D1Plugin, { + d1_databases: [ + { + binding: "DB1", + database_name: "data-base-1", + database_id: crypto.randomUUID(), + }, + { + binding: "DB2", + database_name: "data-base-2", + database_id: crypto.randomUUID(), + }, + ], + miniflare: { d1_persist: "path" }, + }); + t.deepEqual(options, { + d1Databases: ["DB1", "DB2"], + d1Persist: "path", + }); +}); +test("D1Plugin: logs options", (t) => { + const logs = logPluginOptions(D1Plugin, { + d1Databases: ["DB1", "DB2"], + d1Persist: true, + }); + t.deepEqual(logs, ["D1 Namespaces: DB1, DB2", "D1 Persistence: true"]); +}); diff --git a/packages/jest-environment-miniflare/test/fixtures/d1.worker.spec.js b/packages/jest-environment-miniflare/test/fixtures/d1.worker.spec.js new file mode 100644 index 000000000..e337e14a1 --- /dev/null +++ b/packages/jest-environment-miniflare/test/fixtures/d1.worker.spec.js @@ -0,0 +1,67 @@ +// This would normally be provided by the Wrangler shim +// (in local mode, it does nothing but rename the binding) +const DB_1 = __D1_BETA__DB_1; + +async function get() { + const result = await DB_1.prepare(`SELECT value FROM entries LIMIT 1;`).all(); + return result.results[0]?.value ?? ""; +} + +async function append(str) { + const value = await get(); + await DB_1.prepare(`UPDATE entries SET value = ?`) + .bind(value + str) + .run(); +} + +beforeAll(async () => { + await DB_1.exec(`CREATE TABLE entries (id INTEGER PRIMARY KEY, value TEXT)`); + await DB_1.exec(`INSERT INTO entries (value) VALUES ('a')`); +}); +beforeEach(() => append("b")); + +test("D1 test 1", async () => { + await append("c"); + expect(await get()).toBe("abc"); +}); +test("D1 test 2", async () => { + await append("d"); + expect(await get()).toBe("abd"); +}); + +describe("more D1 tests", () => { + beforeAll(() => append("e")); + beforeEach(() => append("f")); + + test("D1 test 3", async () => { + await append("g"); + expect(await get()).toBe("aebfg"); + }); + test("D1 test 4", async () => { + await append("h"); + expect(await get()).toBe("aebfh"); + }); + + describe("even more D1 tests", () => { + beforeAll(() => append("i")); + beforeEach(() => append("j")); + + test("D1 test 5", async () => { + await append("k"); + expect(await get()).toBe("aeibfjk"); + }); + test("D1 test 6", async () => { + await append("l"); + expect(await get()).toBe("aeibfjl"); + }); + }); +}); + +test("D1 test 7", async () => { + await append("m"); + expect(await get()).toBe("abm"); +}); +test("D1 test 8", async () => { + await append("n"); + expect(await get()).toBe("abn"); +}); diff --git a/packages/jest-environment-miniflare/test/index.spec.ts b/packages/jest-environment-miniflare/test/index.spec.ts index 6431d622d..c697259ba 100644 --- a/packages/jest-environment-miniflare/test/index.spec.ts +++ b/packages/jest-environment-miniflare/test/index.spec.ts @@ -39,7 +39,7 @@ async function runJest( "--testEnvironmentOptions", JSON.stringify(options), ], - { cwd } + { cwd, env: { ...process.env, NPX_IMPORT_QUIET: "true" } } ); let output = ""; jest.stdout.on("data", (data) => (output += data)); @@ -53,6 +53,7 @@ test.serial( async (t) => { const [exitCode, output] = await runJest(".worker.spec.js", { kvNamespaces: ["TEST_NAMESPACE"], + d1Databases: ["__D1_BETA__DB_1"], sitePath: fixturesPath, globals: { KEY: "value" }, // Check persistence options ignored diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 525f694b3..91b075a21 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -53,6 +53,7 @@ "@miniflare/html-rewriter": "2.8.2", "@miniflare/http-server": "2.8.2", "@miniflare/kv": "2.8.2", + "@miniflare/d1": "2.8.2", "@miniflare/queues": "2.8.2", "@miniflare/r2": "2.8.2", "@miniflare/runner-vm": "2.8.2", diff --git a/packages/miniflare/src/api.ts b/packages/miniflare/src/api.ts index 02af6e26a..f40c0f375 100644 --- a/packages/miniflare/src/api.ts +++ b/packages/miniflare/src/api.ts @@ -8,6 +8,7 @@ import { MiniflareCore, MiniflareCoreOptions, } from "@miniflare/core"; +import { D1Plugin } from "@miniflare/d1"; import { DurableObjectId, DurableObjectNamespace, @@ -49,6 +50,7 @@ export const PLUGINS = { // Storage KVPlugin, + D1Plugin, R2Plugin, DurableObjectsPlugin, CachePlugin, diff --git a/packages/shared-test-environment/package.json b/packages/shared-test-environment/package.json index cd5993ade..5fdcf06ba 100644 --- a/packages/shared-test-environment/package.json +++ b/packages/shared-test-environment/package.json @@ -37,6 +37,7 @@ "dependencies": { "@miniflare/cache": "2.8.2", "@miniflare/core": "2.8.2", + "@miniflare/d1": "2.8.2", "@miniflare/durable-objects": "2.8.2", "@miniflare/html-rewriter": "2.8.2", "@miniflare/kv": "2.8.2", diff --git a/packages/shared-test-environment/src/index.ts b/packages/shared-test-environment/src/index.ts index 41ecd8e95..ddbb74c8e 100644 --- a/packages/shared-test-environment/src/index.ts +++ b/packages/shared-test-environment/src/index.ts @@ -57,6 +57,7 @@ export async function createMiniflareEnvironment( watch: false, // - Persistence must be disabled for stacked storage to work kvPersist: false, + d1Persist: false, cachePersist: false, durableObjectsPersist: false, // - Allow all global operations, tests will be outside of a request diff --git a/packages/shared-test-environment/src/plugins.ts b/packages/shared-test-environment/src/plugins.ts index 0ea3bf975..8cf06e19d 100644 --- a/packages/shared-test-environment/src/plugins.ts +++ b/packages/shared-test-environment/src/plugins.ts @@ -1,5 +1,6 @@ import { CachePlugin } from "@miniflare/cache"; import { BindingsPlugin, CorePlugin } from "@miniflare/core"; +import { D1Plugin } from "@miniflare/d1"; import { DurableObjectsPlugin } from "@miniflare/durable-objects"; import { HTMLRewriterPlugin } from "@miniflare/html-rewriter"; import { KVPlugin } from "@miniflare/kv"; @@ -15,6 +16,7 @@ import { WebSocketPlugin } from "@miniflare/web-sockets"; export const PLUGINS = { CorePlugin, KVPlugin, + D1Plugin, R2Plugin, DurableObjectsPlugin, CachePlugin, diff --git a/packages/shared-test-environment/src/storage.ts b/packages/shared-test-environment/src/storage.ts index d7631ec46..d1e70f0a1 100644 --- a/packages/shared-test-environment/src/storage.ts +++ b/packages/shared-test-environment/src/storage.ts @@ -4,10 +4,18 @@ import { MemoryStorage } from "@miniflare/storage-memory"; export class StackedMemoryStorage extends MemoryStorage { private readonly stack: Map[] = []; + private readonly transactionStack: string[] = []; push(): void { this.stack.push(this.map); this.map = new Map(this.map); + + if (this.sqliteDB) { + const transactionName = `STACK_${this.transactionStack.length + 1}`; + this.transactionStack.push(transactionName); + + this.sqliteDB.exec(`SAVEPOINT ${transactionName}`); + } } pop(): void { @@ -16,6 +24,15 @@ export class StackedMemoryStorage extends MemoryStorage { // If this happens, default to an empty map, since the storage didn't exist // at the new stack level. this.map = this.stack.pop() ?? new Map(); + + if (this.sqliteDB) { + const transactionToRollback = this.transactionStack.pop(); + // This may be undefined if we popped too many times + if (transactionToRollback) { + this.sqliteDB.exec(`ROLLBACK TO ${transactionToRollback}`); + this.sqliteDB.exec(`RELEASE ${transactionToRollback}`); + } + } } } diff --git a/packages/shared/package.json b/packages/shared/package.json index 3466321e1..ee703d4ec 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -35,11 +35,13 @@ "extends": "../../package.json" }, "dependencies": { + "@types/better-sqlite3": "^7.6.0", "kleur": "^4.1.4", "picomatch": "^2.3.1" }, "devDependencies": { "@miniflare/shared-test": "2.8.2", - "@types/picomatch": "^2.3.0" + "@types/picomatch": "^2.3.0", + "npx-import": "^1.1.3" } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 6f0e4afb0..4ad9efd88 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -10,3 +10,4 @@ export * from "./runner"; export * from "./storage"; export * from "./sync/"; export * from "./wrangler"; +export * from "./sqlite"; diff --git a/packages/shared/src/sqlite.ts b/packages/shared/src/sqlite.ts new file mode 100644 index 000000000..ecb010437 --- /dev/null +++ b/packages/shared/src/sqlite.ts @@ -0,0 +1,28 @@ +import path from "node:path"; +import type { + Database as SqliteDB, + Options as SqliteOptions, +} from "better-sqlite3"; +export type { SqliteDB, SqliteOptions }; +import { npxImport, npxResolve } from "npx-import"; + +// Can't use typeof import(), so reproducing BetterSqlite3.DatabaseConstructor here +export interface DBConstructor { + new (filename: string | Buffer, options?: SqliteOptions): SqliteDB; +} + +export async function createSQLiteDB(dbPath: string): Promise { + const { default: DatabaseConstructor } = await npxImport<{ + default: DBConstructor; + }>("better-sqlite3@7.6.2"); + return new DatabaseConstructor(dbPath, { + nativeBinding: getSQLiteNativeBindingLocation(npxResolve("better-sqlite3")), + }); +} + +export function getSQLiteNativeBindingLocation(sqliteResolvePath: string) { + return path.resolve( + path.dirname(sqliteResolvePath), + "../build/Release/better_sqlite3.node" + ); +} diff --git a/packages/shared/src/storage.ts b/packages/shared/src/storage.ts index 53cba3f35..596f0cbf7 100644 --- a/packages/shared/src/storage.ts +++ b/packages/shared/src/storage.ts @@ -1,3 +1,4 @@ +import type { SqliteDB } from "./sqlite"; import { Awaitable } from "./sync"; export interface StoredMeta { @@ -116,6 +117,9 @@ export abstract class Storage { options: StorageListOptions, skipMetadata: true ): Awaitable>; + async getSqliteDatabase(): Promise { + throw new Error("D1 not implemented for this Storage class"); + } // Batch functions, default implementations may be overridden to optimise diff --git a/packages/shared/src/wrangler.ts b/packages/shared/src/wrangler.ts index 0ebfe6204..2c26768ea 100644 --- a/packages/shared/src/wrangler.ts +++ b/packages/shared/src/wrangler.ts @@ -31,6 +31,11 @@ export interface WranglerEnvironmentConfig { id?: string; preview_id?: string; }[]; // NOT inherited + d1_databases?: { + binding: string; + database_name: string; + database_id: string; + }[]; // NOT inherited r2_buckets?: { binding: string; bucket_name?: string; @@ -78,6 +83,7 @@ export interface WranglerEnvironmentConfig { watch?: boolean; build_watch_dirs?: string[]; kv_persist?: boolean | string; + d1_persist?: boolean | string; r2_persist?: boolean | string; cache?: boolean; cache_persist?: boolean | string; diff --git a/packages/storage-file/src/index.ts b/packages/storage-file/src/index.ts index 0ad840001..88e0074a6 100644 --- a/packages/storage-file/src/index.ts +++ b/packages/storage-file/src/index.ts @@ -1,12 +1,14 @@ -import { existsSync } from "fs"; +import fs, { existsSync } from "fs"; import path from "path"; import { MiniflareError, Range, RangeStoredValueMeta, + SqliteDB, StoredKeyMeta, StoredMeta, StoredValueMeta, + createSQLiteDB, defaultClock, sanitisePath, viewToArray, @@ -34,6 +36,7 @@ export interface FileMeta extends StoredMeta { export class FileStorage extends LocalStorage { protected readonly root: string; + private sqliteDB?: SqliteDB; constructor( root: string, @@ -104,6 +107,14 @@ export class FileStorage extends LocalStorage { } } + async getSqliteDatabase(): Promise { + if (this.sqliteDB) return this.sqliteDB; + + fs.mkdirSync(path.dirname(this.root), { recursive: true }); + this.sqliteDB = await createSQLiteDB(this.root + ".sqlite3"); + return this.sqliteDB; + } + async getRangeMaybeExpired( key: string, { offset: _offset, length: _length, suffix }: Range diff --git a/packages/storage-memory/src/memory.ts b/packages/storage-memory/src/memory.ts index cf943dbee..7976e6358 100644 --- a/packages/storage-memory/src/memory.ts +++ b/packages/storage-memory/src/memory.ts @@ -1,9 +1,17 @@ -import { Range, RangeStoredValueMeta, defaultClock } from "@miniflare/shared"; +import { + Range, + RangeStoredValueMeta, + SqliteDB, + createSQLiteDB, + defaultClock, +} from "@miniflare/shared"; import { StoredKeyMeta, StoredMeta, StoredValueMeta } from "@miniflare/shared"; import { cloneMetadata } from "./helpers"; import { LocalStorage } from "./local"; export class MemoryStorage extends LocalStorage { + protected sqliteDB?: SqliteDB; + constructor( protected map = new Map(), clock = defaultClock @@ -114,4 +122,11 @@ export class MemoryStorage extends LocalStorage { MemoryStorage.entryToStoredKey ) as StoredKeyMeta[]; } + + async getSqliteDatabase(): Promise { + if (this.sqliteDB) return this.sqliteDB; + + this.sqliteDB = await createSQLiteDB(":memory:"); + return await this.sqliteDB; + } } diff --git a/packages/vitest-environment-miniflare/test/fixtures/service-worker/d1.worker.spec.js b/packages/vitest-environment-miniflare/test/fixtures/service-worker/d1.worker.spec.js new file mode 100644 index 000000000..64de23a74 --- /dev/null +++ b/packages/vitest-environment-miniflare/test/fixtures/service-worker/d1.worker.spec.js @@ -0,0 +1,70 @@ +import { beforeAll, beforeEach, expect, test } from "vitest"; +const describe = setupMiniflareIsolatedStorage(); + +// This would normally be provided by the Wrangler shim +// (in local mode, it does nothing but rename the binding) +const DB_1 = __D1_BETA__DB_1; + +async function get() { + const result = await DB_1.prepare(`SELECT value FROM entries LIMIT 1;`).all(); + return result.results[0]?.value ?? ""; +} + +async function append(str) { + const value = await get(); + await DB_1.prepare(`UPDATE entries SET value = ?`) + .bind(value + str) + .run(); +} + +beforeAll(async () => { + await DB_1.exec(`CREATE TABLE entries (id INTEGER PRIMARY KEY, value TEXT)`); + await DB_1.exec(`INSERT INTO entries (value) VALUES ('a')`); +}); +beforeEach(() => append("b")); + +test("D1 test 1", async () => { + await append("c"); + expect(await get()).toBe("abc"); +}); +test("D1 test 2", async () => { + await append("d"); + expect(await get()).toBe("abd"); +}); + +describe("more D1 tests", () => { + beforeAll(() => append("e")); + beforeEach(() => append("f")); + + test("D1 test 3", async () => { + await append("g"); + expect(await get()).toBe("aebfg"); + }); + test("D1 test 4", async () => { + await append("h"); + expect(await get()).toBe("aebfh"); + }); + + describe("even more D1 tests", () => { + beforeAll(() => append("i")); + beforeEach(() => append("j")); + + test("D1 test 5", async () => { + await append("k"); + expect(await get()).toBe("aeibfjk"); + }); + test("D1 test 6", async () => { + await append("l"); + expect(await get()).toBe("aeibfjl"); + }); + }); +}); + +test("D1 test 7", async () => { + await append("m"); + expect(await get()).toBe("abm"); +}); +test("D1 test 8", async () => { + await append("n"); + expect(await get()).toBe("abn"); +}); diff --git a/packages/vitest-environment-miniflare/test/fixtures/service-worker/vitest.config.js b/packages/vitest-environment-miniflare/test/fixtures/service-worker/vitest.config.js index 9d7224f05..79f17a6b4 100644 --- a/packages/vitest-environment-miniflare/test/fixtures/service-worker/vitest.config.js +++ b/packages/vitest-environment-miniflare/test/fixtures/service-worker/vitest.config.js @@ -14,6 +14,7 @@ export default defineConfig({ environment: "miniflare", environmentOptions: { kvNamespaces: ["TEST_NAMESPACE"], + d1Databases: ["__D1_BETA__DB_1"], sitePath: __dirname, globals: { KEY: "value" }, // Check persistence options ignored diff --git a/packages/vitest-environment-miniflare/test/index.spec.ts b/packages/vitest-environment-miniflare/test/index.spec.ts index 8ed39a39a..bfd8d38f6 100644 --- a/packages/vitest-environment-miniflare/test/index.spec.ts +++ b/packages/vitest-environment-miniflare/test/index.spec.ts @@ -48,6 +48,7 @@ async function runVitest( env: { ...process.env, NODE_OPTIONS: "--experimental-vm-modules --no-warnings", + NPX_IMPORT_QUIET: "true", }, } ); diff --git a/scripts/build.mjs b/scripts/build.mjs index 6c0f16dd9..09b931392 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -63,6 +63,8 @@ const buildOptions = { "jest*", // Mark sites manifest as external, it's added by SitesPlugin "__STATIC_CONTENT_MANIFEST", + // SQLite requires a better_sqlite3.node file, so don't bundle it + "better-sqlite3", ], logLevel: watch ? "info" : "warning", watch, diff --git a/types/env.d.ts b/types/env.d.ts index 03fa2ac75..b68ae158b 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -5,6 +5,7 @@ declare namespace NodeJS { MINIFLARE_SUBREQUEST_LIMIT?: string; MINIFLARE_INTERNAL_SUBREQUEST_LIMIT?: string; MINIFLARE_TEST_REDIS_URL?: string; + NPX_IMPORT_QUIET?: string; // REPL options: https://nodejs.org/api/repl.html#environment-variable-options MINIFLARE_REPL_HISTORY?: string;