From 1f42c07d92e064a71b81a3539075ccc072c49f27 Mon Sep 17 00:00:00 2001 From: KarimTamani Date: Mon, 5 May 2025 13:58:21 +0100 Subject: [PATCH] Connect the frontend with the api and powersync service --- .env | 3 + package.json | 6 +- src/App.tsx | 15 ++- src/lib/schemas/app-schema.ts | 24 ++++ src/pages/database/database-page.tsx | 5 + src/providers/sync-provider/sync-provider.tsx | 46 +++++++ src/utils/stackrender-connector.ts | 120 ++++++++++++++++++ vite.config.ts | 14 +- 8 files changed, 224 insertions(+), 9 deletions(-) create mode 100644 .env create mode 100644 src/lib/schemas/app-schema.ts create mode 100644 src/providers/sync-provider/sync-provider.tsx create mode 100644 src/utils/stackrender-connector.ts diff --git a/.env b/.env new file mode 100644 index 0000000..e17f9e4 --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +VITE_BACKEND_URL = "http://localhost:6060" +VITE_POWERSYNC_URL = "http://localhost:8080" +VITE_CHECKPOINT_MODE = "managed" \ No newline at end of file diff --git a/package.json b/package.json index 3f84255..9bf88b2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@heroui/react": "^2.7.6", + "@powersync/react": "^1.5.3", + "@powersync/web": "^1.20.0", "@radix-ui/react-tooltip": "^1.2.3", "@react-aria/visually-hidden": "3.8.21", "@react-types/shared": "3.28.0", @@ -26,7 +28,8 @@ "react-router-dom": "6.23.0", "tailwind-merge": "^3.2.0", "tailwind-variants": "0.3.0", - "tailwindcss": "3.4.16" + "tailwindcss": "3.4.16", + "uuid": "^11.1.0" }, "devDependencies": { "@types/node": "20.5.7", @@ -49,6 +52,7 @@ "prettier": "3.3.3", "typescript": "5.6.3", "vite": "^5.2.0", + "vite-plugin-top-level-await": "^1.5.0", "vite-tsconfig-paths": "^4.3.2" } } diff --git a/src/App.tsx b/src/App.tsx index f0ce546..a725ea4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,15 +3,18 @@ import "@/styles/globals.css" import { TooltipProvider } from "./components/tooltip/tooltip"; import { ReactFlowProvider } from "@xyflow/react"; import useAppRoutes from "./routes/app-route"; +import { SyncProvider } from "./providers/sync-provider"; function App() { - const appRoutes = useAppRoutes() ; + const appRoutes = useAppRoutes(); return ( - - - {appRoutes} - - + + + + {appRoutes} + + + ); } diff --git a/src/lib/schemas/app-schema.ts b/src/lib/schemas/app-schema.ts new file mode 100644 index 0000000..a54f6e5 --- /dev/null +++ b/src/lib/schemas/app-schema.ts @@ -0,0 +1,24 @@ +import { column, Schema, Table } from '@powersync/web'; + +export const DATATYPE_TABLE = 'dataTypes'; + + +const data_types = new Table( + { + name: column.text, + }, +); + + + +export const AppSchema = new Schema({ + data_types, + +}); + +export type Database = (typeof AppSchema)['types']; +export type DataTypesRecord = Database['data_types']; +// OR: +// export type Todo = RowType; + + diff --git a/src/pages/database/database-page.tsx b/src/pages/database/database-page.tsx index 1b69506..6af68cd 100644 --- a/src/pages/database/database-page.tsx +++ b/src/pages/database/database-page.tsx @@ -6,6 +6,7 @@ import Table from "./table/table"; import '@xyflow/react/dist/style.css'; import { Relationship } from "./table/relationship"; import DBController from "./db-controller/db-controller"; +import { useQuery } from "@powersync/react"; interface DatabaseProps { initialTables?: TableType @@ -172,6 +173,10 @@ const DatabasePage: React.FC = ({ initialTables }) => { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + const { data , isLoading , error , isFetching } = useQuery('SELECT * FROM data_types ;' ); + + + console.log (data) const nodeTypes = useMemo(() => ({ table: Table }), []); const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges]); diff --git a/src/providers/sync-provider/sync-provider.tsx b/src/providers/sync-provider/sync-provider.tsx new file mode 100644 index 0000000..23e68e2 --- /dev/null +++ b/src/providers/sync-provider/sync-provider.tsx @@ -0,0 +1,46 @@ + +import { PowerSyncDatabase } from '@powersync/web'; +import { PowerSyncContext } from "@powersync/react"; +import { createContext, Suspense, useContext, useEffect, useState } from 'react'; +import { AppSchema } from '@/lib/schemas/app-schema'; +import { StackRenderConnector } from '@/utils/stackrender-connector'; +import { CircularProgress } from '@heroui/react'; + +export const db = new PowerSyncDatabase({ + database: { + dbFilename: 'stackrender' + }, + schema: AppSchema, + + +}); + + +const ConnectorContext = createContext(null); +export const useConnector = () => useContext(ConnectorContext); + + +interface SyncProviderProps { + children: React.ReactNode; +} + +export const SyncProvider: React.FC = ({ children }) => { + + const [powerSync] = useState(db); + const [connector] = useState(new StackRenderConnector()); + useEffect(() => { + + powerSync.init(); + powerSync.connect(connector); + }, [powerSync, connector]) + return ( + }> + + + {children} + + + + ) +} + diff --git a/src/utils/stackrender-connector.ts b/src/utils/stackrender-connector.ts new file mode 100644 index 0000000..7e3a0a4 --- /dev/null +++ b/src/utils/stackrender-connector.ts @@ -0,0 +1,120 @@ +import { v4 as uuid } from 'uuid'; + +import { AbstractPowerSyncDatabase, PowerSyncBackendConnector } from '@powersync/web'; + +export type DemoConfig = { + backendUrl: string; + powersyncUrl: string; +}; + +enum CheckpointMode { + CUSTOM = 'custom', + MANAGED = 'managed' +} + +const USER_ID_STORAGE_KEY = 'ps_user_id'; + +export class StackRenderConnector implements PowerSyncBackendConnector { + readonly config: DemoConfig; + readonly userId: string; + + private _clientId: string | null; + + constructor() { + let userId = localStorage.getItem(USER_ID_STORAGE_KEY); + if (!userId) { + userId = uuid(); + localStorage.setItem(USER_ID_STORAGE_KEY, userId); + } + this.userId = userId; + this._clientId = null; + + this.config = { + backendUrl: import.meta.env.VITE_BACKEND_URL as string, + powersyncUrl: import.meta.env.VITE_POWERSYNC_URL as string + }; + } + + async fetchCredentials() { + const tokenEndpoint = 'api/auth/token'; + const res = await fetch(`${this.config.backendUrl}/${tokenEndpoint}?user_id=${this.userId}`); + + if (!res.ok) { + throw new Error(`Received ${res.status} from ${tokenEndpoint}: ${await res.text()}`); + } + + const body = await res.json(); + + return { + endpoint: this.config.powersyncUrl, + token: body.token + }; + } + + async uploadData(database: AbstractPowerSyncDatabase): Promise { + + const transaction = await database.getNextCrudTransaction(); + + if (!transaction) { + return; + } + + if (!this._clientId) { + this._clientId = await database.getClientId(); + } + + try { + let batch: any[] = []; + for (let operation of transaction.crud) { + let payload = { + op: operation.op, + table: operation.table, + id: operation.id, + data: operation.opData + }; + batch.push(payload); + } + + const response = await fetch(`${this.config.backendUrl}/api/data`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ batch }) + }); + + if (!response.ok) { + throw new Error(`Received ${response.status} from /api/data: ${await response.text()}`); + } + + await transaction.complete( + import.meta.env.VITE_CHECKPOINT_MODE == CheckpointMode.CUSTOM + ? await this.getCheckpoint(this._clientId) + : undefined + ); + } catch (ex: any) { + console.debug(ex); + throw ex; + } + } + + /** + * Gets a custom Write Checkpoint from the backend. This is only used + * when custom Write Checkpoints are enabled during build. + */ + async getCheckpoint(client_id: string) { + + const r = await fetch(`${this.config.backendUrl}/api/data/checkpoint`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + user_id: this.userId, + client_id: client_id + }) + }); + const j = await r.json(); + return j.checkpoint as string; + } +} diff --git a/vite.config.ts b/vite.config.ts index a38dead..cdb422b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,21 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tsconfigPaths from 'vite-tsconfig-paths' - +import topLevelAwait from 'vite-plugin-top-level-await'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), tsconfigPaths()], + plugins: [react(), tsconfigPaths() , topLevelAwait()], server : { port : 3000 + } , + optimizeDeps: { + // Don't optimize these packages as they contain web workers and WASM files. + // https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673 + exclude: ['@journeyapps/wa-sqlite', '@powersync/web'], + include: ['@powersync/web > js-logger'] + }, + worker: { + format: 'es', + plugins: () => [ topLevelAwait()] } })