From 78aa62ee4c448b00bef6795aea41981ede9534ce Mon Sep 17 00:00:00 2001 From: nnivxix Date: Sun, 15 Dec 2024 15:25:09 +0700 Subject: [PATCH] wip: basic crud supabase and indexedDB for collection --- .env.example | 2 + .gitignore | 3 + README.md | 30 +++++++++ package.json | 1 + pnpm-lock.yaml | 97 ++++++++++++++++++++++++++++ src/App.vue | 10 ++- src/composables/useCollection.js | 9 ++- src/composables/useFormCollection.js | 6 +- src/pages/collection/create.vue | 2 +- src/pages/collection/edit.vue | 25 ++++--- src/pages/collection/id.vue | 53 +++++++++------ src/pages/index.vue | 3 +- src/repositories/adapter.js | 28 +++++--- src/repositories/indexedDB.js | 6 +- src/repositories/supabase.js | 72 +++++++++++++++++++++ src/types.d.ts | 2 +- 16 files changed, 297 insertions(+), 52 deletions(-) create mode 100644 .env.example create mode 100644 src/repositories/supabase.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5a59748 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VITE_SUPABASE_URL= +VITE_SUPABASE_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index afdf4b6..1d9c3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ dev-dist *.njsproj *.sln *.sw? + + +.env \ No newline at end of file diff --git a/README.md b/README.md index e6476ce..258d86e 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,33 @@ Create todo right now, let's do great today, don't busy be productive. Start at 09:31:41 Duration 418ms ``` + + +## Supabase Setup + +```sql +create table + public.collections ( + id bigint generated by default as identity not null, + uid character varying not null, + created_at timestamp with time zone not null default now(), + name character varying not null, + description text null, + constraint collections_pkey primary key (id), + constraint collections_uid_key unique (uid) + ) tablespace pg_default; +``` + +```sql +create table + public.todos ( + id bigint generated by default as identity not null, + name character varying null, + priority character varying null, + is_done boolean not null default false, + created_at timestamp with time zone not null default now(), + collection_id bigint not null, + constraint todos_pkey primary key (id), + constraint todos_collection_id_fkey foreign key (collection_id) references collections (id) on update cascade on delete cascade + ) tablespace pg_default; +``` \ No newline at end of file diff --git a/package.json b/package.json index 8783ff1..092cdd1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@highlightjs/vue-plugin": "^2.1.0", + "@supabase/supabase-js": "^2.46.1", "highlight.js": "^11.10.0", "idb": "^8.0.0", "nanoid": "^5.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae22963..fefa200 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@highlightjs/vue-plugin': specifier: ^2.1.0 version: 2.1.0(highlight.js@11.10.0)(vue@3.4.20) + '@supabase/supabase-js': + specifier: ^2.46.1 + version: 2.46.1 highlight.js: specifier: ^11.10.0 version: 11.10.0 @@ -918,6 +921,28 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@supabase/auth-js@2.65.1': + resolution: {integrity: sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==} + + '@supabase/functions-js@2.4.3': + resolution: {integrity: sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.16.3': + resolution: {integrity: sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==} + + '@supabase/realtime-js@2.10.7': + resolution: {integrity: sha512-OLI0hiSAqQSqRpGMTUwoIWo51eUivSYlaNBgxsXZE7PSoWh12wPRdVt0psUMaUzEonSB85K21wGc7W5jHnT6uA==} + + '@supabase/storage-js@2.7.1': + resolution: {integrity: sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==} + + '@supabase/supabase-js@2.46.1': + resolution: {integrity: sha512-HiBpd8stf7M6+tlr+/82L8b2QmCjAD8ex9YdSAKU+whB/SHXXJdus1dGlqiH9Umy9ePUuxaYmVkGd9BcvBnNvg==} + '@surma/rollup-plugin-off-main-thread@2.2.3': resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} @@ -930,12 +955,18 @@ packages: '@types/node@18.7.14': resolution: {integrity: sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==} + '@types/phoenix@1.6.5': + resolution: {integrity: sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==} + '@types/resolve@1.17.1': resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} '@types/trusted-types@2.0.2': resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + '@vitejs/plugin-vue@5.0.4': resolution: {integrity: sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2146,6 +2177,9 @@ packages: resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -2313,6 +2347,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -2332,6 +2369,9 @@ packages: resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -3365,6 +3405,48 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@supabase/auth-js@2.65.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.3': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.16.3': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.10.7': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.5 + '@types/ws': 8.5.13 + ws: 8.16.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/storage-js@2.7.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.46.1': + dependencies: + '@supabase/auth-js': 2.65.1 + '@supabase/functions-js': 2.4.3 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.16.3 + '@supabase/realtime-js': 2.10.7 + '@supabase/storage-js': 2.7.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@surma/rollup-plugin-off-main-thread@2.2.3': dependencies: ejs: 3.1.8 @@ -3378,12 +3460,18 @@ snapshots: '@types/node@18.7.14': {} + '@types/phoenix@1.6.5': {} + '@types/resolve@1.17.1': dependencies: '@types/node': 18.7.14 '@types/trusted-types@2.0.2': {} + '@types/ws@8.5.13': + dependencies: + '@types/node': 18.7.14 + '@vitejs/plugin-vue@5.0.4(vite@5.1.4(@types/node@18.7.14)(terser@5.15.0))(vue@3.4.20)': dependencies: vite: 5.1.4(@types/node@18.7.14)(terser@5.15.0) @@ -4660,6 +4748,8 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -4817,6 +4907,8 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} webidl-conversions@7.0.0: {} @@ -4832,6 +4924,11 @@ snapshots: tr46: 5.0.0 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 diff --git a/src/App.vue b/src/App.vue index d4f4035..554d09d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,15 +1,21 @@ diff --git a/src/composables/useCollection.js b/src/composables/useCollection.js index 104c629..5565246 100644 --- a/src/composables/useCollection.js +++ b/src/composables/useCollection.js @@ -31,7 +31,7 @@ const useCollection = () => { const addCollection = (collection) => { collections.value.push(collection); - dbCollection.add(collection); + store.create(collection); }; const getCollections = async () => { collections.value = await store.all(); @@ -41,8 +41,11 @@ const useCollection = () => { * @param {string} id * @returns {Collection} */ - const getDetailCollection = (id) => { - return collections.value.find((collection) => collection.id == id); + const getDetailCollection = async (id) => { + const collection = (await model()).find(id); + + return collection; + // return collections.value.find((collection) => collection.id == id); }; /** @param {string} id */ diff --git a/src/composables/useFormCollection.js b/src/composables/useFormCollection.js index 8218756..64eba7a 100644 --- a/src/composables/useFormCollection.js +++ b/src/composables/useFormCollection.js @@ -33,7 +33,7 @@ const useFormCollection = () => { async function editCurrentCollection() { form.value.id = collection.value.id; const updatedCollection = updateCollection(form.value); - console.log(updatedCollection); + return updatedCollection; } function addNewCollection() { @@ -44,9 +44,11 @@ const useFormCollection = () => { id: nanoid(), name: form.value.name, description: form.value.description, - todos: [], + // todos: [], }; + // Validation if collection more than 10 just throw error + addCollection(collection); resetForm(); return collection; diff --git a/src/pages/collection/create.vue b/src/pages/collection/create.vue index 58b0d49..da3283e 100644 --- a/src/pages/collection/create.vue +++ b/src/pages/collection/create.vue @@ -15,7 +15,7 @@ const schema = yup.object({ /** @param {import('@/types').Collection} values */ const onSubmit = (values) => { form.value = values; - form.value.todos = []; + // form.value.todos = []; const collection = addNewCollection(); router.push(`/collection/${collection.id}`); diff --git a/src/pages/collection/edit.vue b/src/pages/collection/edit.vue index 5132ab4..e090116 100644 --- a/src/pages/collection/edit.vue +++ b/src/pages/collection/edit.vue @@ -4,16 +4,18 @@ import { onMounted, toRaw } from "vue"; import { Form } from "vee-validate"; import * as yup from "yup"; import useFormCollection from "@/composables/useFormCollection"; -import dbCollection from "@/repositories/db-collection"; +import model from "@/repositories/adapter"; const router = useRouter(); const route = useRoute(); -const { editCurrentCollection, form, collection } = useFormCollection(); +const { form, collection } = useFormCollection(); + const schema = yup.object({ name: yup.string().required(), description: yup.string(), }); const isEdit = route.fullPath.includes("edit"); +const store = await model(); /** @param {import('@/types').Collection} values */ const onSubmit = async (values) => { @@ -21,20 +23,23 @@ const onSubmit = async (values) => { ...values, todos: toRaw(collection.value.todos), }; - const updatedCollection = await editCurrentCollection(); - router.push(`/collection/${updatedCollection.id}`); + await store.update(route.params.id, { + name: values.name, + description: values.description, + }); + + router.push(`/collection/${route.params.id}`); }; onMounted(async () => { + collection.value = await store.find(route.params.id); if (isEdit) { - const dataCollection = await dbCollection.show(route.params.id); - collection.value = dataCollection; - if (!!dataCollection) { + if (!!collection.value) { form.value = { - name: dataCollection.name, - description: dataCollection.description, - todos: dataCollection.todos, + name: collection.value.name, + description: collection.value.description, + todos: collection.value.todos, }; return; } diff --git a/src/pages/collection/id.vue b/src/pages/collection/id.vue index bb44ad2..9c2e817 100644 --- a/src/pages/collection/id.vue +++ b/src/pages/collection/id.vue @@ -2,6 +2,8 @@ import { useRoute, useRouter } from "vue-router"; import { ref, toRaw, onMounted } from "vue"; import { useForm } from "vee-validate"; +import model from "@/repositories/adapter"; + import * as yup from "yup"; import useCollection from "@/composables/useCollection"; @@ -10,9 +12,13 @@ import useFormTodo from "@/composables/useFormTodo"; import dbCollections from "@/repositories/db-collection"; import exportCollection from "@/utils/export-collection"; +const store = await model(); const route = useRoute(); const router = useRouter(); -const { deleteColllection, collection } = useCollection(); +const { collection, collections } = useCollection(); + +collection.value = await store.find(route.params.id); + const { addTodo, markTodo, editTodo, deleteTodo, doneTodos, todos } = useTodo(); const { formTodo, isEditing, resetForm: resetFormTodo } = useFormTodo(); const vFormTodo = useForm({ @@ -102,10 +108,16 @@ const handleMarkTodo = (index) => { }; /** @param {string} id */ -const handleDeleteCollection = (id) => { +const handleDeleteCollection = async (id) => { const question = confirm("Are you sure delete this collection?"); if (question) { - deleteColllection(id); + store.delete(id); + + const index = collections.value.findIndex((coll) => coll.id === id); + if (index > 0) { + collections.value.splice(index, 1); + } + router.push("/"); return; } @@ -113,7 +125,6 @@ const handleDeleteCollection = (id) => { }; onMounted(async () => { - collection.value = await dbCollections.show(route.params.id); resetFormTodo(); vFormTodo.setValues({ ...formTodo.value, @@ -135,14 +146,14 @@ onMounted(async () => { {{ collection.description }}

no description

- -

- You have {{ todos?.length }} / {{ doneTodos?.length }} - {{ todos.length > 1 ? "todos" : "todo" }} -

+ +
@@ -172,15 +183,15 @@ onMounted(async () => {
- +

- {{ collection.name }} ({{ collection.todos.length }}) + {{ collection.name }} +

{ let store = null; + if (connection === "indexedDB") { store = indexedDB; } + if (connection === "supabase") { + store = supabase; + } // Check if store is set if (!store) { @@ -15,17 +25,17 @@ const model = async (connection = "indexedDB") => { all: async () => { return await store.index(); }, - find: (id) => { - return store.find(id); + find: (key) => { + return store.find(key); }, - create: (collection) => { - store.create(collection); + create: (values) => { + store.create(values); }, - update: async (id, collection) => { - await store.update(collection); + update: async (key, values) => { + await store.update(key, values); }, - delete: (id) => { - store.delete(id); + delete: (key) => { + store.delete(key); }, }; }; diff --git a/src/repositories/indexedDB.js b/src/repositories/indexedDB.js index bc20f81..0966f3d 100644 --- a/src/repositories/indexedDB.js +++ b/src/repositories/indexedDB.js @@ -26,8 +26,10 @@ const indexedDB = { * @param {Collection} collection * @returns {Promise} */ - async update(collection) { - return (await dbPromise).put("collections", collection); + async update(key, collection) { + const values = { id: key, ...collection }; + const db = await dbPromise; + db.put("collections", values); }, /** diff --git a/src/repositories/supabase.js b/src/repositories/supabase.js new file mode 100644 index 0000000..42e55e8 --- /dev/null +++ b/src/repositories/supabase.js @@ -0,0 +1,72 @@ +import { createClient } from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseKey = import.meta.env.VITE_SUPABASE_KEY; + +const client = createClient(supabaseUrl, supabaseKey); + +/** + * @typedef {import('@/types').Collection} Collection + */ + +const supabase = { + async index() { + // const { data } = await client.from("collections").select(`*, todos(count)`); + const { data } = await client + .from("collections") + .select(`name, description, id:uid, created_at, todos(*)`); + + console.log(data); + return data; + }, + + /** @param {Collection} collection */ + async create(collection) { + const { data } = await client + .from("collections") + .insert({ + uid: collection.id, + name: collection.name, + description: collection.description, + }) + .select(`name, description, id:uid, created_at, todos(*)`); + + return data; + }, + + async find(id) { + // return (await dbPromise).get("collections", id); + const { data } = await client + .from("collections") + .select(`name, description, id:uid, created_at, todos(*)`) + .eq("uid", id) + .limit(1); + + return data.at(0); + }, + + async update(id, values) { + const { data, error } = await client + .from("collections") + .update({ ...values }) + .eq("uid", id) + .select(); + + if (error) { + console.error(error); + } + return data; + }, + async delete(key) { + const { data, error } = await client + .from("collections") + .delete() + .eq("uid", key); + + if (error) { + console.error(error); + } + return data; + }, +}; +export default supabase; diff --git a/src/types.d.ts b/src/types.d.ts index d6ed786..9c2ab5e 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,4 +1,4 @@ -export interface Collection { +nexport interface Collection { id: string; name: string; description: string;