Compare commits

...

2 Commits

Author SHA1 Message Date
Sosthene
c6f42e893b Create folder with create/notify/validate flow 2025-06-12 17:25:36 +02:00
Sosthene
02a490d3e3 Define models for roles and folder data 2025-06-12 17:24:54 +02:00
4 changed files with 260 additions and 14 deletions

View File

@ -12,7 +12,7 @@ import Iframe from './sdk/Iframe'
import BlockchainViewer from './components/ProcessesViewer'; import BlockchainViewer from './components/ProcessesViewer';
import FolderModal from './components/FolderModal'; import FolderModal from './components/FolderModal';
import type { ProfileCreated, ProfileData } from './sdk/models/ProfileData' import type { ProfileCreated, ProfileData } from './sdk/models/ProfileData'
import type { FolderCreated, FolderData } from './sdk/models/FolderData' import { FolderPrivateFields, setDefaultFolderRoles, type FolderCreated, type FolderData } from './sdk/models/FolderData'
const iframeUrl = 'https://dev3.4nkweb.com' const iframeUrl = 'https://dev3.4nkweb.com'
@ -126,14 +126,22 @@ function App() {
// Gestionnaire pour soumettre les données du dossier // Gestionnaire pour soumettre les données du dossier
const handleFolderSubmit = useCallback((folderData: FolderData) => { const handleFolderSubmit = useCallback((folderData: FolderData) => {
MessageBus.getInstance(iframeUrl).createFolder(folderData).then((_folderCreated: FolderCreated) => { if (userPairingId !== null) {
MessageBus.getInstance(iframeUrl).getProcesses().then((processes: any) => { const roles = setDefaultFolderRoles(userPairingId, [], []);
setProcesses(processes); const folderPrivateFields = FolderPrivateFields;
MessageBus.getInstance(iframeUrl).createFolder(folderData, folderPrivateFields, roles).then((_folderCreated: FolderCreated) => {
MessageBus.getInstance(iframeUrl).notifyProcessUpdate(_folderCreated.processId, _folderCreated.process.states[0].state_id).then(() => {
MessageBus.getInstance(iframeUrl).validateState(_folderCreated.processId, _folderCreated.process.states[0].state_id).then((_updatedProcess: any) => {
MessageBus.getInstance(iframeUrl).getProcesses().then((processes: any) => {
setProcesses(processes);
});
});
})
}); });
});
setShowFolderModal(false); setShowFolderModal(false);
}, []); }
}, [userPairingId]);
// Gestionnaire du clic sur le bouton Vider les messages // Gestionnaire du clic sur le bouton Vider les messages
const handleClearMessages = useCallback(() => { const handleClearMessages = useCallback(() => {

View File

@ -2,8 +2,9 @@ import IframeReference from './IframeReference';
import EventBus from './EventBus'; import EventBus from './EventBus';
import UserStore from './UserStrore'; import UserStore from './UserStrore';
import type { ProfileCreated, ProfileData } from './models/ProfileData'; import type { ProfileCreated, ProfileData } from './models/ProfileData';
import type { FolderCreated, FolderData } from './models/FolderData'; import { isFolderData, type FolderCreated, type FolderData } from './models/FolderData';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { RoleDefinition } from './models/Roles';
export default class MessageBus { export default class MessageBus {
private static instance: MessageBus; private static instance: MessageBus;
@ -336,7 +337,7 @@ export default class MessageBus {
}); });
} }
public createFolder(folderData: FolderData): Promise<FolderCreated> { public createFolder(folderData: FolderData, folderPrivateData: string[], roles: Record<string, RoleDefinition>): Promise<FolderCreated> {
return new Promise<FolderCreated>((resolve: (folderData: FolderCreated) => void, reject: (error: string) => void) => { return new Promise<FolderCreated>((resolve: (folderData: FolderCreated) => void, reject: (error: string) => void) => {
this.checkToken().then(() => { this.checkToken().then(() => {
const userStore = UserStore.getInstance(); const userStore = UserStore.getInstance();
@ -345,16 +346,29 @@ export default class MessageBus {
const correlationId = uuidv4(); const correlationId = uuidv4();
this.initMessageListener(correlationId); this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('FOLDER_CREATED', (responseId: string, folderCreated: FolderCreated) => { const unsubscribe = EventBus.getInstance().on('PROCESS_CREATED', (responseId: string, processCreated: any) => {
console.log(processCreated);
if (responseId !== correlationId) { if (responseId !== correlationId) {
return; return;
} }
unsubscribe(); unsubscribe();
this.destroyMessageListener(); this.destroyMessageListener();
// Return value must contain the data commited in the new process
const folderData = processCreated.folderCreated;
if (!folderData || !isFolderData(folderData)) reject('Returned invalid process data');
if (!processCreated.processId || typeof processCreated.processId !== 'string') reject('Returned invalid process id');
// TODO check that process is of type Process
const folderCreated: FolderCreated = {
processId: processCreated.processId,
process: processCreated.process,
folderData
};
resolve(folderCreated); resolve(folderCreated);
}); });
const unsubscribeError = EventBus.getInstance().on('ERROR_FOLDER_CREATED', (responseId: string, error: string) => { const unsubscribeError = EventBus.getInstance().on('ERROR_PROCESS_CREATED', (responseId: string, error: string) => {
if (responseId !== correlationId) { if (responseId !== correlationId) {
return; return;
} }
@ -364,9 +378,86 @@ export default class MessageBus {
}); });
this.sendMessage({ this.sendMessage({
type: 'CREATE_FOLDER', type: 'CREATE_PROCESS',
folderData, processData: folderData,
token: accessToken privateFields: folderPrivateData,
roles,
accessToken
});
}).catch(console.error);
});
}
public notifyProcessUpdate(processId: string, stateId: string): Promise<void> {
return new Promise<void>((resolve: () => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const userStore = UserStore.getInstance();
const accessToken = userStore.getAccessToken()!;
const correlationId = uuidv4();
this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('UPDATE_NOTIFIED', (responseId: string) => {
if (responseId !== correlationId) {
return;
}
unsubscribe();
this.destroyMessageListener();
resolve();
});
const unsubscribeError = EventBus.getInstance().on('ERROR_UPDATE_NOTIFIED', (responseId: string, error: string) => {
if (responseId !== correlationId) {
return;
}
unsubscribeError();
this.destroyMessageListener();
reject(error);
});
this.sendMessage({
type: 'NOTIFY_UPDATE',
processId,
stateId,
accessToken
});
}).catch(console.error);
});
}
public validateState(processId: string, stateId: string): Promise<any> {
return new Promise<any>((resolve: (updatedProcess: any) => void, reject: (error: string) => void) => {
this.checkToken().then(() => {
const userStore = UserStore.getInstance();
const accessToken = userStore.getAccessToken()!;
const correlationId = uuidv4();
this.initMessageListener(correlationId);
const unsubscribe = EventBus.getInstance().on('STATE_VALIDATED', (responseId: string, updatedProcess: any) => {
console.log(updatedProcess);
if (responseId !== correlationId) {
return;
}
unsubscribe();
this.destroyMessageListener();
resolve(updatedProcess);
});
const unsubscribeError = EventBus.getInstance().on('ERROR_STATE_VALIDATED', (responseId: string, error: string) => {
if (responseId !== correlationId) {
return;
}
unsubscribeError();
this.destroyMessageListener();
reject(error);
});
this.sendMessage({
type: 'VALIDATE_STATE',
processId,
stateId,
accessToken
}); });
}).catch(console.error); }).catch(console.error);
}); });
@ -520,6 +611,39 @@ export default class MessageBus {
EventBus.getInstance().emit('DATA_RETRIEVED', correlationId, message.data); EventBus.getInstance().emit('DATA_RETRIEVED', correlationId, message.data);
break; break;
case 'PROCESS_CREATED':
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
delete this.errors[correlationId];
EventBus.getInstance().emit('ERROR_PROCESS_CREATED', correlationId, error);
return;
}
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit('PROCESS_CREATED', correlationId, message.processCreated);
break;
case 'UPDATE_NOTIFIED':
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
delete this.errors[correlationId];
EventBus.getInstance().emit('ERROR_UPDATE_NOTIFIED', correlationId, error);
return;
}
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit('UPDATE_NOTIFIED', correlationId);
break;
case 'STATE_VALIDATED':
if (this.errors[correlationId]) {
const error = this.errors[correlationId];
delete this.errors[correlationId];
EventBus.getInstance().emit('ERROR_STATE_VALIDATED', correlationId, error);
return;
}
EventBus.getInstance().emit('MESSAGE_RECEIVED', message);
EventBus.getInstance().emit('STATE_VALIDATED', correlationId, message.validatedProcess);
break;
case 'ERROR': case 'ERROR':
console.error('Error:', message); console.error('Error:', message);
this.errors[correlationId] = message.error; this.errors[correlationId] = message.error;

View File

@ -1,3 +1,5 @@
import type { RoleDefinition } from "./Roles";
export interface FolderData { export interface FolderData {
folderNumber: string; folderNumber: string;
name: string; name: string;
@ -13,8 +15,109 @@ export interface FolderData {
stakeholders: string[]; stakeholders: string[];
} }
export function isFolderData(data: any): data is FolderData {
if (typeof data !== 'object' || data === null) return false;
const requiredStringFields = [
'folderNumber',
'name',
'deedType',
'description',
'archived_description',
'status',
'created_at',
'updated_at'
];
for (const field of requiredStringFields) {
if (typeof data[field] !== 'string') return false;
}
const requiredArrayFields = [
'customers',
'documents',
'motes',
'stakeholders'
];
for (const field of requiredArrayFields) {
if (!Array.isArray(data[field]) || !data[field].every((item: any) => typeof item === 'string')) {
return false;
}
}
return true;
}
const emptyFolderData: FolderData = {
folderNumber: '',
name: '',
deedType: '',
description: '',
archived_description: '',
status: '',
created_at: '',
updated_at: '',
customers: [],
documents: [],
motes: [],
stakeholders: []
};
const folderDataFields: string[] = Object.keys(emptyFolderData);
const FolderPublicFields: string[] = [];
// All the attributes are private in that case
export const FolderPrivateFields = [
...folderDataFields.filter(key => !FolderPublicFields.includes(key))
];
export interface FolderCreated { export interface FolderCreated {
processId: string, processId: string,
process: any, // Process process: any, // Process
folderData: FolderData, folderData: FolderData,
} }
export function setDefaultFolderRoles(ownerId: string, stakeholdersId: string[], customersId: string[]): Record<string, RoleDefinition> {
return {
owner: {
members: [ownerId],
validation_rules: [
{
quorum: 0.5,
fields: [...folderDataFields, 'roles'],
min_sig_member: 1,
},
],
storages: []
},
stakeholders: {
members: stakeholdersId,
validation_rules: [
{
quorum: 0.5,
fields: ['documents', 'motes'],
min_sig_member: 1,
},
],
storages: []
},
customers: {
members: customersId,
validation_rules: [
{
quorum: 0.0,
fields: folderDataFields,
min_sig_member: 0.0,
},
],
storages: []
},
apophis: {
members: [ownerId],
validation_rules: [],
storages: []
}
}
};

11
src/sdk/models/Roles.ts Normal file
View File

@ -0,0 +1,11 @@
export interface ValidationRule {
quorum: number,
fields: string[],
min_sig_member: number,
}
export interface RoleDefinition {
members: string[],
validation_rules: ValidationRule[],
storages: string[]
}