diff --git a/README.md b/README.md index 5e753c2..5543a57 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # kysely-migrate -[Kysely](https://github.com/kysely-org/kysely) migrations CLI +[Kysely](https://github.com/kysely-org/kysely) migrations and codegen CLI ## Installation @@ -23,12 +23,13 @@ Usage: $ kysely-migrate [options] Commands: - create create new migration - down migrate one step down - init create configuration file - list list migrations - to migrate to selected migration - up migrate one step up + codegen generate types from database metadata + create create new migration + down migrate one step down + init create configuration file + list list migrations + to migrate to selected migration + up migrate one step up For more info, run any command with the `--help` flag: $ kysely-migrate create --help diff --git a/package.json b/package.json index b269c50..a2650d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kysely-migrate", - "description": "Kysely migrations CLI", + "description": "Kysely migrations and codegen CLI", "version": "0.0.3", "license": "MIT", "repository": { @@ -55,11 +55,6 @@ "kysely": ">=0.26.3", "typescript": ">=5" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - }, "dependencies": { "@clack/prompts": "^0.7.0", "bundle-require": "^4.0.2", @@ -71,17 +66,21 @@ "find-up": "^6.3.0", "human-id": "^4.1.0", "is-unicode-supported": "^1.3.0", + "kysely-codegen": "^0.11.0", + "mysql2": "^3.6.2", "picocolors": "^1.0.0" }, "devDependencies": { "@biomejs/biome": "1.1.2", "@changesets/changelog-github": "0.4.6", "@changesets/cli": "^2.26.2", + "@types/fs-extra": "^11.0.3", "@types/node": "^20.8.7", "@vitest/coverage-v8": "^0.34.5", "bun": "1.0.1", "bun-types": "^1.0.3", "execa": "^8.0.1", + "fs-extra": "^11.1.1", "glob": "^10.3.10", "knip": "^2.29.0", "kysely": "^0.26.3", @@ -91,27 +90,15 @@ "typescript": "5.2.2", "vitest": "^0.34.5" }, - "contributors": [ - "tmm@awkweb.com" - ], + "contributors": ["tmm@awkweb.com"], "funding": "https://github.com/sponsors/tmm", - "keywords": [ - "kysely", - "cli", - "migrate", - "migrations" - ], + "keywords": ["kysely", "cli", "migrate", "migrations", "codegen"], "packageManager": "pnpm@8.8.0", "simple-git-hooks": { "pre-commit": "pnpm format && pnpm lint:fix" }, "knip": { - "entry": [ - "src/**/*.ts!", - "src/exports/index.ts!" - ], - "project": [ - ".scripts/**/*.ts" - ] + "entry": ["src/**/*.ts!", "src/exports/index.ts!"], + "project": [".scripts/**/*.ts"] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bfd514..fe49040 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ dependencies: is-unicode-supported: specifier: ^1.3.0 version: 1.3.0 + kysely-codegen: + specifier: ^0.11.0 + version: 0.11.0(kysely@0.26.3)(mysql2@3.6.2) + mysql2: + specifier: ^3.6.2 + version: 3.6.2 picocolors: specifier: ^1.0.0 version: 1.0.0 @@ -49,6 +55,9 @@ devDependencies: '@changesets/cli': specifier: ^2.26.2 version: 2.26.2 + '@types/fs-extra': + specifier: ^11.0.3 + version: 11.0.3 '@types/node': specifier: ^20.8.7 version: 20.8.7 @@ -64,6 +73,9 @@ devDependencies: execa: specifier: ^8.0.1 version: 8.0.1 + fs-extra: + specifier: ^11.1.1 + version: 11.1.1 glob: specifier: ^10.3.10 version: 10.3.10 @@ -1152,6 +1164,13 @@ packages: resolution: {integrity: sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==} dev: true + /@types/fs-extra@11.0.3: + resolution: {integrity: sha512-sF59BlXtUdzEAL1u0MSvuzWd7PdZvZEtnaVkzX5mjpdWTJ8brG0jUqve3jPCzSzvAKKMHTG8F8o/WMQLtleZdQ==} + dependencies: + '@types/jsonfile': 6.1.3 + '@types/node': 20.8.7 + dev: true + /@types/is-ci@3.0.3: resolution: {integrity: sha512-FdHbjLiN2e8fk9QYQyVYZrK8svUDJpxSaSWLUga8EZS1RGAvvrqM9zbVARBtQuYPeLgnJxM2xloOswPwj1o2cQ==} dependencies: @@ -1162,6 +1181,12 @@ packages: resolution: {integrity: sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==} dev: true + /@types/jsonfile@6.1.3: + resolution: {integrity: sha512-/yqTk2SZ1wIezK0hiRZD7RuSf4B3whFxFamB1kGStv+8zlWScTMcHanzfc0XKWs5vA1TkHeckBlOyM8jxU8nHA==} + dependencies: + '@types/node': 20.8.7 + dev: true + /@types/minimist@1.2.4: resolution: {integrity: sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==} dev: true @@ -1294,14 +1319,12 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -1390,7 +1413,6 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true /better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -1411,7 +1433,6 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true /brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -1424,7 +1445,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /breakword@1.0.6: resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} @@ -1528,7 +1548,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1536,7 +1555,6 @@ packages: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true /chalk@5.3.0: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} @@ -1606,22 +1624,18 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - dev: true /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} @@ -1636,7 +1650,6 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true /constant-case@3.0.4: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} @@ -1763,6 +1776,11 @@ packages: object-keys: 1.1.1 dev: true + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + /detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1773,6 +1791,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff@3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + dev: false + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1980,7 +2003,6 @@ packages: /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: true /esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} @@ -2052,7 +2074,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /filter-iterator@0.0.1: resolution: {integrity: sha512-v4lhL7Qa8XpbW3LN46CEnmhGk3eHZwxfNl5at20aEkreesht4YKb/Ba3BUIbnPhAC/r3dmu7ABaGk6MAvh2alA==} @@ -2117,6 +2138,15 @@ packages: universalify: 2.0.0 dev: true + /fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + /fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -2137,7 +2167,6 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -2149,7 +2178,6 @@ packages: /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true /function.prototype.name@1.1.6: resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} @@ -2165,6 +2193,12 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + dependencies: + is-property: 1.0.2 + dev: false + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2196,6 +2230,17 @@ packages: get-intrinsic: 1.2.2 dev: true + /git-diff@2.0.6: + resolution: {integrity: sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA==} + engines: {node: '>= 4.8.0'} + dependencies: + chalk: 2.4.2 + diff: 3.5.0 + loglevel: 1.8.1 + shelljs: 0.8.5 + shelljs.exec: 1.1.8 + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2224,7 +2269,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} @@ -2303,12 +2347,10 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - dev: true /has-own-property@0.1.0: resolution: {integrity: sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw==} @@ -2342,7 +2384,6 @@ packages: engines: {node: '>= 0.4'} dependencies: function-bind: 1.1.2 - dev: true /header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} @@ -2387,6 +2428,13 @@ packages: safer-buffer: 2.1.2 dev: true + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /identity-function@1.0.0: resolution: {integrity: sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw==} dev: true @@ -2417,11 +2465,9 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} @@ -2432,6 +2478,11 @@ packages: side-channel: 1.0.4 dev: true + /interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: false + /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -2474,7 +2525,6 @@ packages: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} dependencies: hasown: 2.0.0 - dev: true /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -2525,13 +2575,16 @@ packages: /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-plain-obj@1.1.0: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} dev: true + /is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + dev: false + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -2744,10 +2797,37 @@ packages: - domexception dev: true + /kysely-codegen@0.11.0(kysely@0.26.3)(mysql2@3.6.2): + resolution: {integrity: sha512-8aklzXygjANshk5BoGSQ0BWukKIoPL4/k1iFWyteGUQ/VtB1GlyrELBZv1GglydjLGECSSVDpsOgEXyWQmuksg==} + hasBin: true + peerDependencies: + '@libsql/kysely-libsql': ^0.3.0 + better-sqlite3: '>=7.6.2' + kysely: '>=0.19.12' + mysql2: ^2.3.3 || ^3.0.0 + pg: ^8.8.0 + peerDependenciesMeta: + '@libsql/kysely-libsql': + optional: true + better-sqlite3: + optional: true + mysql2: + optional: true + pg: + optional: true + dependencies: + chalk: 4.1.2 + dotenv: 16.3.1 + git-diff: 2.0.6 + kysely: 0.26.3 + micromatch: 4.0.5 + minimist: 1.2.8 + mysql2: 3.6.2 + dev: false + /kysely@0.26.3: resolution: {integrity: sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw==} engines: {node: '>=14.0.0'} - dev: true /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2812,6 +2892,15 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true + /loglevel@1.8.1: + resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} + engines: {node: '>= 0.6.0'} + dev: false + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -2843,6 +2932,16 @@ packages: yallist: 4.0.0 dev: true + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + + /lru-cache@8.0.5: + resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} + engines: {node: '>=16.14'} + dev: false + /magic-string@0.16.0: resolution: {integrity: sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==} dependencies: @@ -2933,7 +3032,6 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true /mimic-fn@3.1.0: resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} @@ -2954,7 +3052,6 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: true /minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} @@ -2988,7 +3085,6 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true /minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} @@ -3023,6 +3119,27 @@ packages: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true + /mysql2@3.6.2: + resolution: {integrity: sha512-m5erE6bMoWfPXW1D5UrVwlT8PowAoSX69KcZzPuARQ3wY1RJ52NW9PdvdPo076XiSIkQ5IBTis7hxdlrQTlyug==} + engines: {node: '>= 8.0'} + dependencies: + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru-cache: 8.0.5 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + dev: false + + /named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + dependencies: + lru-cache: 7.18.3 + dev: false + /nanoid@3.3.6: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3151,7 +3268,6 @@ packages: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true /onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} @@ -3303,7 +3419,6 @@ packages: /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: true /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -3317,7 +3432,6 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-scurry@1.10.1: resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} @@ -3353,7 +3467,6 @@ packages: /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -3489,6 +3602,13 @@ packages: util-deprecate: 1.0.2 dev: true + /rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.8 + dev: false + /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -3539,7 +3659,6 @@ packages: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -3610,7 +3729,6 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} @@ -3638,6 +3756,10 @@ packages: upper-case-first: 2.0.2 dev: false + /seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + dev: false + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true @@ -3689,6 +3811,21 @@ packages: resolution: {integrity: sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg==} dev: true + /shelljs.exec@1.1.8: + resolution: {integrity: sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==} + engines: {node: '>= 4.0.0'} + dev: false + + /shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: false + /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -3799,6 +3936,11 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true + /sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + dev: false + /ssri@10.0.5: resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3929,19 +4071,16 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - dev: true /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -3999,7 +4138,6 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /to-space-case@1.0.0: resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} @@ -4414,7 +4552,6 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true /y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} diff --git a/src/cli.test.ts b/src/cli.test.ts new file mode 100644 index 0000000..0d82d02 --- /dev/null +++ b/src/cli.test.ts @@ -0,0 +1,77 @@ +import { join } from 'node:path' +import { type ExecaSyncReturnValue, type SyncOptions } from 'execa' +import { execaCommandSync } from 'execa' +import fs from 'fs-extra' +import pc from 'picocolors' +import { afterEach, beforeAll, expect, test } from 'vitest' + +import { version } from './version.js' + +const cliPath = join(__dirname, '../src/cli.ts') + +const projectName = 'test-app' +const genPath = join(__dirname, projectName) + +function run(args: string[], options: SyncOptions = {}): ExecaSyncReturnValue { + return execaCommandSync(`bun ${cliPath} ${args.join(' ')}`, options) +} + +beforeAll(() => { + fs.remove(genPath) +}) + +afterEach(() => fs.remove(genPath)) + +test('--help', () => { + const { stdout } = run(['--help']) + expect( + stdout + .replace(version, 'x.y.z') + .replace(pc.green(''), ''), + ).toMatchInlineSnapshot(` + "kysely-migrate/x.y.z + + Usage: + $ kysely-migrate [options] + + Commands: + codegen generate types from database metadata + create create new migration + down migrate one step down + init create configuration file + list list migrations + to migrate to selected migration + up migrate one step up + + For more info, run any command with the \`--help\` flag: + $ kysely-migrate codegen --help + $ kysely-migrate create --help + $ kysely-migrate down --help + $ kysely-migrate init --help + $ kysely-migrate list --help + $ kysely-migrate to --help + $ kysely-migrate up --help + + Options: + -h, --help Display this message + -v, --version Display version number " + `) +}) + +test('--version', () => { + const { stdout } = run(['--version']) + expect(stdout).toContain(`kysely-migrate/${version} `) +}) + +test('init', () => { + fs.ensureDirSync(genPath) + const { stdout } = run(['init'], { cwd: genPath }) + const generatedFiles = fs.readdirSync(genPath).sort() + + expect(stdout).toContain('Created config file at kysely-migrate.config.ts') + expect(generatedFiles).toMatchInlineSnapshot(` + [ + "kysely-migrate.config.ts", + ] + `) +}) diff --git a/src/cli.ts b/src/cli.ts index 5ee5450..a96676a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,6 +3,7 @@ import { intro, outro } from '@clack/prompts' import { cac } from 'cac' import pc from 'picocolors' +import { type CodegenOptions, codegen } from './commands/codegen.js' import { type CreateOptions, create } from './commands/create.js' import { type DownOptions, down } from './commands/down.js' import { type InitOptions, init } from './commands/init.js' @@ -13,6 +14,17 @@ import { version } from './version.js' const cli = cac('kysely-migrate') +cli + .command('codegen', 'generate types from database metadata') + .option('-c, --config ', '[string] path to config file') + .option( + '-d, --debug', + '[boolean] debug generated types against kysely-codegen package', + ) + .option('-r, --root ', '[string] root path to resolve config from') + .example((name) => `${name} codegen`) + .action(async (options: CodegenOptions) => await codegen(options)) + cli .command('create', 'create new migration') .option('-n, --name ', '[string] migration name') diff --git a/src/commands/codegen.ts b/src/commands/codegen.ts new file mode 100644 index 0000000..e27832d --- /dev/null +++ b/src/commands/codegen.ts @@ -0,0 +1,67 @@ +import { Cli } from 'kysely-codegen' +import pc from 'picocolors' + +import { relative } from 'path' +import { readFile, writeFile } from 'fs/promises' +import { getTypes } from '../utils/codegen/getTypes.js' +import { findConfig } from '../utils/findConfig.js' +import { loadConfig } from '../utils/loadConfig.js' + +export type CodegenOptions = { + config?: string | undefined + debug?: boolean | undefined + root?: string | undefined +} + +export async function codegen(options: CodegenOptions) { + // Get cli config file + const configPath = await findConfig(options, true) + + const config = await loadConfig({ configPath }) + + const db = config.db + if (!db) throw new Error('`db` config required to generate types.') + + const tables = await db.introspection.getTables() + + if (!config.codegen) + throw new Error('`codegen` config required to generate types.') + + const content = getTypes( + tables, + config.codegen.dialect, + config.codegen.definitions, + ) + await writeFile(config.codegen.out, content) + + if (options.debug) { + const cli = new Cli() + const outFile = `${config.codegen.out}`.replace('.ts', '2.ts') + await cli.generate({ + camelCase: false, + dialectName: undefined, + envFile: undefined, + excludePattern: undefined, + includePattern: undefined, + logLevel: 0, + outFile, + print: false, + schema: undefined, + typeOnlyImports: true, + url: `mysql://${process.env.database_username}:${ + process.env.database_password + }@${process.env.database_host}/${process.env.database_name}${ + process.env.enable_psdb === 'true' + ? '?ssl={"rejectUnauthorized":true}' + : '' + }`, + verify: false, + }) + const content2 = await readFile(outFile, 'utf-8') + console.log(content) + console.log(content2) + } + + const codegenRelativeFilePath = relative(process.cwd(), config.codegen.out) + return `Created ${pc.green(codegenRelativeFilePath)}` +} diff --git a/src/commands/create.ts b/src/commands/create.ts index 4f5096b..9e72fb0 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -7,6 +7,7 @@ import { humanId } from 'human-id' import pc from 'picocolors' import { findConfig } from '../utils/findConfig.js' +import { getMigrator } from '../utils/getMigrator.js' import { loadConfig } from '../utils/loadConfig.js' export type CreateOptions = { @@ -17,19 +18,15 @@ export type CreateOptions = { export async function create(options: CreateOptions) { // Get cli config file - const configPath = await findConfig(options) - if (!configPath) { - if (options.config) - throw new Error(`Config not found at ${pc.gray(options.config)}`) - throw new Error('Config not found') - } + const configPath = await findConfig(options, true) const config = await loadConfig({ configPath }) + const migrator = getMigrator(config) - const migrationsDir = config.out + const migrationsDir = config.migrationFolder if (!existsSync(migrationsDir)) await mkdir(migrationsDir) - const migrations = await config.migrator.getMigrations() + const migrations = await migrator.getMigrations() const migrationsCount = migrations.length const migrationNumber = (migrationsCount + 1).toString().padStart(4, '0') diff --git a/src/commands/down.ts b/src/commands/down.ts index 5e21d1f..c2a81c3 100644 --- a/src/commands/down.ts +++ b/src/commands/down.ts @@ -3,10 +3,10 @@ import { mkdir } from 'node:fs/promises' import { setTimeout as sleep } from 'node:timers/promises' import { cancel, confirm, isCancel, spinner } from '@clack/prompts' import { type MigrationResultSet } from 'kysely' -import pc from 'picocolors' import { findConfig } from '../utils/findConfig.js' import { getAppliedMigrationsCount } from '../utils/getAppliedMigrationsCount.js' +import { getMigrator } from '../utils/getMigrator.js' import { loadConfig } from '../utils/loadConfig.js' import { logResultSet } from '../utils/logResultSet.js' @@ -18,19 +18,15 @@ export type DownOptions = { export async function down(options: DownOptions) { // Get cli config file - const configPath = await findConfig(options) - if (!configPath) { - if (options.config) - throw new Error(`Config not found at ${pc.gray(options.config)}`) - throw new Error('Config not found') - } + const configPath = await findConfig(options, true) const config = await loadConfig({ configPath }) + const migrator = getMigrator(config) - const migrationsDir = config.out + const migrationsDir = config.migrationFolder if (!existsSync(migrationsDir)) await mkdir(migrationsDir) - const migrations = await config.migrator.getMigrations() + const migrations = await migrator.getMigrations() const executedMigrations = migrations.filter((m) => m.executedAt) if (executedMigrations.length === 0) return 'No migrations executed.' @@ -54,12 +50,12 @@ export async function down(options: DownOptions) { if (options.reset) { // TODO: migrator.migrateTo(NO_MIGRATIONS) throwing when run with linked package const migration = migrations[0]! - resultSet = await config.migrator.migrateTo(migration.name) + resultSet = await migrator.migrateTo(migration.name) if (!resultSet.error) { - const { results } = await config.migrator.migrateDown() + const { results } = await migrator.migrateDown() resultSet.results!.push(...(results ?? [])) } - } else resultSet = await config.migrator.migrateDown() + } else resultSet = await migrator.migrateDown() const { error, results = [] } = resultSet s.stop('Ran migrations', error ? 1 : 0) diff --git a/src/commands/list.ts b/src/commands/list.ts index 66783cc..2ee17f6 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -2,6 +2,7 @@ import pc from 'picocolors' import { S_BAR, S_INFO, S_SUCCESS, message } from '../utils/clack.js' import { findConfig } from '../utils/findConfig.js' +import { getMigrator } from '../utils/getMigrator.js' import { loadConfig } from '../utils/loadConfig.js' export type ListOptions = { @@ -11,16 +12,12 @@ export type ListOptions = { export async function list(options: ListOptions) { // Get cli config file - const configPath = await findConfig(options) - if (!configPath) { - if (options.config) - throw new Error(`Config not found at ${pc.gray(options.config)}`) - throw new Error('Config not found') - } + const configPath = await findConfig(options, true) const config = await loadConfig({ configPath }) + const migrator = getMigrator(config) - const migrations = await config.migrator.getMigrations() + const migrations = await migrator.getMigrations() const migrationsCount = migrations.length process.stdout.write(`${pc.gray(S_BAR)}\n`) diff --git a/src/commands/to.ts b/src/commands/to.ts index f0cd890..f6e8519 100644 --- a/src/commands/to.ts +++ b/src/commands/to.ts @@ -6,6 +6,7 @@ import pc from 'picocolors' import { findConfig } from '../utils/findConfig.js' import { getAppliedMigrationsCount } from '../utils/getAppliedMigrationsCount.js' +import { getMigrator } from '../utils/getMigrator.js' import { loadConfig } from '../utils/loadConfig.js' import { logResultSet } from '../utils/logResultSet.js' @@ -17,22 +18,17 @@ export type ToOptions = { export async function to(options: ToOptions) { // Get cli config file - const configPath = await findConfig(options) - if (!configPath) { - if (options.config) - throw new Error(`Config not found at ${pc.gray(options.config)}`) - throw new Error('Config not found') - } + const configPath = await findConfig(options, true) const config = await loadConfig({ configPath }) + const migrator = getMigrator(config) - const migrationsDir = config.out + const migrationsDir = config.migrationFolder if (!existsSync(migrationsDir)) await mkdir(migrationsDir) - const migrations = await config.migrator.getMigrations() + const migrations = await migrator.getMigrations() if (migrations.length === 0) return 'No migrations.' - if (migrations.length === 1) return 'No enough migrations.' let migration: string | symbol @@ -76,7 +72,7 @@ export async function to(options: ToOptions) { s.start('Running migrations') await sleep(500) // so spinner has a chance :) - const resultSet = await config.migrator.migrateTo(migration as string) + const resultSet = await migrator.migrateTo(migration as string) const { error, results = [] } = resultSet s.stop('Ran migrations', error ? 1 : 0) diff --git a/src/commands/up.ts b/src/commands/up.ts index 8ecb5eb..dbd4e5f 100644 --- a/src/commands/up.ts +++ b/src/commands/up.ts @@ -2,11 +2,11 @@ import { existsSync } from 'node:fs' import { mkdir } from 'node:fs/promises' import { setTimeout as sleep } from 'node:timers/promises' import { spinner } from '@clack/prompts' -import type { MigrationResultSet } from 'kysely' -import pc from 'picocolors' +import { type MigrationResultSet } from 'kysely' import { findConfig } from '../utils/findConfig.js' import { getAppliedMigrationsCount } from '../utils/getAppliedMigrationsCount.js' +import { getMigrator } from '../utils/getMigrator.js' import { loadConfig } from '../utils/loadConfig.js' import { logResultSet } from '../utils/logResultSet.js' @@ -18,19 +18,15 @@ export type UpOptions = { export async function up(options: UpOptions) { // Get cli config file - const configPath = await findConfig(options) - if (!configPath) { - if (options.config) - throw new Error(`Config not found at ${pc.gray(options.config)}`) - throw new Error('Config not found') - } + const configPath = await findConfig(options, true) const config = await loadConfig({ configPath }) + const migrator = getMigrator(config) - const migrationsDir = config.out + const migrationsDir = config.migrationFolder if (!existsSync(migrationsDir)) await mkdir(migrationsDir) - const migrations = await config.migrator.getMigrations() + const migrations = await migrator.getMigrations() const pendingMigrations = migrations.filter((m) => !m.executedAt) if (pendingMigrations.length === 0) return 'No pending migrations.' @@ -40,8 +36,8 @@ export async function up(options: UpOptions) { await sleep(500) // so spinner has a chance :) let resultSet: MigrationResultSet - if (options.latest) resultSet = await config.migrator.migrateToLatest() - else resultSet = await config.migrator.migrateUp() + if (options.latest) resultSet = await migrator.migrateToLatest() + else resultSet = await migrator.migrateUp() const { error, results = [] } = resultSet s.stop('Ran migrations', error ? 1 : 0) diff --git a/src/config.ts b/src/config.ts index 6fd6235..77ac4cf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,14 +1,47 @@ -import { Migrator } from 'kysely' +import { + type FileMigrationProviderFS, + type FileMigrationProviderPath, + type Kysely, + Migrator, +} from 'kysely' -export type Config = { - migrator: Migrator - out: string -} +import { type Definitions } from './utils/codegen/types.js' + +export type Config = + | { + codegen?: Evaluate | undefined + db: Kysely + fs?: FileMigrationProviderFS | undefined + path?: FileMigrationProviderPath | undefined + migrationFolder: string + } + | { + codegen?: Evaluate | undefined + db?: Kysely | undefined + migrationFolder: string + migrator: Migrator + } + +type Codegen = + | { + definitions?: Evaluate | undefined + dialect: 'mysql' | 'postgres' | 'sqlite' + out: string + } + | { + definitions: Evaluate | undefined + dialect?: 'mysql' | 'postgres' | 'sqlite' + out: string + } export function defineConfig( - config: Config | (() => Config | Promise), + config: + | Evaluate + | (() => Evaluate | Promise>), ) { return config } export const defaultConfig = {} + +type Evaluate = { [key in keyof type]: type[key] } & unknown diff --git a/src/exports/index.test.ts b/src/exports/index.test.ts index a7bd4d3..441d7cd 100644 --- a/src/exports/index.test.ts +++ b/src/exports/index.test.ts @@ -7,6 +7,9 @@ test('exports', () => { [ "defineConfig", "loadEnv", + "mysqlDefinitions", + "postgresDefinitions", + "sqliteDefinitions", "version", ] `) diff --git a/src/exports/index.ts b/src/exports/index.ts index 06241e5..b339e7f 100644 --- a/src/exports/index.ts +++ b/src/exports/index.ts @@ -5,4 +5,8 @@ export { export { loadEnv } from '../utils/loadEnv.js' +export { mysqlDefinitions } from '../utils/codegen/definitions/mysql.js' +export { postgresDefinitions } from '../utils/codegen/definitions/postgres.js' +export { sqliteDefinitions } from '../utils/codegen/definitions/sqlite.js' + export { version } from '../version.js' diff --git a/src/utils/codegen/declarations.ts b/src/utils/codegen/declarations.ts new file mode 100644 index 0000000..b293630 --- /dev/null +++ b/src/utils/codegen/declarations.ts @@ -0,0 +1,159 @@ +import { SyntaxKind, factory } from 'typescript' + +export const kyselyColumnTypeIdentifier = factory.createIdentifier('ColumnType') +export const kyselyColumnTypeImportSpecifier = factory.createImportSpecifier( + true, + undefined, + kyselyColumnTypeIdentifier, +) + +export const generatedIdentifier = factory.createIdentifier('Generated') +export const generatedTypeAlias = factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + generatedIdentifier, + [ + factory.createTypeParameterDeclaration( + undefined, + factory.createIdentifier('columnType'), + undefined, + undefined, + ), + ], + factory.createConditionalTypeNode( + factory.createTypeReferenceNode( + factory.createIdentifier('columnType'), + undefined, + ), + factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createInferTypeNode( + factory.createTypeParameterDeclaration( + undefined, + factory.createIdentifier('selectType'), + undefined, + undefined, + ), + ), + factory.createInferTypeNode( + factory.createTypeParameterDeclaration( + undefined, + factory.createIdentifier('insertType'), + undefined, + undefined, + ), + ), + factory.createInferTypeNode( + factory.createTypeParameterDeclaration( + undefined, + factory.createIdentifier('updateType'), + undefined, + undefined, + ), + ), + ]), + factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createTypeReferenceNode( + factory.createIdentifier('selectType'), + undefined, + ), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode( + factory.createIdentifier('insertType'), + undefined, + ), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + ]), + factory.createTypeReferenceNode( + factory.createIdentifier('updateType'), + undefined, + ), + ]), + factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createTypeReferenceNode( + factory.createIdentifier('columnType'), + undefined, + ), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode( + factory.createIdentifier('columnType'), + undefined, + ), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + ]), + factory.createTypeReferenceNode( + factory.createIdentifier('columnType'), + undefined, + ), + ]), + ), +) + +export const jsonIdentifier = factory.createIdentifier('Json') +export const jsonValueIdentifier = factory.createIdentifier('JsonValue') +export const jsonArrayIdentifier = factory.createIdentifier('JsonArray') +export const jsonObjectIndentifier = factory.createIdentifier('JsonObject') +export const jsonPrimitiveIdentifier = factory.createIdentifier('JsonPrimitive') + +export const jsonTypeAlias = factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + jsonIdentifier, + undefined, + factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createTypeReferenceNode(jsonValueIdentifier, undefined), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]), +) + +export const jsonValueTypeAlias = factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + jsonValueIdentifier, + undefined, + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(jsonArrayIdentifier, undefined), + factory.createTypeReferenceNode(jsonObjectIndentifier, undefined), + factory.createTypeReferenceNode(jsonPrimitiveIdentifier, undefined), + ]), +) + +export const jsonArrayTypeAlias = factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + jsonArrayIdentifier, + undefined, + factory.createArrayTypeNode( + factory.createTypeReferenceNode(jsonValueIdentifier, undefined), + ), +) + +export const jsonObjectTypeAlias = factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + jsonObjectIndentifier, + undefined, + factory.createMappedTypeNode( + undefined, + factory.createTypeParameterDeclaration( + undefined, + factory.createIdentifier('key'), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + undefined, + ), + undefined, + factory.createToken(SyntaxKind.QuestionToken), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(jsonValueIdentifier, undefined), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + ]), + undefined, + ), +) + +export const jsonPrimitiveTypeAlias = factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + jsonPrimitiveIdentifier, + undefined, + factory.createUnionTypeNode([ + factory.createKeywordTypeNode(SyntaxKind.BooleanKeyword), + factory.createLiteralTypeNode(factory.createNull()), + factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]), +) diff --git a/src/utils/codegen/definitions/mysql.ts b/src/utils/codegen/definitions/mysql.ts new file mode 100644 index 0000000..8ff1c26 --- /dev/null +++ b/src/utils/codegen/definitions/mysql.ts @@ -0,0 +1,91 @@ +import { SyntaxKind, factory } from 'typescript' +import { + jsonArrayTypeAlias, + jsonIdentifier, + jsonObjectTypeAlias, + jsonPrimitiveTypeAlias, + jsonTypeAlias, + jsonValueTypeAlias, + kyselyColumnTypeImportSpecifier, +} from '../declarations.js' +import { type Definitions } from '../types.js' + +// TODO: Complete definitions +export const mysqlDefinitions = { + bigint: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + binary: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + bit: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + blob: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + char: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + date: factory.createTypeReferenceNode( + factory.createIdentifier('Date'), + undefined, + ), + datetime: factory.createTypeReferenceNode( + factory.createIdentifier('Date'), + undefined, + ), + // decimal: new IdentifierNode('Decimal'), + double: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + float: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + // geomcollection: new ArrayExpressionNode(new IdentifierNode('Geometry')), // Specified as "geometrycollection" in Adminer. + // geometry: new IdentifierNode('Geometry'), + int: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + json: { + imports: { kysely: [kyselyColumnTypeImportSpecifier] }, + declarations: [ + jsonTypeAlias, + jsonValueTypeAlias, + jsonArrayTypeAlias, + jsonObjectTypeAlias, + jsonPrimitiveTypeAlias, + ], + value: factory.createTypeReferenceNode(jsonIdentifier, undefined), + }, + // linestring: new IdentifierNode('LineString'), + longblob: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + longtext: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + mediumblob: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + mediumint: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + mediumtext: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + // multilinestring: new ArrayExpressionNode(new IdentifierNode('LineString')), + // multipoint: new ArrayExpressionNode(new IdentifierNode('Point')), + // multipolygon: new ArrayExpressionNode(new IdentifierNode('Polygon')), + // point: new IdentifierNode('Point'), + // polygon: new IdentifierNode('Polygon'), + set: factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword), + smallint: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + text: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + time: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + timestamp: factory.createTypeReferenceNode( + factory.createIdentifier('Date'), + undefined, + ), + tinyblob: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + tinyint: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + tinytext: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + varbinary: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + varchar: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + year: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), +} satisfies Definitions diff --git a/src/utils/codegen/definitions/postgres.ts b/src/utils/codegen/definitions/postgres.ts new file mode 100644 index 0000000..f6eef66 --- /dev/null +++ b/src/utils/codegen/definitions/postgres.ts @@ -0,0 +1,68 @@ +import { SyntaxKind, factory } from 'typescript' + +import { + jsonArrayTypeAlias, + jsonIdentifier, + jsonObjectTypeAlias, + jsonPrimitiveTypeAlias, + jsonTypeAlias, + jsonValueTypeAlias, + kyselyColumnTypeImportSpecifier, +} from '../declarations.js' +import { type DefinitionNode, type Definitions } from '../types.js' + +const json = { + imports: { kysely: [kyselyColumnTypeImportSpecifier] }, + declarations: [ + jsonTypeAlias, + jsonValueTypeAlias, + jsonArrayTypeAlias, + jsonObjectTypeAlias, + jsonPrimitiveTypeAlias, + ], + value: factory.createTypeReferenceNode(jsonIdentifier, undefined), +} satisfies DefinitionNode + +// TODO: Complete definitions +export const postgresDefinitions = { + bit: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + // bool: new IdentifierNode('boolean'), // Specified as "boolean" in Adminer. + box: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + bpchar: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), // Specified as "character" in Adminer. + bytea: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + cidr: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + // circle: new IdentifierNode('Circle'), + // date: new IdentifierNode('Timestamp'), + float4: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), // Specified as "real" in Adminer. + float8: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), // Specified as "double precision" in Adminer. + inet: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + int2: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), // Specified in 'pg' source code. + int4: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), // Specified in 'pg' source code. + // int8: new IdentifierNode('Int8'), // Specified as "bigint" in Adminer. + // interval: new IdentifierNode('Interval'), + json, + jsonb: json, + line: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + lseg: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + macaddr: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + money: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + // numeric: new IdentifierNode('Numeric'), + oid: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), // Specified in 'pg' source code. + path: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + // point: new IdentifierNode('Point'), + polygon: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + text: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + time: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + // timestamp: new IdentifierNode('Timestamp'), + // timestamptz: new IdentifierNode('Timestamp'), + tsquery: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + tsvector: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + txid_snapshot: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + uuid: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + varbit: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), // Specified as "bit varying" in Adminer. + varchar: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), // Specified as "character varying" in Adminer. + xml: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), +} satisfies Definitions diff --git a/src/utils/codegen/definitions/sqlite.ts b/src/utils/codegen/definitions/sqlite.ts new file mode 100644 index 0000000..b7f8f88 --- /dev/null +++ b/src/utils/codegen/definitions/sqlite.ts @@ -0,0 +1,16 @@ +import { SyntaxKind, factory } from 'typescript' + +import { type Definitions } from '../types.js' + +export const sqliteDefinitions = { + any: factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword), + blob: factory.createTypeReferenceNode( + factory.createIdentifier('Buffer'), + undefined, + ), + boolean: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + integer: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + numeric: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + real: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + text: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), +} satisfies Definitions diff --git a/src/utils/codegen/getTypes.ts b/src/utils/codegen/getTypes.ts new file mode 100644 index 0000000..d45bbb2 --- /dev/null +++ b/src/utils/codegen/getTypes.ts @@ -0,0 +1,172 @@ +import { capitalCase } from 'change-case' +import { type TableMetadata } from 'kysely' +import { + EmitHint, + type ImportSpecifier, + NewLineKind, + ScriptTarget, + SyntaxKind, + type TypeAliasDeclaration, + type TypeNode, + createPrinter, + createSourceFile, + factory, +} from 'typescript' + +import { + generatedIdentifier, + generatedTypeAlias, + kyselyColumnTypeImportSpecifier, +} from './declarations.js' +import { mysqlDefinitions } from './definitions/mysql.js' +import { postgresDefinitions } from './definitions/postgres.js' +import { sqliteDefinitions } from './definitions/sqlite.js' +import { type DefinitionNode, type Definitions, type Dialect } from './types.js' + +const dialectDefinitions = { + mysql: mysqlDefinitions, + postgres: postgresDefinitions, + sqlite: sqliteDefinitions, +} satisfies Record + +export function getTypes( + tableMetadata: TableMetadata[], + dialect: Dialect | undefined, + customDefinitions: Definitions | undefined = {}, +) { + // TODO: Parse out enums (https://github.com/RobinBlomberg/kysely-codegen/blob/b749a677e6bfd7370559767e57e4c69746898f94/src/dialects/mysql/mysql-introspector.ts#L28-L46) + // TODO: Tests for different dialects, custom definitions, table metadata, etc. + + // Get dialect node mapping + if (!dialect && !customDefinitions) + throw new Error( + '`dialect` and/or `customDefinitions` required for codegen.', + ) + const definitions = { + ...(dialect && dialectDefinitions[dialect]), + ...customDefinitions, + } + + const nodes = [] + const importsMap: Map> = new Map() + const typeDeclarations: Set = new Set() + + // Create types + const dbTypeParameters = [] + for (const table of tableMetadata) { + // Create type property for each column + const columnProperties = [] + for (const column of table.columns) { + // Get type from lookup + let type: TypeNode + if (column.dataType in definitions) { + const definition = definitions[ + column.dataType as keyof typeof definitions + ] as TypeNode | DefinitionNode + if ('value' in definition) { + type = definition.value + for (const [name, imports] of Object.entries(definition.imports)) { + if (importsMap.has(name)) { + const nameImports = importsMap.get(name)! + importsMap.set(name, new Set([...nameImports, ...imports])) + } else importsMap.set(name, new Set(imports)) + } + for (const declaration of definition.declarations) { + typeDeclarations.add(declaration) + } + } else type = definition + } else type = factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword) + + // Create node based on properties (e.g. nullable, default) + let columnTypeNode: TypeNode + if (column.isNullable) + columnTypeNode = factory.createUnionTypeNode([ + type, + factory.createLiteralTypeNode(factory.createNull()), + ]) + else if (column.hasDefaultValue) { + if (importsMap.has('kysely')) { + const kyselyImports = importsMap.get('kysely')! + kyselyImports.add(kyselyColumnTypeImportSpecifier) + importsMap.set('kysely', kyselyImports) + } else { + importsMap.set('kysely', new Set([kyselyColumnTypeImportSpecifier])) + } + typeDeclarations.add(generatedTypeAlias) + columnTypeNode = factory.createTypeReferenceNode(generatedIdentifier, [ + type, + ]) + } else columnTypeNode = type + + // Create property + const columnProperty = factory.createPropertySignature( + undefined, + factory.createIdentifier(column.name), + undefined, + columnTypeNode, + ) + columnProperties.push(columnProperty) + } + + // Create table type alias + const tableTypeName = capitalCase(table.name) + const tableTypeIdentifier = factory.createIdentifier(tableTypeName) + const tableTypeAlias = factory.createTypeAliasDeclaration( + [factory.createModifier(SyntaxKind.ExportKeyword)], + tableTypeIdentifier, + undefined, + factory.createTypeLiteralNode(columnProperties), + ) + nodes.push(tableTypeAlias) + + // Create table type property for encompassing `DB` type + const tableDbTypeParameter = factory.createPropertySignature( + undefined, + tableTypeIdentifier, + undefined, + factory.createTypeReferenceNode(tableTypeIdentifier, undefined), + ) + dbTypeParameters.push(tableDbTypeParameter) + } + + // Add in type declarations + nodes.unshift(...typeDeclarations) + + // Add imports statement to start of nodes + for (const [name, imports] of importsMap.entries()) { + const importDeclaration = factory.createImportDeclaration( + undefined, + factory.createImportClause( + false, + undefined, + factory.createNamedImports([...imports.values()]), + ), + factory.createStringLiteral(name), + undefined, + ) + nodes.unshift(importDeclaration) + } + + // Create `DB` type alias + const dbNode = factory.createTypeAliasDeclaration( + [factory.createModifier(SyntaxKind.ExportKeyword)], + factory.createIdentifier('DB'), + undefined, + factory.createTypeLiteralNode(dbTypeParameters), + ) + nodes.push(dbNode) + + // Print and combine all nodes + let content = '/** generated by kysely-migrate */\n' + const printer = createPrinter({ newLine: NewLineKind.LineFeed }) + for (const node of nodes) { + content += printer.printNode( + EmitHint.Unspecified, + node, + createSourceFile('', '', ScriptTarget.Latest), + ) + content += '\n\n' + } + + return content +} diff --git a/src/utils/codegen/types.ts b/src/utils/codegen/types.ts new file mode 100644 index 0000000..4a00a5a --- /dev/null +++ b/src/utils/codegen/types.ts @@ -0,0 +1,20 @@ +import { + type ImportSpecifier, + type TypeAliasDeclaration, + type TypeNode, +} from 'typescript' + +import { type Config } from '../../config.js' + +export type Dialect = NonNullable['dialect']> + +export type Definitions = Record< + key, + TypeNode | DefinitionNode +> + +export type DefinitionNode = { + imports: { [key in 'kysely' | string]: readonly ImportSpecifier[] } + declarations: readonly TypeAliasDeclaration[] + value: TypeNode +} diff --git a/src/utils/findConfig.ts b/src/utils/findConfig.ts index e0c4aae..83ae290 100644 --- a/src/utils/findConfig.ts +++ b/src/utils/findConfig.ts @@ -1,23 +1,37 @@ import { existsSync } from 'node:fs' import { resolve } from 'node:path' import { findUp } from 'find-up' +import pc from 'picocolors' // Do not reorder // In order of preference files are checked -const configFiles = ['kysely-migrate.config.ts', 'kysely-migrate.config.mts'] +const configFiles = [ + 'kysely-migrate.config.ts', + 'kysely-migrate.config.mts', + 'kysely.config.ts', + 'kysely.config.mts', +] type FindConfigParameters = { config?: string | undefined root?: string | undefined } -export async function findConfig(parameters: FindConfigParameters = {}) { +export async function findConfig( + parameters: FindConfigParameters, + throwIfNotFound?: throwIfNotFound, +): Promise { const { config, root } = parameters const rootDir = resolve(root || process.cwd()) if (config) { const path = resolve(rootDir, config) - if (existsSync(path)) return path - return + if (existsSync(path)) return path as any + if (throwIfNotFound) + throw new Error(`Config not found at ${pc.gray(config)}`) } - return findUp(configFiles, { cwd: rootDir }) + + const configPath = await findUp(configFiles, { cwd: rootDir }) + if (throwIfNotFound && !configPath) throw new Error('Config not found') + + return configPath as any } diff --git a/src/utils/getMigrator.ts b/src/utils/getMigrator.ts new file mode 100644 index 0000000..e04f353 --- /dev/null +++ b/src/utils/getMigrator.ts @@ -0,0 +1,14 @@ +import nodeFs from 'node:fs/promises' +import nodePath from 'node:path' +import { FileMigrationProvider, Migrator } from 'kysely' + +import { type Config } from '../config.js' + +export function getMigrator(config: Config) { + if ('migrator' in config) return config.migrator + + const { db, fs = nodeFs, path = nodePath } = config + const migrationFolder = nodePath.resolve(config.migrationFolder) + const provider = new FileMigrationProvider({ fs, migrationFolder, path }) + return new Migrator({ db, provider }) +} diff --git a/src/utils/loadConfig.ts b/src/utils/loadConfig.ts index faaeb32..4459f20 100644 --- a/src/utils/loadConfig.ts +++ b/src/utils/loadConfig.ts @@ -1,6 +1,6 @@ import { bundleRequire } from 'bundle-require' -import type { Config } from '../config.js' +import { type Config } from '../config.js' type LoadConfigParameters = { configPath: string diff --git a/src/utils/logResultSet.ts b/src/utils/logResultSet.ts index caee9ac..3c0d1d1 100644 --- a/src/utils/logResultSet.ts +++ b/src/utils/logResultSet.ts @@ -1,4 +1,4 @@ -import type { MigrationResultSet } from 'kysely' +import { type MigrationResultSet } from 'kysely' import pc from 'picocolors' import { S_BAR, S_ERROR, S_INFO, S_SUCCESS, message } from './clack.js' diff --git a/tsconfig.json b/tsconfig.json index 9908551..5032a5f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig.build.json", - "include": [".scripts/**/*.ts", "src/**/*.ts", "test/**/*.ts"], + "include": [".scripts/**/*.ts", "src/**/*.ts"], "exclude": [], "compilerOptions": { "types": ["bun-types"] diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d690189 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + coverage: { + reporter: process.env.CI ? ['lcov'] : ['text', 'json', 'html'], + exclude: ['**/dist/**', '**/*.test.ts', '**/*.test-d.ts'], + }, + }, +})