From 895fd22cdd782e15d8b95fd29ddf5fdbd6f1e331 Mon Sep 17 00:00:00 2001 From: Barry Michael Doyle Date: Wed, 19 Apr 2023 16:53:42 +0200 Subject: [PATCH] added an implementation for types --- .eslintignore | 3 +- .eslintrc | 1 + .prettierrc | 1 + example/database.types.ts | 63 +++++ example/generated.ts | 238 ++++++++++++++++++ example/supabase.ts | 5 + package-lock.json | 78 +++--- package.json | 2 + scripts/generate.js | 6 + src/cli.ts | 13 +- src/generate.ts | 118 ++++----- src/utils/generateHooks/generateHooks.ts | 78 ++++++ .../toHookName.spec.ts | 30 ++- src/utils/{ => generateHooks}/toHookName.ts | 7 +- src/utils/generateTypes/generateTypes.ts | 68 +++++ src/utils/generateTypes/toTypeName.ts | 14 ++ supabase-react-query-codegen.config.js | 8 + 17 files changed, 607 insertions(+), 126 deletions(-) create mode 100644 example/database.types.ts create mode 100644 example/generated.ts create mode 100644 example/supabase.ts create mode 100644 scripts/generate.js create mode 100644 src/utils/generateHooks/generateHooks.ts rename src/utils/{spec => generateHooks}/toHookName.spec.ts (62%) rename src/utils/{ => generateHooks}/toHookName.ts (70%) create mode 100644 src/utils/generateTypes/generateTypes.ts create mode 100644 src/utils/generateTypes/toTypeName.ts create mode 100644 supabase-react-query-codegen.config.js diff --git a/.eslintignore b/.eslintignore index b512c09..0539464 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +scripts \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index bd03507..d310bf9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,6 +12,7 @@ ], "rules": { "eqeqeq": "error", + "@typescript-eslint/ban-types": "off", "linebreak-style": ["error", "unix"], "prettier/prettier": "error", "unused-imports/no-unused-imports": "error", diff --git a/.prettierrc b/.prettierrc index 7caba5b..51deda6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,7 @@ "useTabs": false, "tabWidth": 2, "singleQuote": true, + "semi": true, "printWidth": 80, "trailingComma": "es5", "endOfLine": "lf" diff --git a/example/database.types.ts b/example/database.types.ts new file mode 100644 index 0000000..a80f179 --- /dev/null +++ b/example/database.types.ts @@ -0,0 +1,63 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json } + | Json[]; + +export interface Database { + public: { + Tables: { + todo_items: { + Row: { + created_at: string; + description: string; + id: string; + name: string; + }; + Insert: { + created_at?: string; + description: string; + id?: string; + name: string; + }; + Update: { + created_at?: string; + description?: string; + id?: string; + name?: string; + }; + }; + profiles: { + Row: { + first_name: string | null; + id: string; + last_name: string | null; + }; + Insert: { + first_name?: string | null; + id: string; + last_name?: string | null; + }; + Update: { + first_name?: string | null; + id?: string; + last_name?: string | null; + }; + }; + }; + Views: { + [_ in never]: never; + }; + Functions: { + [_ in never]: never; + }; + Enums: { + [_ in never]: never; + }; + CompositeTypes: { + [_ in never]: never; + }; + }; +} diff --git a/example/generated.ts b/example/generated.ts new file mode 100644 index 0000000..d153027 --- /dev/null +++ b/example/generated.ts @@ -0,0 +1,238 @@ +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { Database } from './database.types'; +import { supabase } from './supabase'; + +export type GetTodoItemRequest = string; +export type GetTodoItemResponse = { + created_at: string; + description: string; + id: string; + name: string; +}; +export type GetAllTodoItemsResponse = { + created_at: string; + description: string; + id: string; + name: string; +}[]; +export type AddTodoItemRequest = { + created_at?: string; + description: string; + id?: string; + name: string; +}; +export type UpdateTodoItemRequest = { + id: string; + changes: { + created_at?: string; + description?: string; + id?: string; + name?: string; + }; +}; +export type DeleteTodoItemRequest = string; +export type GetProfileRequest = string; +export type GetProfileResponse = { + first_name: string; + id: string; + last_name: string; +}; +export type GetAllProfilesResponse = { + first_name: string; + id: string; + last_name: string; +}[]; +export type AddProfileRequest = { + first_name?: string; + id: string; + last_name?: string; +}; +export type UpdateProfileRequest = { + id: string; + changes: { first_name?: string; id?: string; last_name?: string }; +}; +export type DeleteProfileRequest = string; + +export function useGetTodoItem(id: string) { + return useQuery( + ['todo_items', id], + async () => { + const { data, error } = await supabase + .from('todo_items') + .select('*') + .eq('id', id) + .single(); + + if (error) { + throw error; + } + + if (!data) { + throw new Error('No data found'); + } + + return data; + }, + { + enabled: !!id, + } + ); +} + +export function useGetAllTodoItems() { + return useQuery( + ['todo_items'], + async () => { + const { data, error } = await supabase + .from('todo_items') + .select(); + if (error) throw error; + return data as Database['public']['Tables']['todo_items']['Row'][]; + } + ); +} + +export function useAddTodoItem() { + const queryClient = useQueryClient(); + return useMutation( + (item: Database['public']['Tables']['todo_items']['Insert']) => + supabase + .from('todo_items') + .insert(item) + .single(), + { + onSuccess: () => { + queryClient.invalidateQueries('todo_items'); + }, + } + ); +} + +export function useUpdateTodoItem() { + const queryClient = useQueryClient(); + return useMutation( + (item: { + id: string; + changes: Database['public']['Tables']['todo_items']['Update']; + }) => + supabase + .from('todo_items') + .update(item.changes) + .eq('id', item.id) + .single(), + { + onSuccess: () => { + queryClient.invalidateQueries('todo_items'); + }, + } + ); +} + +export function useDeleteTodoItem() { + const queryClient = useQueryClient(); + return useMutation( + (id: string) => + supabase + .from('todo_items') + .delete() + .eq('id', id) + .single(), + { + onSuccess: () => { + queryClient.invalidateQueries('todo_items'); + }, + } + ); +} + +export function useGetProfile(id: string) { + return useQuery( + ['profiles', id], + async () => { + const { data, error } = await supabase + .from('profiles') + .select('*') + .eq('id', id) + .single(); + + if (error) { + throw error; + } + + if (!data) { + throw new Error('No data found'); + } + + return data; + }, + { + enabled: !!id, + } + ); +} + +export function useGetAllProfiles() { + return useQuery( + ['profiles'], + async () => { + const { data, error } = await supabase + .from('profiles') + .select(); + if (error) throw error; + return data as Database['public']['Tables']['profiles']['Row'][]; + } + ); +} + +export function useAddProfile() { + const queryClient = useQueryClient(); + return useMutation( + (item: Database['public']['Tables']['profiles']['Insert']) => + supabase + .from('profiles') + .insert(item) + .single(), + { + onSuccess: () => { + queryClient.invalidateQueries('profiles'); + }, + } + ); +} + +export function useUpdateProfile() { + const queryClient = useQueryClient(); + return useMutation( + (item: { + id: string; + changes: Database['public']['Tables']['profiles']['Update']; + }) => + supabase + .from('profiles') + .update(item.changes) + .eq('id', item.id) + .single(), + { + onSuccess: () => { + queryClient.invalidateQueries('profiles'); + }, + } + ); +} + +export function useDeleteProfile() { + const queryClient = useQueryClient(); + return useMutation( + (id: string) => + supabase + .from('profiles') + .delete() + .eq('id', id) + .single(), + { + onSuccess: () => { + queryClient.invalidateQueries('profiles'); + }, + } + ); +} diff --git a/example/supabase.ts b/example/supabase.ts new file mode 100644 index 0000000..3b6bca2 --- /dev/null +++ b/example/supabase.ts @@ -0,0 +1,5 @@ +import { createClient } from '@supabase/supabase-js'; + +import type { Database } from './database.types'; + +export const supabase = createClient('supabsase-url', 'supabase-key'); diff --git a/package-lock.json b/package-lock.json index c23608c..599f2cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "supabase-react-query-codegen": "dist/cli.js" }, "devDependencies": { + "@supabase/supabase-js": "^2.21.0", "@types/jest": "^29.5.0", "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^5.59.0", @@ -1378,16 +1379,16 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.1.1.tgz", "integrity": "sha512-bIR1Puae6W+1/MzPfYBWOG/SCWGo4B5CB7c0ZZksvliNEAzhxNBJ0UFKYINcGdGtxG8ZC+1xr3utWpNZNwnoRw==", - "peer": true, + "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/gotrue-js": { - "version": "2.22.3", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.22.3.tgz", - "integrity": "sha512-GBu+YT7jaWgmmVRNPZnT2YmO7TjscjoBVMzMDnr2yPL6C2ImzRmOKIbHnGliDeI5EaEsPPljWoYR4xB+WJb98w==", - "peer": true, + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.23.0.tgz", + "integrity": "sha512-N6o+MMGsPDbdiz0R0Oy9GlgefYFjJJvoBduR16s8c1H3yG2jp6jq+pMEP18P1bg7uk2DljEjyBnVN7Wj7SJ2Zw==", + "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } @@ -1396,7 +1397,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.5.0.tgz", "integrity": "sha512-YaU1HBE43Ba+FGmnXuvK+xYeHylkDKd04PYeKDUCoE2bUHoxSDqnjHbOwmLjnusGZi3X1MrFeUH1Wwb4bHYyIg==", - "peer": true, + "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } @@ -1405,7 +1406,7 @@ "version": "2.7.2", "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.7.2.tgz", "integrity": "sha512-Fi6xAl5PUkqnjl3wo4rdcQIbMG3+yTRX1aUZe/yfvTG84RMvmCXJ1yN6MmafVLeZpU1xkaz5Vx4L0tnHcLiy6w==", - "peer": true, + "dev": true, "dependencies": { "@types/phoenix": "^1.5.4", "@types/websocket": "^1.0.3", @@ -1416,19 +1417,19 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.1.tgz", "integrity": "sha512-nkR0fQA9ScAtIKA3vNoPEqbZv1k5B5HVRYEvRWdlP6mUpFphM9TwPL2jZ/ztNGMTG5xT6SrHr+H7Ykz8qzbhjw==", - "peer": true, + "dev": true, "dependencies": { "cross-fetch": "^3.1.5" } }, "node_modules/@supabase/supabase-js": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.20.0.tgz", - "integrity": "sha512-6sNX4WEaVBbmOS1K2U3PcpAhp0FcjVo1FRSEoj4N+NZ6DIokX+yut8IBhF176pg5kFCdFIL2wyNhf3SltREmWw==", - "peer": true, + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.21.0.tgz", + "integrity": "sha512-FW3ZzBoc4orSgfX0dXrmJoXAcI/hiekmqXTkN64vjtUF2Urp3UjyAf71UTtV9Jl6ejHoe3K++e0+Rg9zKUJh5w==", + "dev": true, "dependencies": { "@supabase/functions-js": "^2.1.0", - "@supabase/gotrue-js": "^2.22.0", + "@supabase/gotrue-js": "^2.23.0", "@supabase/postgrest-js": "^1.1.1", "@supabase/realtime-js": "^2.7.2", "@supabase/storage-js": "^2.5.1", @@ -1564,13 +1565,14 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", + "dev": true }, "node_modules/@types/phoenix": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.6.tgz", "integrity": "sha512-e7jZ6I9uyRGsg7MNwQcarmBvRlbGb9DibbocE9crVnxqsy6C23RMxLWbJ2CQ3vgCW7taoL1L+F02EcjA6ld7XA==", - "peer": true + "dev": true }, "node_modules/@types/prettier": { "version": "2.7.2", @@ -1594,7 +1596,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz", "integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==", - "peer": true, + "dev": true, "dependencies": { "@types/node": "*" } @@ -2369,8 +2371,8 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", + "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -2639,7 +2641,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "peer": true, + "dev": true, "dependencies": { "node-fetch": "2.6.7" } @@ -2662,7 +2664,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "peer": true, + "dev": true, "dependencies": { "es5-ext": "^0.10.50", "type": "^1.0.1" @@ -2672,7 +2674,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, + "dev": true, "dependencies": { "ms": "2.0.0" } @@ -2798,8 +2800,8 @@ "version": "0.10.62", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", @@ -2813,7 +2815,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "peer": true, + "dev": true, "dependencies": { "d": "1", "es5-ext": "^0.10.35", @@ -2824,7 +2826,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "peer": true, + "dev": true, "dependencies": { "d": "^1.0.1", "ext": "^1.1.2" @@ -3180,7 +3182,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "peer": true, + "dev": true, "dependencies": { "type": "^2.7.2" } @@ -3189,7 +3191,7 @@ "version": "2.7.2", "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "peer": true + "dev": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -3688,7 +3690,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "peer": true + "dev": true }, "node_modules/isexe": { "version": "2.0.0", @@ -4943,7 +4945,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "dev": true }, "node_modules/nano-time": { "version": "1.0.0", @@ -4970,13 +4972,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "peer": true + "dev": true }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "peer": true, + "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -4996,7 +4998,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", - "peer": true, + "dev": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -5965,7 +5967,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "peer": true + "dev": true }, "node_modules/ts-jest": { "version": "29.1.0", @@ -6121,7 +6123,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "peer": true + "dev": true }, "node_modules/type-check": { "version": "0.4.0", @@ -6160,7 +6162,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "peer": true, + "dev": true, "dependencies": { "is-typedarray": "^1.0.0" } @@ -6231,8 +6233,8 @@ "version": "5.0.10", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "dev": true, "hasInstallScript": true, - "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -6279,13 +6281,13 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "peer": true + "dev": true }, "node_modules/websocket": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", - "peer": true, + "dev": true, "dependencies": { "bufferutil": "^4.0.1", "debug": "^2.2.0", @@ -6302,7 +6304,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "peer": true, + "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -6380,7 +6382,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", - "peer": true, + "dev": true, "engines": { "node": ">=0.10.32" } diff --git a/package.json b/package.json index ebf2d5d..5c0559a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "lint": "eslint --cache \"src/**/*.{js,ts}\"", "lint:fix": "eslint --cache --fix \"src/**/*.{js,ts}\"", "test": "jest", + "generate": "npm run build && node scripts/generate.js", "test:watch": "jest --watch" }, "bin": { @@ -36,6 +37,7 @@ }, "homepage": "https://github.com/barrymichaeldoyle/supabase-react-query-codegen#readme", "devDependencies": { + "@supabase/supabase-js": "^2.21.0", "@types/jest": "^29.5.0", "@types/yargs": "^17.0.24", "@typescript-eslint/eslint-plugin": "^5.59.0", diff --git a/scripts/generate.js b/scripts/generate.js new file mode 100644 index 0000000..8f73a94 --- /dev/null +++ b/scripts/generate.js @@ -0,0 +1,6 @@ +const config = require('../supabase-react-query-codegen.config.js'); + +const { default: generate } = require('../dist/generate.js'); + +generate(config) + diff --git a/src/cli.ts b/src/cli.ts index 3be65ec..8fd1029 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -27,11 +27,14 @@ yargs(process.argv.slice(2)) }, handler: (argv) => { console.log('Inside generate command handler'); - generateHooks( - argv.typesPath as string, - argv.outputPath as string, - argv.supabaseClientPath as string - ); + generateHooks({ + outputPath: argv['outputPath'], + prettierConfigPath: argv['prettierConfigPath'], + relativeSupabasePath: argv['relativeSupabasePath'], + relativeTypesPath: argv['relativeTypesPath'], + supabaseExportName: argv['supabaseExportName'], + typesPath: argv['typesPath'], + }); console.log('generateHooks function has been called'); }, }) diff --git a/src/generate.ts b/src/generate.ts index 6e900ed..54d9cf5 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -1,100 +1,74 @@ // src/generate.ts import fs from 'fs'; -import { toHookName } from './utils/toHookName'; +import prettier, { resolveConfig } from 'prettier'; import { getTablesProperties } from './utils/getTablesProperties'; +import { generateTypes } from './utils/generateTypes/generateTypes'; +import { generateHooks } from './utils/generateHooks/generateHooks'; -// Utility function to generate hook names +export interface Config { + outputPath: string; + prettierConfigPath: string; + relativeSupabasePath: string; + relativeTypesPath: string; + supabaseExportName?: string | false; + typesPath: string; +} -export default function generateHooks( - typesPath: string, - outputPath: string, - supabaseClientPath: string -) { +export default async function generate({ + outputPath, + prettierConfigPath = '.prettierrc', + relativeTypesPath, + relativeSupabasePath, + supabaseExportName = 'supabase', + typesPath, +}: Config) { console.log('Generating hooks with the following arguments:', { - typesPath, outputPath, - supabaseClientPath, + prettierConfigPath, + relativeTypesPath, + relativeSupabasePath, + supabaseExportName, + typesPath, }); const tablesProperties = getTablesProperties(typesPath); // Iterate through table keys and generate hooks const hooks: string[] = []; + const types: string[] = []; for (const table of tablesProperties) { const tableName = table.getName(); - // Generate hooks for fetching, adding, updating, and deleting - hooks.push( - `export function ${toHookName(tableName, 'Get')}(id: string) { - return useQuery( - ['${tableName}', id], - async () => { - const { data, error } = await supabase - .from('${tableName}') - .select('*') - .eq('id', id) - .single(); - - if (error) { - throw error; - } - - if (!data) { - throw new Error('No data found'); - } - - return data; - }, - { - enabled: !!id, - } - ); - }`, - `export function ${toHookName(tableName, 'GetAll')}() { - return useQuery(['${tableName}'], async () => { - const { data, error } = await supabase.from('${tableName}').select(); - if (error) throw error; - return data as Database['public']['Tables']['${tableName}']['Row'][]; - }); - }`, - `export function ${toHookName(tableName, 'Add')}() { - const queryClient = useQueryClient(); - return useMutation((item: Database['public']['Tables']['${tableName}']['Insert']) => supabase.from('${tableName}').insert(item).single(), { - onSuccess: () => { - queryClient.invalidateQueries('${tableName}'); - }, - }); - }`, - `export function ${toHookName(tableName, 'Update')}() { - const queryClient = useQueryClient(); - return useMutation((item: { id: string; changes: Database['public']['Tables']['${tableName}']['Update'] }) => supabase.from('${tableName}').update(item.changes).eq('id', item.id).single(), { - onSuccess: () => { - queryClient.invalidateQueries('${tableName}'); - }, - }); - }`, - `export function ${toHookName(tableName, 'Delete')}() { - const queryClient = useQueryClient(); - return useMutation((id: string) => supabase.from('${tableName}').delete().eq('id', id).single(), { - onSuccess: () => { - queryClient.invalidateQueries('${tableName}'); - }, - }); - }` - ); + hooks.push(...generateHooks({ supabaseExportName, tableName })); + types.push(...generateTypes({ table, tableName })); } // Create the output file content with imports and hooks - const hooksFileContent = ` + const generatedFileContent = ` import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { Database } from '${typesPath}'; -import { supabase } from '${supabaseClientPath}'; +import { Database } from '${relativeTypesPath}'; +import ${ + supabaseExportName ? `{ ${supabaseExportName} }` : 'supabase' + } from '${relativeSupabasePath}'; + +${types.join('\n')} ${hooks.join('\n\n')} `; + const prettierConfig = prettierConfigPath + ? await resolveConfig(prettierConfigPath) + : undefined; + + // Format the file content using Prettier + const formattedFileContent = prettier.format(generatedFileContent, { + parser: 'typescript', + // Additional Prettier options can be added here + ...(prettierConfig || {}), + }); + // Write the output file - fs.writeFileSync(outputPath, hooksFileContent); + fs.writeFileSync(outputPath, formattedFileContent); } diff --git a/src/utils/generateHooks/generateHooks.ts b/src/utils/generateHooks/generateHooks.ts new file mode 100644 index 0000000..83e743d --- /dev/null +++ b/src/utils/generateHooks/generateHooks.ts @@ -0,0 +1,78 @@ +import { toHookName } from './toHookName'; + +interface GenerateHooksArgs { + tableName: string; + supabaseExportName?: string | false; +} + +export function generateHooks({ + tableName, + supabaseExportName, +}: GenerateHooksArgs): string[] { + const hooks: string[] = []; + const supabase = supabaseExportName || 'supabase'; + + hooks.push( + `export function ${toHookName({ + operation: 'Get', + tableName, + })}(id: string) { + return useQuery( + ['${tableName}', id], + async () => { + const { data, error } = await ${supabase} + .from('${tableName}') + .select('*') + .eq('id', id) + .single(); + + if (error) { + throw error; + } + + if (!data) { + throw new Error('No data found'); + } + + return data; + }, + { + enabled: !!id, + } + ); +}`, + `export function ${toHookName({ operation: 'GetAll', tableName })}() { + return useQuery(['${tableName}'], async () => { + const { data, error } = await ${supabase}.from('${tableName}').select(); + if (error) throw error; + return data as Database['public']['Tables']['${tableName}']['Row'][]; + }); +}`, + `export function ${toHookName({ operation: 'Add', tableName })}() { + const queryClient = useQueryClient(); + return useMutation((item: Database['public']['Tables']['${tableName}']['Insert']) => ${supabase}.from('${tableName}').insert(item).single(), { + onSuccess: () => { + queryClient.invalidateQueries('${tableName}'); + }, + }); +}`, + `export function ${toHookName({ operation: 'Update', tableName })}() { + const queryClient = useQueryClient(); + return useMutation((item: { id: string; changes: Database['public']['Tables']['${tableName}']['Update'] }) => ${supabase}.from('${tableName}').update(item.changes).eq('id', item.id).single(), { + onSuccess: () => { + queryClient.invalidateQueries('${tableName}'); + }, + }); +}`, + `export function ${toHookName({ operation: 'Delete', tableName })}() { + const queryClient = useQueryClient(); + return useMutation((id: string) => ${supabase}.from('${tableName}').delete().eq('id', id).single(), { + onSuccess: () => { + queryClient.invalidateQueries('${tableName}'); + }, + }); +}` + ); + + return hooks; +} diff --git a/src/utils/spec/toHookName.spec.ts b/src/utils/generateHooks/toHookName.spec.ts similarity index 62% rename from src/utils/spec/toHookName.spec.ts rename to src/utils/generateHooks/toHookName.spec.ts index 0d5f068..e9b516f 100644 --- a/src/utils/spec/toHookName.spec.ts +++ b/src/utils/generateHooks/toHookName.spec.ts @@ -1,43 +1,55 @@ -import { toHookName } from '../toHookName'; +import { toHookName } from './toHookName'; describe('toHookName', () => { it('should return the hook name for a table with a single word name.', () => { - const hookName = toHookName('users', 'Get'); + const hookName = toHookName({ tableName: 'users', operation: 'Get' }); expect(hookName).toBe('useGetUser'); }); it('should return the hook name for a table with a single word name and the GetAll operation', () => { - const hookName = toHookName('users', 'GetAll'); + const hookName = toHookName({ tableName: 'users', operation: 'GetAll' }); expect(hookName).toBe('useGetAllUsers'); }); it('should return the hook name for a table with a snake_case name and a non-GetAll operation', () => { - const hookName = toHookName('todo_items', 'Update'); + const hookName = toHookName({ + tableName: 'todo_items', + operation: 'Update', + }); expect(hookName).toBe('useUpdateTodoItem'); }); it('should return the hook name for a table with a snake_case name and the GetAll operation', () => { - const hookName = toHookName('todo_items', 'GetAll'); + const hookName = toHookName({ + tableName: 'todo_items', + operation: 'GetAll', + }); expect(hookName).toBe('useGetAllTodoItems'); }); it('should return the hook name for a table with a multi-word snake_case name and a non-GetAll operation', () => { - const hookName = toHookName('order_item_details', 'Delete'); + const hookName = toHookName({ + tableName: 'order_item_details', + operation: 'Delete', + }); expect(hookName).toBe('useDeleteOrderItemDetail'); }); it('should return the hook name for a table with a multi-word snake_case name and the GetAll operation', () => { - const hookName = toHookName('order_item_details', 'GetAll'); + const hookName = toHookName({ + tableName: 'order_item_details', + operation: 'GetAll', + }); expect(hookName).toBe('useGetAllOrderItemDetails'); }); it('should return the hook name for a table named people and a non-GetAll operation', () => { - const hookName = toHookName('people', 'Delete'); + const hookName = toHookName({ tableName: 'people', operation: 'Delete' }); expect(hookName).toBe('useDeletePerson'); }); it('should return the hook name for a table named people and the GetAll operation', () => { - const hookName = toHookName('people', 'GetAll'); + const hookName = toHookName({ tableName: 'people', operation: 'GetAll' }); expect(hookName).toBe('useGetAllPeople'); }); }); diff --git a/src/utils/toHookName.ts b/src/utils/generateHooks/toHookName.ts similarity index 70% rename from src/utils/toHookName.ts rename to src/utils/generateHooks/toHookName.ts index 84c184b..ef04ff5 100644 --- a/src/utils/toHookName.ts +++ b/src/utils/generateHooks/toHookName.ts @@ -1,4 +1,9 @@ -export function toHookName(tableName: string, operation: string): string { +interface ToHookNameArgs { + operation: string; + tableName: string; +} + +export function toHookName({ operation, tableName }: ToHookNameArgs): string { const camelCaseTableName = tableName.replace(/(_\w)/g, (match) => match[1].toUpperCase() ); diff --git a/src/utils/generateTypes/generateTypes.ts b/src/utils/generateTypes/generateTypes.ts new file mode 100644 index 0000000..692831d --- /dev/null +++ b/src/utils/generateTypes/generateTypes.ts @@ -0,0 +1,68 @@ +import type { Symbol } from 'ts-morph'; + +import { toTypeName } from './toTypeName'; + +interface GenerateTypesArg { + table: Symbol; + tableName: string; +} + +export function generateTypes({ + tableName, + table, +}: GenerateTypesArg): string[] { + // Get the table type + const tableType = table.getTypeAtLocation(table.getValueDeclarationOrThrow()); + + // Find the 'Row' property within the table type + const rowProperty = tableType.getProperty('Row'); + if (!rowProperty) { + throw new Error(`Unable to find Row property type for ${tableName}.`); + } + + // Get the type of the 'Row' property + const rowType = rowProperty.getTypeAtLocation( + rowProperty.getValueDeclarationOrThrow() + ); + + const insertProperty = tableType.getProperty('Insert'); + if (!insertProperty) { + throw new Error(`Unable to find insert property type for ${tableName}.`); + } + + const insertType = insertProperty.getTypeAtLocation( + insertProperty.getValueDeclarationOrThrow() + ); + + const updateProperty = tableType.getProperty('Update'); + if (!updateProperty) { + throw new Error(`Unable to find update property type for ${tableName}.`); + } + + const updateType = updateProperty.getTypeAtLocation( + updateProperty.getValueDeclarationOrThrow() + ); + + const rowTypeString = rowType.getText(); + const insertTypeString = insertType.getText(); + const updateTypeString = updateType.getText(); + + const types: string[] = []; + + types.push( + `export type ${toTypeName(tableName, 'Get')}Request = string;`, + `export type ${toTypeName(tableName, 'Get')}Response = ${rowTypeString};`, + `export type ${toTypeName( + tableName, + 'GetAll' + )}Response = ${rowTypeString}[];`, + `export type ${toTypeName(tableName, 'Add')}Request = ${insertTypeString};`, + `export type ${toTypeName( + tableName, + 'Update' + )}Request = { id: string; changes: ${updateTypeString} };`, + `export type ${toTypeName(tableName, 'Delete')}Request = string;` + ); + + return types; +} diff --git a/src/utils/generateTypes/toTypeName.ts b/src/utils/generateTypes/toTypeName.ts new file mode 100644 index 0000000..c6b2a99 --- /dev/null +++ b/src/utils/generateTypes/toTypeName.ts @@ -0,0 +1,14 @@ +export function toTypeName(tableName: string, operation: string): string { + const camelCaseTableName = tableName.replace(/(_\w)/g, (match) => + match[1].toUpperCase() + ); + + const formattedTableName = + camelCaseTableName[0].toUpperCase() + camelCaseTableName.slice(1); + + if (operation !== 'GetAll') { + return `${operation}${formattedTableName.slice(0, -1)}`; + } else { + return `${operation}${formattedTableName}`; + } +} diff --git a/supabase-react-query-codegen.config.js b/supabase-react-query-codegen.config.js new file mode 100644 index 0000000..04f8948 --- /dev/null +++ b/supabase-react-query-codegen.config.js @@ -0,0 +1,8 @@ +module.exports = { + outputPath: './example/generated.ts', + prettierPath: '.prettierrc', + relativeSupabasePath: './supabase', + relativeTypesPath: './database.types', + supabaseExportName: 'supabase', + typesPath: './example/database.types.ts', +};