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
+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;
}