Persist the meeting point, and absorb the prefix the generator drops

The meeting point is the product's unit of value and had no shape at all: it
lived in React state, lost on reload. The shape comes from the sibling branch
rather than from my own reading of the domain -- event, host, title, optional
description/place/time, and the optional inbox its own backend will want.

Regenerating the bindings does what that branch documented it would: the
emitted names lose the Fp prefix, because they follow each shape's IRI and
those IRIs are the persisted RDF classes, not ours to rename. So the aliasing
happens at the four import sites, which keeps every downstream name unchanged
and stops the DOM's own Event from being shadowed. The generated files are
never edited; build:orm overwrites them.

I had called this blocked, as "a binding with no consumer". That was wrong and
worth correcting: the shape and the bindings are the PREREQUISITE every
NextGraph-flavoured backend needs, and only the RecordBinding waits for one.
The RecordBindings slot stays optional until a backend can fill it.

Still local-only in the data context: persisting a meeting point needs a
backend to write it through, and the registry is empty.
This commit is contained in:
Sylvain Duchesne
2026-10-01 19:58:21 +02:00
parent 33b4588da4
commit a5f6e01562
8 changed files with 318 additions and 43 deletions
+16 -7
View File
@@ -28,13 +28,22 @@ import { useBackend } from '../refine/RefineProvider';
// custom vars, so a bare `process.env.FESTIPOD_MULTISTORE` throws
// "process is not defined" in the browser harness. Guard it.
const MULTISTORE = typeof process !== 'undefined' && process?.env?.FESTIPOD_MULTISTORE === '1';
// `FpEventShapeType` is NOT imported here any more: events go through Refine, and
// the resource → shape binding lives in refine/shapeTypes.ts.
import {
FpUserProfileShapeType,
FpParticipationShapeType,
} from '../shapes/orm/festipodShapes.shapeTypes';
import type { FpEvent, FpUserProfile, FpParticipation } from '../shapes/orm/festipodShapes.typings';
// NO ShapeType IMPORTED HERE ANY MORE: every entity reads through a Refine
// backend, and the resource → shape binding is the backend's business.
// The ORM generator emits BARE shape names (`Event`, `UserProfile`, …), taken from
// each shape's IRI local name. They are aliased back to the `Fp*` spelling HERE,
// at the import, so nothing downstream depends on the generator's naming: `Event`
// is a DOM global, and the bare names collide with the app's own vocabulary
// (`FpEventData` & co.). The prefix cannot be restored at the generator — the
// emitted name follows the shape IRI, and those IRIs are the persisted RDF
// classes, not ours to rename. Never edit the generated files; `build:orm`
// overwrites them.
import type {
Event as FpEvent,
MeetingPoint as FpMeetingPoint,
Participation as FpParticipation,
UserProfile as FpUserProfile,
} from '../shapes/orm/festipodShapes.typings';
import { bootstrapWallet, type BootstrapResult } from '../utils/ngBootstrap';
import { autoSeedEnabled, shouldAutoSeed } from '../utils/autoSeed';
+181 -19
View File
@@ -5,7 +5,7 @@ import type { Schema } from "@ng-org/shex-orm";
* festipodShapesSchema: Schema for festipodShapes
* =============================================================================
*/
export const festipodShapesSchema: Schema = {
export const festipodShapesSchema = {
"http://festipod.org/Event": {
iri: "http://festipod.org/Event",
predicates: [
@@ -22,63 +22,99 @@ export const festipodShapesSchema: Schema = {
readablePredicate: "@type",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/title",
readablePredicate: "title",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/description",
readablePredicate: "description",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/date",
readablePredicate: "date",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/location",
readablePredicate: "location",
},
{
dataTypes: [{ valType: "number" }],
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/distance",
readablePredicate: "distance",
},
{
dataTypes: [{ valType: "number" }],
dataTypes: [
{
valType: "number",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/participantCount",
readablePredicate: "participantCount",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/coverImage",
readablePredicate: "coverImage",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/hostName",
readablePredicate: "hostName",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/hostInitials",
@@ -102,35 +138,55 @@ export const festipodShapesSchema: Schema = {
readablePredicate: "@type",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/name",
readablePredicate: "name",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/initials",
readablePredicate: "initials",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/username",
readablePredicate: "username",
},
{
dataTypes: [{ valType: "string" }],
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/role",
readablePredicate: "role",
},
{
dataTypes: [{ valType: "boolean" }],
dataTypes: [
{
valType: "boolean",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/isPublic",
@@ -154,21 +210,33 @@ export const festipodShapesSchema: Schema = {
readablePredicate: "@type",
},
{
dataTypes: [{ valType: "iri" }],
dataTypes: [
{
valType: "iri",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/event",
readablePredicate: "event",
},
{
dataTypes: [{ valType: "iri" }],
dataTypes: [
{
valType: "iri",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/user",
readablePredicate: "user",
},
{
dataTypes: [{ valType: "boolean" }],
dataTypes: [
{
valType: "boolean",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/isConfirmed",
@@ -176,4 +244,98 @@ export const festipodShapesSchema: Schema = {
},
],
},
};
"http://festipod.org/MeetingPoint": {
iri: "http://festipod.org/MeetingPoint",
predicates: [
{
dataTypes: [
{
valType: "iri",
literals: ["http://festipod.org/MeetingPoint"],
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
},
{
dataTypes: [
{
valType: "iri",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/event",
readablePredicate: "event",
},
{
dataTypes: [
{
valType: "iri",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/host",
readablePredicate: "host",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/title",
readablePredicate: "title",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/description",
readablePredicate: "description",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/place",
readablePredicate: "place",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/time",
readablePredicate: "time",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/inbox",
readablePredicate: "inbox",
},
],
},
} as const satisfies Schema;
@@ -1,17 +1,30 @@
import type { ShapeType } from "@ng-org/shex-orm";
import { festipodShapesSchema } from "./festipodShapes.schema";
import type { FpEvent, FpUserProfile, FpParticipation } from "./festipodShapes.typings";
import { festipodShapesSchema } from "./festipodShapes.schema.ts";
import type {
Event,
UserProfile,
Participation,
MeetingPoint,
} from "./festipodShapes.typings.ts";
// ShapeTypes for festipodShapes
export const FpEventShapeType: ShapeType<FpEvent> = {
export const EventShapeType: ShapeType<Event> = {
schema: festipodShapesSchema,
shape: "http://festipod.org/Event",
};
export const FpUserProfileShapeType: ShapeType<FpUserProfile> = {
export const UserProfileShapeType: ShapeType<UserProfile> = {
schema: festipodShapesSchema,
shape: "http://festipod.org/UserProfile",
};
export const FpParticipationShapeType: ShapeType<FpParticipation> = {
export const ParticipationShapeType: ShapeType<Participation> = {
schema: festipodShapesSchema,
shape: "http://festipod.org/Participation",
};
export const MeetingPointShapeType: ShapeType<MeetingPoint> = {
schema: festipodShapesSchema,
shape: "http://festipod.org/MeetingPoint",
};
@@ -9,9 +9,9 @@ export type IRI = string;
/**
* Event Type
*/
export interface FpEvent {
export interface Event {
/**
* The graph IRI.
* The graph NURI.
*/
readonly "@graph": IRI;
/**
@@ -81,9 +81,9 @@ export interface FpEvent {
/**
* UserProfile Type
*/
export interface FpUserProfile {
export interface UserProfile {
/**
* The graph IRI.
* The graph NURI.
*/
readonly "@graph": IRI;
/**
@@ -129,9 +129,9 @@ export interface FpUserProfile {
/**
* Participation Type
*/
export interface FpParticipation {
export interface Participation {
/**
* The graph IRI.
* The graph NURI.
*/
readonly "@graph": IRI;
/**
@@ -161,3 +161,63 @@ export interface FpParticipation {
*/
isConfirmed: boolean;
}
/**
* MeetingPoint Type
*/
export interface MeetingPoint {
/**
* 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": "http://festipod.org/MeetingPoint";
/**
* Reference to the public event this meeting point is anchored to
*
* Original IRI: http://festipod.org/event
*/
event: IRI;
/**
* Reference to the user hosting the meeting point
*
* Original IRI: http://festipod.org/host
*/
host: IRI;
/**
* The title of the meeting point
*
* Original IRI: http://festipod.org/title
*/
title: string;
/**
* A description of the meeting point
*
* Original IRI: http://festipod.org/description
*/
description?: string;
/**
* Where participants gather (e.g. 'Entrée principale, sous l'horloge')
*
* Original IRI: http://festipod.org/place
*/
place?: string;
/**
* When participants gather (display string)
*
* Original IRI: http://festipod.org/time
*/
time?: string;
/**
* NURI of the meeting point's inbox (wired in T02.b/c)
*
* Original IRI: http://festipod.org/inbox
*/
inbox?: string;
}
@@ -47,3 +47,21 @@ fp:Participation {
fp:isConfirmed xsd:boolean
// rdfs:comment "Whether the participation is confirmed" ;
}
fp:MeetingPoint {
a [fp:MeetingPoint] ;
fp:event IRI
// rdfs:comment "Reference to the public event this meeting point is anchored to" ;
fp:host IRI
// rdfs:comment "Reference to the user hosting the meeting point" ;
fp:title xsd:string
// rdfs:comment "The title of the meeting point" ;
fp:description xsd:string ?
// rdfs:comment "A description of the meeting point" ;
fp:place xsd:string ?
// rdfs:comment "Where participants gather (e.g. 'Entrée principale, sous l'horloge')" ;
fp:time xsd:string ?
// rdfs:comment "When participants gather (display string)" ;
fp:inbox xsd:string ?
// rdfs:comment "NURI of the meeting point's inbox (wired in T02.b/c)" ;
}
+9 -4
View File
@@ -17,11 +17,16 @@ import { useShapeSet, useShapeSubscription } from '../hooks/useShapeSet';
import { ng } from '@ng-org/web';
import type { DeepSignalSet } from '@ng-org/alien-deepsignals';
import {
FpEventShapeType,
FpUserProfileShapeType,
FpParticipationShapeType,
EventShapeType as FpEventShapeType,
ParticipationShapeType as FpParticipationShapeType,
UserProfileShapeType as FpUserProfileShapeType,
} from '../shapes/orm/festipodShapes.shapeTypes';
import type { FpEvent, FpUserProfile, FpParticipation } from '../shapes/orm/festipodShapes.typings';
import type {
Event as FpEvent,
MeetingPoint as FpMeetingPoint,
Participation as FpParticipation,
UserProfile as FpUserProfile,
} from '../shapes/orm/festipodShapes.typings';
import { seedEvents, seedUsers, seedParticipations } from '../data/seedData';
import { ensureGraphNuri } from '../utils/ngGraph';
+6 -1
View File
@@ -12,7 +12,12 @@ import React, { useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { deepSignal } from '@ng-org/alien-deepsignals';
import type { DeepSignalSet } from '@ng-org/alien-deepsignals';
import type { FpEvent, FpUserProfile, FpParticipation } from '../shapes/orm/festipodShapes.typings';
import type {
Event as FpEvent,
MeetingPoint as FpMeetingPoint,
Participation as FpParticipation,
UserProfile as FpUserProfile,
} from '../shapes/orm/festipodShapes.typings';
// ============================================================================
// Seed data — same events/users as the app's seedData.ts
+4 -1
View File
@@ -14,7 +14,10 @@
* module runs outside the component tree.
*/
import type { FpEvent, FpUserProfile } from '../shapes/orm/festipodShapes.typings';
import type {
Event as FpEvent,
UserProfile as FpUserProfile,
} from '../shapes/orm/festipodShapes.typings';
import type { FpEventData, FpUserData } from '../data/types';
import { seedEvents, seedUsers } from '../data/seedData';