First commit

This commit is contained in:
srosset81
2026-07-10 11:45:54 +02:00
commit 30ea3db684
37 changed files with 10866 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
.parcel-cache
*.log
+14
View File
@@ -0,0 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
# production
dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
+10
View File
@@ -0,0 +1,10 @@
## Simple app for NextGraph
A simple app that use Refine providers compatible with NextGraph
## Quick start
```
pnpm install
pnpm run dev
```
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using refine" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<title>refine authentication example</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm run dev` or `yarn dev`.
To create a production bundle, use `npm run build` or `yarn build`.
--></body>
</html>
+53
View File
@@ -0,0 +1,53 @@
{
"name": "antd-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "refine build",
"dev": "refine dev",
"refine": "refine",
"start": "refine start",
"build:orm": "rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@ng-org/orm": "0.1.2-alpha.19",
"@ng-org/web": "0.1.2-alpha.13",
"@ng-org/refine-providers": "workspace:*",
"@refinedev/antd": "^6.0.3",
"@refinedev/cli": "^2.16.52",
"@refinedev/core": "^5.0.12",
"@refinedev/react-router": "^2.0.4",
"@uiw/react-md-editor": "^4.0.8",
"antd": "^5.23.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.0.2"
},
"devDependencies": {
"@ng-org/shex-orm": "0.1.2-alpha.8",
"@types/node": "^20",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.8.3",
"vite": "^5.4.15"
},
"engines": {
"node": ">=20"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

+15
View File
@@ -0,0 +1,15 @@
{
"short_name": "refine authentication example",
"name": "refine authentication example",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+128
View File
@@ -0,0 +1,128 @@
import {
Refine,
Authenticated,
} from "@refinedev/core";
import {
useNotificationProvider,
ThemedLayout,
ErrorComponent,
RefineThemes,
} from "@refinedev/antd";
import {
DashboardOutlined,
} from "@ant-design/icons";
import routerProvider, {
NavigateToResource,
CatchAllNavigate,
UnsavedChangesNotifier,
DocumentTitleHandler,
} from "@refinedev/react-router";
import { BrowserRouter, Routes, Route, Outlet, Navigate } from "react-router";
import { App as AntdApp, ConfigProvider } from "antd";
import "@ant-design/v5-patch-for-react-19";
import "@refinedev/antd/dist/reset.css";
import { authProvider, dataProvider, liveProvider } from "@ng-org/refine-providers";
import dataModels from "./shapes/data-models";
import AuthPage from "./components/AuthPage";
import { PostList, PostCreate, PostEdit, PostShow } from "../src/pages/posts";
import { DashboardPage } from "../src/pages/dashboard";
import { getSession } from "./utils/initNg";
const App: React.FC = () => (
<BrowserRouter basename={import.meta.env.MODE === 'production' ? '/nextgraph-refine-app/' : '/'}>
<ConfigProvider theme={RefineThemes.Blue}>
<AntdApp>
<Refine
// authProvider={authProvider}
dataProvider={dataProvider({ dataModels, getSession })}
liveProvider={liveProvider({ dataModels })}
routerProvider={routerProvider}
resources={[
// {
// name: "dashboard",
// list: "/",
// meta: {
// label: "Dashboard",
// icon: <DashboardOutlined />,
// },
// },
{
name: "posts",
list: "/posts",
create: "/posts/create",
show: "/posts/show/:id",
edit: "/posts/edit/:id",
meta: {
canDelete: true,
},
},
]}
notificationProvider={useNotificationProvider}
options={{
syncWithLocation: true,
warnWhenUnsavedChanges: true,
liveMode: "auto",
disableTelemetry: true,
}}
>
<Routes>
<Route
element={
// <Authenticated
// key="authenticated-routes"
// fallback={<CatchAllNavigate to="/login" />}
// >
<ThemedLayout>
<Outlet />
</ThemedLayout>
// </Authenticated>
}
>
<Route index element={<Navigate to="/" />} />
{/* <Route index element={<DashboardPage />} /> */}
<Route path="/posts">
<Route index element={<PostList />} />
<Route path="create" element={<PostCreate />} />
<Route path="edit/:id" element={<PostEdit />} />
<Route path="show/:id" element={<PostShow />} />
</Route>
</Route>
{/* <Route
element={
<Authenticated key="auth-pages" fallback={<Outlet />}>
<NavigateToResource resource="posts" />
</Authenticated>
}
>
<Route path="/login" element={<AuthPage />} />
<Route path="/register" element={<AuthPage />} />
</Route> */}
<Route
element={
// <Authenticated key="catch-all">
<ThemedLayout>
<Outlet />
</ThemedLayout>
// </Authenticated>
}
>
<Route path="*" element={<ErrorComponent />} />
</Route>
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</Refine>
</AntdApp>
</ConfigProvider>
</BrowserRouter>
);
export default App;
@@ -0,0 +1,172 @@
import type { CSSProperties } from "react";
import {
type LoginPageProps,
type LoginFormTypes,
} from "@refinedev/core";
import {
Row,
Col,
Layout,
Card,
Form,
Input,
Button,
type CardProps,
type LayoutProps,
type FormProps,
theme,
} from "antd";
import { useLogin, useTranslate } from "@refinedev/core";
import { ThemedTitle } from "@refinedev/antd";
export const layoutStyles: CSSProperties = {};
export const containerStyles: CSSProperties = {
maxWidth: "400px",
margin: "auto",
padding: "32px",
boxShadow:
"0px 2px 4px rgba(0, 0, 0, 0.02), 0px 1px 6px -1px rgba(0, 0, 0, 0.02), 0px 1px 2px rgba(0, 0, 0, 0.03)",
};
export const headStyles: CSSProperties = {
borderBottom: 0,
padding: 0,
};
export const bodyStyles: CSSProperties = { padding: 0 };
export const titleStyles: CSSProperties = {
textAlign: "center",
marginBottom: 0,
fontSize: "24px",
lineHeight: "32px",
fontWeight: 700,
overflowWrap: "break-word",
hyphens: "manual",
textOverflow: "unset",
whiteSpace: "pre-wrap",
};
type LoginProps = LoginPageProps<LayoutProps, CardProps, FormProps>;
export const AuthPage: React.FC<LoginProps> = ({
contentProps,
wrapperProps,
renderContent,
formProps,
title,
hideForm,
mutationVariables,
}) => {
const { token } = theme.useToken();
const [form] = Form.useForm<LoginFormTypes>();
const translate = useTranslate();
const { mutate: login, isPending } = useLogin<LoginFormTypes>();
const PageTitle =
title === false ? null : (
<div
style={{
display: "flex",
justifyContent: "center",
marginBottom: "32px",
fontSize: "20px",
}}
>
{title ?? <ThemedTitle collapsed={false} />}
</div>
);
const CardContent = (
<Card
// title={CardTitle}
styles={{
header: headStyles,
body: bodyStyles,
}}
style={{
...containerStyles,
backgroundColor: token.colorBgElevated,
}}
{...(contentProps ?? {})}
>
<Form<LoginFormTypes>
layout="vertical"
form={form}
onFinish={(values) => login({ ...values, ...mutationVariables })}
requiredMark={false}
initialValues={{
brokerUrl: 'https://nextgraph.eu'
}}
{...formProps}
>
<Form.Item
name="brokerUrl"
label={translate("pages.login.fields.brokerUrl", "Broker URL")}
rules={[
{
required: true,
message: translate(
"pages.login.errors.requiredBrokerUrl",
"Broker URL is required",
),
},
{
type: "url",
message: translate(
"pages.login.errors.validBrokerUrl",
"Invalid broker URL",
),
},
]}
>
<Input
size="large"
placeholder={translate("pages.login.fields.brokerUrl", "Broker URL")}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
size="large"
htmlType="submit"
loading={isPending}
block
>
{translate("pages.login.signin", "Sign in")}
</Button>
</Form.Item>
</Form>
</Card>
);
return (
<Layout style={layoutStyles} {...(wrapperProps ?? {})}>
<Row
justify="center"
align={hideForm ? "top" : "middle"}
style={{
padding: "16px 0",
minHeight: "100dvh",
paddingTop: hideForm ? "15dvh" : "16px",
}}
>
<Col xs={22}>
{renderContent ? (
renderContent(CardContent, PageTitle)
) : (
<>
{PageTitle}
{CardContent}
</>
)}
</Col>
</Row>
</Layout>
);
};
export default AuthPage;
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
// eslint-disable-next-line
const root = createRoot(container!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+40
View File
@@ -0,0 +1,40 @@
import { useGetIdentity, usePermissions } from "@refinedev/core";
import { Row, Col, Card, Avatar, Typography, Space } from "antd";
const { Text } = Typography;
export const DashboardPage: React.FC = () => {
const { data: identity } = useGetIdentity<{
id: string;
name: string;
avatar: string;
}>();
const permissions = usePermissions<string[], { permissions: string[] }>({
params: { permissions: ["admin"] },
});
return (
<Row gutter={20}>
<Col span={6}>
<Card
title="Identity"
style={{ height: "300px", borderRadius: "15px" }}
headStyle={{ textAlign: "center" }}
>
<Space align="center" direction="horizontal">
<Avatar size="large" src={identity?.avatar} />
<Text>{identity?.name}</Text>
</Space>
</Card>
</Col>
<Col span={6}>
<Card
title="Permissions"
style={{ height: "300px", borderRadius: "15px" }}
headStyle={{ textAlign: "center" }}
>
<Text>{permissions.data}</Text>
</Card>
</Col>
</Row>
);
};
@@ -0,0 +1,33 @@
import { Create, useForm } from "@refinedev/antd";
import { Form, Input } from "antd";
import MDEditor from "@uiw/react-md-editor";
import type { Note } from "../../shapes/orm/post.typings";
export const PostCreate = () => {
const { formProps, saveButtonProps } = useForm<Note>();
return (
<Create saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input placeholder="Enter a post title" />
</Form.Item>
<Form.Item
label="Content"
name="content"
>
<MDEditor data-color-mode="light" />
</Form.Item>
</Form>
</Create>
);
};
@@ -0,0 +1,68 @@
import React, { useCallback, useState } from "react";
import { Edit, useForm, RefreshButton } from "@refinedev/antd";
import { useNavigation, useNotification, type LiveEvent } from "@refinedev/core";
import { Alert, Form, Input } from "antd";
import MDEditor from "@uiw/react-md-editor";
import type { Note } from "../../shapes/orm/post.typings";
export const PostEdit = () => {
const [deprecated, setDeprecated] = useState<boolean>(false);
const { list } = useNavigation();
const { open } = useNotification();
const onLiveEvent = useCallback((event: LiveEvent) => {
console.log('onLiveEvent', event);
if (event.type === "deleted") {
open?.({
type: "error",
message: "This post has been deleted",
});
list("posts");
} else if (event.type === "updated") {
setDeprecated(true);
}
}, [open, list]);
const { formProps, saveButtonProps, query: queryResult } = useForm<Note>({
liveMode: 'manual',
onLiveEvent,
});
const handleRefresh = () => {
queryResult?.refetch();
setDeprecated(false);
};
return (
<Edit saveButtonProps={saveButtonProps}>
<Form {...formProps} layout="vertical">
{deprecated && (
<Alert
message="This post has been updated. Refresh to see changes."
type="warning"
style={{ marginBottom: 20 }}
action={<RefreshButton size="small" onClick={handleRefresh} />}
/>
)}
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Content"
name="content"
>
<MDEditor data-color-mode="light" />
</Form.Item>
</Form>
</Edit>
);
};
@@ -0,0 +1,4 @@
export * from "./list";
export * from "./create";
export * from "./edit";
export * from "./show";
@@ -0,0 +1,49 @@
import {
List,
useTable,
EditButton,
ShowButton,
DeleteButton,
CreateButton,
} from "@refinedev/antd";
import { useNavigation } from "@refinedev/core";
import { Table, Space } from "antd";
import type { Note } from "../../shapes/orm/post.typings";
const shortenNuri = (nuri: string) => {
return nuri?.split(':')[3]?.slice(0, 7);
};
export const PostList = () => {
const { tableProps } = useTable<Note>();
const { show } = useNavigation();
return (
<List headerButtons={<CreateButton />}>
<Table
{...tableProps}
rowKey="@graph"
onRow={(record) => ({
onClick: () => show("posts", record["@graph"]),
style: { cursor: "pointer" },
})}
>
<Table.Column dataIndex="@graph" title="ID" render={shortenNuri} />
<Table.Column dataIndex="title" title="Title" />
<Table.Column dataIndex="content" title="Content" />
<Table.Column<Note>
title="Actions"
dataIndex="actions"
align="right"
render={(_, record) => (
<Space onClick={(e) => e.stopPropagation()}>
<EditButton hideText size="small" recordItemId={record['@graph']} />
<ShowButton hideText size="small" recordItemId={record['@graph']} />
<DeleteButton hideText size="small" recordItemId={record['@graph']} />
</Space>
)}
/>
</Table>
</List>
);
};
@@ -0,0 +1,37 @@
import { useCallback } from "react";
import { useShow, useNavigation, useNotification, type LiveEvent } from "@refinedev/core";
import { Show, MarkdownField } from "@refinedev/antd";
import { Typography } from "antd";
import type { Note } from "../../shapes/orm/post.typings";
const { Title, Text } = Typography;
export const PostShow = () => {
const { list } = useNavigation();
const { open } = useNotification();
const onLiveEvent = useCallback((event: LiveEvent) => {
if (event.type === "deleted") {
open?.({
type: "error",
message: "This post has been deleted",
});
list("posts");
}
}, [open, list]);
const { query: queryResult } = useShow<Note>({ liveMode: "auto", onLiveEvent });
const { data, isLoading } = queryResult;
const record = data?.data;
return (
<Show isLoading={isLoading}>
<Title level={5}>Id</Title>
<Text>{record?.['@graph']}</Text>
<Title level={5}>Title</Title>
<Text>{record?.title}</Text>
<Title level={5}>Content</Title>
<MarkdownField value={record?.content} />
</Show>
);
};
@@ -0,0 +1,10 @@
import { DataModels } from "@ng-org/refine-providers";
import { NoteShapeType } from "./orm/post.shapeTypes";
const dataModels : DataModels = {
posts: {
shapeType: NoteShapeType
}
}
export default dataModels;
@@ -0,0 +1,48 @@
import type { Schema } from "@ng-org/shex-orm";
/**
* =============================================================================
* postSchema: Schema for post
* =============================================================================
*/
export const postSchema = {
"https://www.w3.org/ns/activitystreams#Note": {
iri: "https://www.w3.org/ns/activitystreams#Note",
predicates: [
{
dataTypes: [
{
valType: "iri",
literals: ["https://www.w3.org/ns/activitystreams#Note"],
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "https://www.w3.org/ns/activitystreams#title",
readablePredicate: "title",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "https://www.w3.org/ns/activitystreams#content",
readablePredicate: "content",
},
],
},
} as const satisfies Schema;
@@ -0,0 +1,9 @@
import type { ShapeType } from "@ng-org/shex-orm";
import { postSchema } from "./post.schema";
import type { Note } from "./post.typings";
// ShapeTypes for post
export const NoteShapeType = {
schema: postSchema,
shape: "https://www.w3.org/ns/activitystreams#Note",
} as const satisfies ShapeType<Note>;
@@ -0,0 +1,37 @@
export type IRI = string;
/**
* =============================================================================
* Typescript Typings for post
* =============================================================================
*/
/**
* Note Type
*/
export interface Note {
/**
* The graph NURI.
*/
readonly "@graph": IRI;
/**
* The subject IRI.
*/
readonly "@id": IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
"@type": "https://www.w3.org/ns/activitystreams#Note";
/**
* The title of the post
*
* Original IRI: https://www.w3.org/ns/activitystreams#title
*/
title: string;
/**
* The content of the post
*
* Original IRI: https://www.w3.org/ns/activitystreams#content
*/
content?: string;
}
@@ -0,0 +1,11 @@
PREFIX as: <https://www.w3.org/ns/activitystreams#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
as:Note {
a [as:Note] ;
as:title xsd:string
// rdfs:comment "The title of the post" ;
as:content xsd:string ?
// rdfs:comment "The content of the post" ;
}
+17
View File
@@ -0,0 +1,17 @@
import { ng, init, Session } from "@ng-org/web";
import { initNg } from "@ng-org/orm";
export let session: Session | undefined;
init(
async (event: any) => {
session = event.session;
// The ORM needs to have access to ng,
// the interface to the engine running in WASM.
initNg(ng, session!);
},
true,
[],
);
export const getSession = () => session;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
+113
View File
@@ -0,0 +1,113 @@
{
"name": "@ng-org/refine-providers",
"version": "0.1.2",
"private": false,
"type": "module",
"authors": [
"Sébastien Rosset"
],
"license": "MIT/Apache-2.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./auth-provider": {
"types": "./dist/auth-provider.d.ts",
"default": "./dist/auth-provider.js"
},
"./data-provider": {
"types": "./dist/data-provider.d.ts",
"default": "./dist/data-provider.js"
},
"./live-provider": {
"types": "./dist/live-provider.d.ts",
"default": "./dist/live-provider.js"
},
"./types": {
"types": "./dist/types.d.ts"
},
"./utils": {
"types": "./dist/utils.d.ts",
"default": "./dist/utils.js"
}
},
"targets": {
"index": {
"source": "src/index.ts",
"distDir": "dist",
"context": "browser",
"outputFormat": "esmodule",
"isLibrary": true,
"includeNodeModules": false,
"sourceMap": true
},
"auth-provider": {
"source": "src/auth-provider.ts",
"distDir": "dist",
"context": "browser",
"outputFormat": "esmodule",
"isLibrary": true,
"includeNodeModules": false,
"sourceMap": true
},
"data-provider": {
"source": "src/data-provider.ts",
"distDir": "dist",
"context": "browser",
"outputFormat": "esmodule",
"isLibrary": true,
"includeNodeModules": false,
"sourceMap": true
},
"live-provider": {
"source": "src/live-provider.ts",
"distDir": "dist",
"context": "browser",
"outputFormat": "esmodule",
"isLibrary": true,
"includeNodeModules": false,
"sourceMap": true
},
"utils": {
"source": "src/utils.ts",
"distDir": "dist",
"context": "browser",
"outputFormat": "esmodule",
"isLibrary": true,
"includeNodeModules": false,
"sourceMap": true
}
},
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "rm -rf dist .parcel-cache && parcel build && tsc -p tsconfig.build.json",
"watch": "concurrently -n parcel,tsc -c blue,yellow \"parcel watch --no-cache\" \"tsc -p tsconfig.build.json --watch --preserveWatchOutput\"",
"typecheck": "tsc --noEmit",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@refinedev/core": "^5.0.12",
"@ng-org/alien-deepsignals": "0.1.2-alpha.12",
"@ng-org/orm": "0.1.2-alpha.19",
"@ng-org/shex-orm": "0.1.2-alpha.8",
"@ng-org/web": "0.1.2-alpha.13"
},
"devDependencies": {
"@ng-org/alien-deepsignals": "0.1.2-alpha.12",
"@ng-org/orm": "0.1.2-alpha.19",
"@ng-org/shex-orm": "0.1.2-alpha.8",
"@ng-org/web": "0.1.2-alpha.13",
"@refinedev/core": "^5.0.12",
"concurrently": "^10.0.3",
"parcel": "^2.16.4",
"typescript": "^5.8.3"
}
}
+9532
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
packages:
- examples/*
+96
View File
@@ -0,0 +1,96 @@
import {
type AuthProvider,
} from "@refinedev/core";
import {
ng,
init as initNgWeb,
type Session as NextGraphSession,
} from "@ng-org/web";
import { initNg as initNgSignals } from "@ng-org/orm";
export type NgAuthProvider = AuthProvider & {
getSession: () => NextGraphSession | undefined;
};
/** The session with the NextGraph engine or undefined if not loaded. */
export let session: NextGraphSession | undefined;
export let resolveSessionPromise: (
value: NextGraphSession | PromiseLike<NextGraphSession>,
) => void;
export let rejectSessionPromise: (reason?: any) => void;
/** Resolves to the current NextGraph session. */
export let sessionPromise: Promise<NextGraphSession> = new Promise(
(resolve, reject) => {
resolveSessionPromise = resolve;
rejectSessionPromise = reject;
},
);
const authProvider: NgAuthProvider = {
login: async ({ brokerUrl }) => {
// try {
// await initNgWeb(
// async (event: any) => {
// session = event.session;
// session!.ng ??= ng;
// localStorage.setItem("logged", "true");
// console.log('session', session);
// resolveSessionPromise(session!);
// initNgSignals(ng, session!);
// },
// true,
// [],
// );
// } catch (error) {
// rejectSessionPromise(error);
// }
return {
success: true
};
},
register: async ({ brokerUrl }) => {
console.log('brokerUrl', brokerUrl);
return {
success: true
};
},
logout: async () => {
localStorage.removeItem("logged");
return {
success: true,
redirectTo: "/login",
};
},
onError: async (error) => {
if (error.response?.status === 401) {
return {
logout: true,
};
}
return { error };
},
check: async () => {
return ({ authenticated: localStorage.getItem("logged") === 'true' });
},
getPermissions: async (params) => params?.permissions,
getIdentity: async () => ({
id: 1,
name: "Jane Doe",
avatar:
"https://unsplash.com/photos/IWLOvomUmWU/download?force=true&w=640",
}),
getSession : () => session
};
export default authProvider;
+104
View File
@@ -0,0 +1,104 @@
import { DataProvider } from "@refinedev/core";
import { ng } from "@ng-org/web";
import { getObjects } from "@ng-org/orm";
import { addIdToObject, isGraphKey, getSignalObject } from "./utils";
import { DataProviderConfig } from "./types";
const dataProvider = ({ dataModels, getSession }: DataProviderConfig) : DataProvider => ({
getList: async ({ resource, pagination, sorters, filters, meta }) => {
console.log('list', resource, pagination, sorters, filters, meta);
const objects = await getObjects(dataModels[resource].shapeType, "");
return {
data: [...objects].map(addIdToObject) as any,
total: 1,
};
},
create: async ({ resource, variables, meta }) => {
console.log("create", resource, variables, meta);
const session = getSession();
console.log('session', session);
const docNuri = await ng.doc_create(
session!.session_id,
"Graph",
"data:graph",
"store", // Private store
undefined
);
const set = await getSignalObject(dataModels[resource].shapeType, [docNuri]);
set.add({
"@graph": docNuri,
"@type": "https://www.w3.org/ns/activitystreams#Note",
"@id": "",
...variables
});
const newData = set.first();
return {
data: addIdToObject({ ...newData }),
};
},
update: async ({ resource, id, variables, meta }) => {
console.log('update', resource, id, variables, meta);
const set = await getSignalObject(dataModels[resource].shapeType, [`${id}`]);
const oldData = set.first()!;
const objectId = oldData["@id"];
const newData = {
"@graph": id,
"@type": "https://www.w3.org/ns/activitystreams#Note",
"@id": objectId,
...variables
};
// Delete old data and add new data to the set
// Hopefully the ORM will handle this intelligently
set.delete(oldData);
set.add(newData);
return {
data: addIdToObject(newData)
};
},
deleteOne: async ({ resource, id, variables, meta }) => {
console.log('deleteOne', resource, id, variables, meta);
const set = await getSignalObject(dataModels[resource].shapeType, [`${id}`]);
const oldData = set.first()!;
set.delete(oldData);
return {
data: addIdToObject({ ...oldData })
};
},
getOne: async ({ resource, id, meta }) => {
console.log('getOne', resource, id, meta);
// The ID should be a graph URI, but if it's an object key, we can use the subjects filter to the the object
const objects = await getObjects(dataModels[resource].shapeType, isGraphKey(id) ? `${id}` : { subjects: [`${id}`] });
return {
data: addIdToObject([...objects][0])
}
},
getApiUrl: () => "",
// optional methods
// getMany: ({ resource, ids, meta }) => Promise,
// createMany: ({ resource, variables, meta }) => Promise,
// deleteMany: ({ resource, ids, variables, meta }) => Promise,
// updateMany: ({ resource, ids, variables, meta }) => Promise,
// custom: ({ url, method, filters, sorters, payload, query, headers, meta }) => Promise,
});
export default dataProvider;
+8
View File
@@ -0,0 +1,8 @@
export { default as authProvider } from "./auth-provider";
export * from "./auth-provider";
export { default as dataProvider } from "./data-provider";
export { default as liveProvider } from "./live-provider";
export * from "./types";
export * from "./utils";
+76
View File
@@ -0,0 +1,76 @@
import { LiveProvider } from "@refinedev/core";
import { watch } from "@ng-org/alien-deepsignals"
import { getSignalObject } from "./utils";
import { parsePatches } from "./utils";
import { LiveProviderConfig, UnwatchFn } from "./types";
const liveProvider = ({ dataModels }: LiveProviderConfig) : LiveProvider => ({
subscribe: async ({ channel, params, types, callback, meta }) : Promise<UnwatchFn> => {
console.log('subscribe', channel, params?.subscriptionType, params, types, meta);
const ids = params?.ids as string[];
if (channel.startsWith("resources/")) {
const resource = channel.split('/')[1];
if (dataModels[resource]) {
if (ids?.length > 0) {
// We don't close the subscription because we want to keep listening for changes
const set = await getSignalObject(dataModels[resource].shapeType, ids, false);
const { stopListening } = watch(set, ({ newValue, patches }) => {
console.log('object modified', newValue, patches);
const modifiedIds = parsePatches(patches);
for( const [graphId, eventType] of Object.entries(modifiedIds)) {
callback({
channel,
type: eventType,
payload: { ids: [graphId] },
date: new Date(),
});
}
});
return stopListening;
} else {
// We don't close the subscription because we want to keep listening for changes
const set = await getSignalObject(dataModels[resource].shapeType, undefined, false);
console.log('set listening list', set);
const { stopListening } = watch(set, ({ newValue, patches }) => {
console.log('objects list modified', newValue, patches);
const modifiedIds = parsePatches(patches);
for( const [graphId, eventType] of Object.entries(modifiedIds)) {
callback({
channel,
type: eventType,
payload: { ids: [graphId] },
date: new Date(),
});
}
});
return stopListening;
}
}
}
return () => undefined;
},
unsubscribe: async (unwatchFn: UnwatchFn | Promise<UnwatchFn>) => {
console.log('unsubscribe', typeof unwatchFn, unwatchFn);
const fn = await Promise.resolve(unwatchFn);
if (typeof fn === "function") {
fn();
}
}
});
export default liveProvider;
+19
View File
@@ -0,0 +1,19 @@
import type { ShapeType } from "@ng-org/shex-orm";
import type { Session } from "@ng-org/web";
export type DataModel = {
shapeType: ShapeType<any>;
}
export type DataModels = Record<string, DataModel>;
export type DataProviderConfig = {
dataModels: DataModels;
getSession: () => Session | undefined;
};
export type LiveProviderConfig = {
dataModels: DataModels;
};
export type UnwatchFn = () => void;
+54
View File
@@ -0,0 +1,54 @@
import { BaseKey } from "@refinedev/core";
import { OrmSubscription } from "@ng-org/orm";
import type { ShapeType } from "@ng-org/shex-orm";
import { DeepSignalSet, DeepPatch } from "@ng-org/alien-deepsignals"
export const getSignalObject = async (shapeType: ShapeType<any>, ids?: string[], closeSubscription: boolean = true) : Promise<DeepSignalSet<any>> => {
const subscription = OrmSubscription.getOrCreate(shapeType, { graphs: ids || [''] });
await subscription.readyPromise;
const { signalObject } = subscription;
if (closeSubscription) subscription.close();
return signalObject;
}
export const isGraphKey = (key: BaseKey): boolean => {
return key !== null && typeof key === "string" && key.includes(":v:");
}
export const addIdToObject = (obj: any) => {
if (!obj["@graph"]) {
throw new Error("Object is missing @graph property");
}
return { id: obj["@graph"], ...obj };
};
export const parsePatches = (patches: DeepPatch[]) => {
let modifiedIds : Record<string, 'created' | 'updated' | 'deleted'> = {};
for( const patch of patches) {
// The path property include the @id property first, then the modified field
const [graphId] = (patch.path[0] as string).split('|');
const modifiedField = patch.path[1] as string;
// If a remove operation includes only the ID, this is a deletion
if (patch.op === "remove" && !modifiedField) {
modifiedIds[graphId] = 'deleted';
}
// If an add operation involves an @id, this is a creation
if (patch.op === "add" && modifiedField === "@id") {
if (modifiedIds[graphId] === 'deleted') {
// If the object was previously marked as deleted, consider this an update instead
modifiedIds[graphId] = 'updated';
} else {
modifiedIds[graphId] = 'created';
}
}
if (modifiedIds[graphId] !== 'created' && modifiedIds[graphId] !== 'deleted') {
modifiedIds[graphId] = 'updated';
}
};
return modifiedIds;
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"emitDeclarationOnly": true,
"declaration": true,
"rootDir": "src",
"outDir": "dist"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"noEmit": true
},
"include": ["src"]
}