Connect the frontend with the api and powersync service

This commit is contained in:
KarimTamani
2025-05-05 13:58:21 +01:00
parent d9905351bd
commit 1f42c07d92
8 changed files with 224 additions and 9 deletions
+3
View File
@@ -0,0 +1,3 @@
VITE_BACKEND_URL = "http://localhost:6060"
VITE_POWERSYNC_URL = "http://localhost:8080"
VITE_CHECKPOINT_MODE = "managed"
+5 -1
View File
@@ -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"
}
}
+9 -6
View File
@@ -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 (
<ReactFlowProvider>
<TooltipProvider delayDuration={0}>
{appRoutes}
</TooltipProvider>
</ReactFlowProvider>
<SyncProvider>
<ReactFlowProvider>
<TooltipProvider delayDuration={0}>
{appRoutes}
</TooltipProvider>
</ReactFlowProvider>
</SyncProvider>
);
}
+24
View File
@@ -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<typeof todos>;
+5
View File
@@ -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<DatabaseProps> = ({ 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]);
@@ -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<StackRenderConnector | null>(null);
export const useConnector = () => useContext(ConnectorContext);
interface SyncProviderProps {
children: React.ReactNode;
}
export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
const [powerSync] = useState(db);
const [connector] = useState(new StackRenderConnector());
useEffect(() => {
powerSync.init();
powerSync.connect(connector);
}, [powerSync, connector])
return (
<Suspense fallback={<CircularProgress/>}>
<PowerSyncContext.Provider value={powerSync}>
<ConnectorContext.Provider value={connector}>
{children}
</ConnectorContext.Provider>
</PowerSyncContext.Provider>
</Suspense>
)
}
+120
View File
@@ -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<void> {
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;
}
}
+12 -2
View File
@@ -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()]
}
})