Widen the backend contract from one entity to the set
The contract carried a single event binding, which only worked while Events was the one vertical on Refine. It now carries one binding per persisted entity, and each binding says which resource name it lives under and who must be able to read it. Visibility is DECLARED, not yet enforced: Festipod's confidentiality is per-entity while Refine has no notion of it, and the NextGraph provider reads across every graph it can see. Naming it marks where each backend has to close that gap rather than pretending it is closed. meetingPoint is optional for now because its SHEX shape does not exist on this branch; adding it means regenerating the ORM bindings, which drops the Fp prefix and is its own step. Friendship is absent on purpose — it has no shape and no persistence anywhere in this project.
This commit is contained in:
@@ -246,7 +246,7 @@ function useNgData(): FestipodDataContextValue {
|
||||
const { username } = useAccount();
|
||||
// The selected backend's binding for events — read here, so a switch re-renders
|
||||
// with the new one instead of keeping whichever was current at import time.
|
||||
const eventBinding = useBackend().event;
|
||||
const eventBinding = useBackend().bindings.event;
|
||||
// Mono-store fallback scope: the shared wallet's private store NURI. Opens the
|
||||
// repo in the verifier (enables reads + writes), per the data-layer rule.
|
||||
const privateNuri = session ? `did:ng:${session.private_store_id}` : undefined;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
|
||||
import { Refine } from '@refinedev/core';
|
||||
import { getBackend, initialBackendName, type RefineBackend } from './providers';
|
||||
import { getBackend, initialBackendName, resourceNames, type RefineBackend } from './providers';
|
||||
|
||||
interface BackendSelection {
|
||||
backend: RefineBackend;
|
||||
@@ -81,7 +81,7 @@ export function RefineProvider({ children }: { children: ReactNode }) {
|
||||
dataProvider={providers.dataProvider}
|
||||
liveProvider={providers.liveProvider}
|
||||
authProvider={providers.authProvider}
|
||||
resources={backend.resources.map((resource) => ({ name: resource }))}
|
||||
resources={resourceNames(backend).map((resource) => ({ name: resource }))}
|
||||
options={{
|
||||
// `auto` is what makes the live provider actually subscribe behind every
|
||||
// query — without it the ORM pushes changes and no hook is listening.
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
import { nextgraphBackend } from './nextgraph';
|
||||
import type { RefineBackend } from './types';
|
||||
|
||||
export type { RecordBinding, RefineBackend } from './types';
|
||||
export type { RecordBinding, RecordBindings, RefineBackend } from './types';
|
||||
export { resourceNames } from './types';
|
||||
|
||||
const BACKENDS: Record<string, RefineBackend> = {
|
||||
[nextgraphBackend.name]: nextgraphBackend,
|
||||
|
||||
@@ -9,24 +9,51 @@
|
||||
|
||||
import { dataProvider, liveProvider } from '@ng-org/refine-providers';
|
||||
import type { BaseKey } from '@refinedev/core';
|
||||
import type { FpEventData } from '../../data/types';
|
||||
import type { FpEvent } from '../../shapes/orm/festipodShapes.typings';
|
||||
import shapeTypes, { RESOURCE_EVENTS } from '../shapeTypes';
|
||||
import type { FpEventData, FpParticipationData, FpUserData } from '../../data/types';
|
||||
import type { FpEvent, FpParticipation, FpUserProfile } from '../../shapes/orm/festipodShapes.typings';
|
||||
import shapeTypes, {
|
||||
RESOURCE_EVENTS,
|
||||
RESOURCE_PARTICIPATIONS,
|
||||
RESOURCE_PROFILES,
|
||||
} from '../shapeTypes';
|
||||
import { createNgAuthProvider } from '../ngAuthProvider';
|
||||
import type { RecordBinding, RefineBackend } from './types';
|
||||
|
||||
/** The RDF class Festipod events are persisted as. */
|
||||
const EVENT_TYPE = 'http://festipod.org/Event';
|
||||
/**
|
||||
* The RDF classes Festipod persists. These IRIs ARE the stored data — renaming
|
||||
* one renames what is on disk, so they are never adjusted for convenience.
|
||||
*/
|
||||
const TYPE = {
|
||||
event: 'http://festipod.org/Event',
|
||||
profile: 'http://festipod.org/UserProfile',
|
||||
participation: 'http://festipod.org/Participation',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Shared by every binding here: this provider addresses a record by the DOCUMENT
|
||||
* NURI, while the app's entities reference each other by the SUBJECT IRI. One
|
||||
* document per record keeps them 1-1, but they are different strings and `update`
|
||||
* only accepts the first.
|
||||
*/
|
||||
const recordKey = (record: unknown): BaseKey =>
|
||||
(record as { '@graph': string })['@graph'] as BaseKey;
|
||||
const domainId = (record: unknown): string => (record as { '@id': string })['@id'];
|
||||
|
||||
/** This provider's `update` deletes the old object and adds one built from values. */
|
||||
const replaces = {
|
||||
updateReplaces: true as const,
|
||||
toPatch: (): never => {
|
||||
throw new Error('nextgraph: update replaces the record — send the whole of it, not a patch');
|
||||
},
|
||||
};
|
||||
|
||||
const eventBinding: RecordBinding<FpEventData> = {
|
||||
// Refine addresses a record by its DOCUMENT NURI. `create` makes one document
|
||||
// per record, so this is 1-1 with the subject IRI below — but it is a different
|
||||
// string, and `update` only accepts this one.
|
||||
recordKey: (record) => (record as FpEvent)['@graph'] as BaseKey,
|
||||
|
||||
// What the rest of the app references: a participation's `fp:event` holds the
|
||||
// SUBJECT IRI, so that is the id `FpEventData` must carry.
|
||||
domainId: (record) => (record as FpEvent)['@id'],
|
||||
resource: RESOURCE_EVENTS,
|
||||
// Events are what other users must find — see concept `functional-domain`.
|
||||
visibility: 'public',
|
||||
recordKey,
|
||||
domainId,
|
||||
...replaces,
|
||||
|
||||
toDomain: (record) => {
|
||||
const e = record as FpEvent;
|
||||
@@ -49,7 +76,7 @@ const eventBinding: RecordBinding<FpEventData> = {
|
||||
// the caller's values AFTER it, so passing `@type` is what makes the record a
|
||||
// Festipod Event. Drop it and the shape stops matching on read.
|
||||
toValues: (data) => ({
|
||||
'@type': EVENT_TYPE,
|
||||
'@type': TYPE.event,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
date: data.date,
|
||||
@@ -60,22 +87,73 @@ const eventBinding: RecordBinding<FpEventData> = {
|
||||
hostName: data.hostName,
|
||||
hostInitials: data.hostInitials,
|
||||
}),
|
||||
};
|
||||
|
||||
// Measured: `update` deletes the old object and adds one built from `values`.
|
||||
updateReplaces: true,
|
||||
const profileBinding: RecordBinding<FpUserData> = {
|
||||
resource: RESOURCE_PROFILES,
|
||||
// A profile is readable by one's network, not by everyone.
|
||||
visibility: 'protected',
|
||||
recordKey,
|
||||
domainId,
|
||||
...replaces,
|
||||
|
||||
// Never reached while `updateReplaces` is true. It exists so a patching backend
|
||||
// has somewhere to put its answer, and it fails loudly rather than sending a
|
||||
// partial record to a provider that would drop everything else.
|
||||
toPatch: () => {
|
||||
throw new Error('nextgraph: update replaces the record — send the whole of it, not a patch');
|
||||
toDomain: (record) => {
|
||||
const u = record as FpUserProfile;
|
||||
return {
|
||||
id: u['@id'],
|
||||
name: u.name,
|
||||
initials: u.initials,
|
||||
username: u.username,
|
||||
role: u.role,
|
||||
isPublic: u.isPublic,
|
||||
};
|
||||
},
|
||||
|
||||
toValues: (data) => ({
|
||||
'@type': TYPE.profile,
|
||||
name: data.name,
|
||||
initials: data.initials,
|
||||
username: data.username,
|
||||
role: data.role,
|
||||
isPublic: data.isPublic,
|
||||
}),
|
||||
};
|
||||
|
||||
const participationBinding: RecordBinding<FpParticipationData> = {
|
||||
resource: RESOURCE_PARTICIPATIONS,
|
||||
visibility: 'protected',
|
||||
recordKey,
|
||||
domainId,
|
||||
...replaces,
|
||||
|
||||
// `fp:event` and `fp:user` hold SUBJECT IRIs, which is why `domainId` above is
|
||||
// the subject and not the document: these references resolve against it.
|
||||
toDomain: (record) => {
|
||||
const p = record as FpParticipation;
|
||||
return {
|
||||
id: p['@id'],
|
||||
eventId: p.event,
|
||||
userId: p.user,
|
||||
isConfirmed: p.isConfirmed,
|
||||
};
|
||||
},
|
||||
|
||||
toValues: (data) => ({
|
||||
'@type': TYPE.participation,
|
||||
event: data.eventId,
|
||||
user: data.userId,
|
||||
isConfirmed: data.isConfirmed,
|
||||
}),
|
||||
};
|
||||
|
||||
export const nextgraphBackend: RefineBackend = {
|
||||
name: 'nextgraph',
|
||||
resources: [RESOURCE_EVENTS],
|
||||
event: eventBinding,
|
||||
bindings: {
|
||||
event: eventBinding,
|
||||
profile: profileBinding,
|
||||
participation: participationBinding,
|
||||
// meetingPoint: pending its SHEX shape — see types.ts.
|
||||
},
|
||||
build: () => {
|
||||
// Festipod's own AuthProvider, not the package's — see ngAuthProvider for why
|
||||
// a second `init()` on one WASM session is a collision, not a convenience.
|
||||
|
||||
@@ -26,7 +26,12 @@
|
||||
*/
|
||||
|
||||
import type { AuthProvider, BaseKey, DataProvider, LiveProvider } from '@refinedev/core';
|
||||
import type { FpEventData } from '../../data/types';
|
||||
import type {
|
||||
FpEventData,
|
||||
FpMeetingPointData,
|
||||
FpParticipationData,
|
||||
FpUserData,
|
||||
} from '../../data/types';
|
||||
|
||||
/** How one entity's records translate to and from the app's domain type. */
|
||||
export interface RecordBinding<T extends { id: string }> {
|
||||
@@ -52,6 +57,41 @@ export interface RecordBinding<T extends { id: string }> {
|
||||
toPatch(updates: Partial<T>): Record<string, unknown>;
|
||||
/** True when `update` replaces the record rather than patching it. */
|
||||
readonly updateReplaces: boolean;
|
||||
/**
|
||||
* Which Refine resource these records live under. The NAME is the backend's,
|
||||
* not the app's: `@ng-org` keys `shapeTypes` by it, `@activitypods` keys
|
||||
* `resources` by it, and nothing says the two must agree on a spelling.
|
||||
*/
|
||||
readonly resource: string;
|
||||
/**
|
||||
* WHO MUST BE ABLE TO READ these records. Festipod's confidentiality model is
|
||||
* per-entity (concept `functional-domain`), and each backend enforces it its own
|
||||
* way — NextGraph by placing a document in a store, ActivityPods by shape trees
|
||||
* and ACLs. Refine itself has NO notion of it, so the intent is declared here
|
||||
* and the backend is what honours it.
|
||||
*
|
||||
* Naming it does not make it enforced: the NextGraph data provider currently
|
||||
* reads across every graph it can see, so this records the INTENT and marks
|
||||
* where a backend has to close the gap.
|
||||
*/
|
||||
readonly visibility: 'public' | 'protected' | 'private';
|
||||
}
|
||||
|
||||
/**
|
||||
* The entities the app binds. One key per domain type it persists.
|
||||
*
|
||||
* `meetingPoint` is OPTIONAL only because its SHEX shape does not exist on this
|
||||
* branch yet — adding it means regenerating the ORM bindings, which is its own
|
||||
* step with its own failure mode (the generator drops the `Fp` prefix, since the
|
||||
* emitted name follows the shape IRI). Once that lands it becomes required like
|
||||
* the others. `Friendship` is deliberately absent: it has no shape and no
|
||||
* persistence anywhere in this project.
|
||||
*/
|
||||
export interface RecordBindings {
|
||||
event: RecordBinding<FpEventData>;
|
||||
profile: RecordBinding<FpUserData>;
|
||||
participation: RecordBinding<FpParticipationData>;
|
||||
meetingPoint?: RecordBinding<FpMeetingPointData>;
|
||||
}
|
||||
|
||||
/** Everything Festipod needs from one Refine backend. */
|
||||
@@ -67,8 +107,11 @@ export interface RefineBackend {
|
||||
authProvider?: AuthProvider;
|
||||
liveProvider?: LiveProvider;
|
||||
};
|
||||
/** Resource names this backend serves, for `<Refine resources>`. */
|
||||
readonly resources: readonly string[];
|
||||
/** The Events vertical is the only one wired so far. */
|
||||
readonly event: RecordBinding<FpEventData>;
|
||||
/** One binding per entity the app persists. */
|
||||
readonly bindings: RecordBindings;
|
||||
}
|
||||
|
||||
/** Every resource name a backend serves, derived from its bindings. */
|
||||
export function resourceNames(backend: RefineBackend): string[] {
|
||||
return [...new Set(Object.values(backend.bindings).map((b) => b.resource))];
|
||||
}
|
||||
|
||||
@@ -5,21 +5,30 @@
|
||||
* `@ng-org/refine-providers` identifies data by shape type, not by endpoint: its
|
||||
* data provider looks `shapeTypes[resource]` up on every call, so a resource name
|
||||
* that is absent here silently resolves to `undefined` and the read comes back
|
||||
* empty. Every `useList` / `useOne` / `useCreate` / `useUpdate` call site must
|
||||
* therefore name a resource declared below.
|
||||
* empty — no error, just nothing. Every `useList` / `useOne` / `useCreate` /
|
||||
* `useUpdate` call site must therefore name a resource declared below.
|
||||
*
|
||||
* Only `events` is wired: the Events vertical is what goes through Refine —
|
||||
* profiles and participations still read through `useShapeWithDefaults`.
|
||||
* These names are the NextGraph backend's, which is why this file lives beside it
|
||||
* rather than in the app: another backend keys its own configuration by its own
|
||||
* names, and nothing says the two must agree on a spelling.
|
||||
*/
|
||||
|
||||
import type { ShapeTypes } from '@ng-org/refine-providers';
|
||||
import { FpEventShapeType } from '../shapes/orm/festipodShapes.shapeTypes';
|
||||
import {
|
||||
FpEventShapeType,
|
||||
FpParticipationShapeType,
|
||||
FpUserProfileShapeType,
|
||||
} from '../shapes/orm/festipodShapes.shapeTypes';
|
||||
|
||||
/** Refine resource name for Festipod events. Import it — never retype the string. */
|
||||
/** Resource names. Import them — never retype the strings. */
|
||||
export const RESOURCE_EVENTS = 'events';
|
||||
export const RESOURCE_PROFILES = 'profiles';
|
||||
export const RESOURCE_PARTICIPATIONS = 'participations';
|
||||
|
||||
const shapeTypes: ShapeTypes = {
|
||||
[RESOURCE_EVENTS]: FpEventShapeType,
|
||||
[RESOURCE_PROFILES]: FpUserProfileShapeType,
|
||||
[RESOURCE_PARTICIPATIONS]: FpParticipationShapeType,
|
||||
};
|
||||
|
||||
export default shapeTypes;
|
||||
|
||||
Reference in New Issue
Block a user