Unified Module System - Technical Specification
Technical implementation details for merging Singularity + Modularity into unified module workbench
Unified Module System - Technical Specification
Overview
This specification details how to merge singularity-lib and modularity-lib into a unified Module Workbench that creates real, production-ready modules.
1. Module Context System
1.1 Module Context Service
// libs/libs-structural/unified-module-workbench/src/lib/services/module-context.service.ts
export interface ModuleContext {
// Identity
moduleId: string;
moduleName: string;
moduleDescription: string;
moduleCategory: ModuleCategory;
// Data Layer
entities: EntityDefinition[];
stateDefinitions: StateDefinition[];
// Logic Layer
actions: ActionDefinition[];
activeActionId: string | null;
// UI Layer
embeddables: EmbeddableDefinition[];
routes: RouteDefinition[];
// Configuration
permissions: PermissionDefinition[];
configuration: ConfigurationSchema;
// Metadata
createdAt: string;
updatedAt: string;
status: 'draft' | 'building' | 'ready' | 'deployed';
}
@Injectable({ providedIn: 'root' })
export class ModuleContextService {
private readonly storage = inject(GgStorage);
private readonly logger = inject(LoggerService).child({ source: 'ModuleContext' });
// Reactive state
private readonly _context = signal<ModuleContext | null>(null);
readonly context = this._context.asReadonly();
// Computed signals for quick access
readonly moduleId = computed(() => this._context()?.moduleId ?? null);
readonly entities = computed(() => this._context()?.entities ?? []);
readonly actions = computed(() => this._context()?.actions ?? []);
readonly embeddables = computed(() => this._context()?.embeddables ?? []);
readonly activeAction = computed(() => {
const ctx = this._context();
if (!ctx?.activeActionId) return null;
return ctx.actions.find(a => a.id === ctx.activeActionId) ?? null;
});
// Create new module context
createModule(name: string, category: ModuleCategory): Observable<ModuleContext> {
const moduleId = `${this.toKebabCase(name)}-${Date.now()}`;
const context: ModuleContext = {
moduleId,
moduleName: name,
moduleDescription: '',
moduleCategory: category,
entities: [],
stateDefinitions: [],
actions: [],
activeActionId: null,
embeddables: [],
routes: [],
permissions: [],
configuration: {},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
status: 'draft'
};
return from(this.storage.put(`module-context:${moduleId}`, context)).pipe(
tap(() => this._context.set(context)),
map(() => context)
);
}
// Load existing module context
loadModule(moduleId: string): Observable<ModuleContext | null> {
return from(this.storage.get<ModuleContext>(`module-context:${moduleId}`)).pipe(
tap(ctx => this._context.set(ctx))
);
}
// Update context (auto-saves)
updateContext(updates: Partial<ModuleContext>): Observable<ModuleContext> {
const current = this._context();
if (!current) throw new Error('No active module context');
const updated: ModuleContext = {
...current,
...updates,
updatedAt: new Date().toISOString()
};
return from(this.storage.put(`module-context:${current.moduleId}`, updated)).pipe(
tap(() => this._context.set(updated)),
map(() => updated)
);
}
// Set active action (for Singularity to know what it's editing)
setActiveAction(actionId: string | null): void {
const current = this._context();
if (!current) return;
this._context.set({ ...current, activeActionId: actionId });
}
}
1.2 Entity Definition Model
// libs/libs-structural/unified-module-workbench/src/lib/models/entity-definition.model.ts
export interface EntityFieldDefinition {
name: string;
type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'reference';
label: string;
description?: string;
required: boolean;
unique?: boolean;
default?: unknown;
// For enum type
enumValues?: string[];
// For array type
arrayItemType?: Omit<EntityFieldDefinition, 'name'>;
// For reference type (relations)
referenceEntity?: string;
referenceType?: 'one-to-one' | 'one-to-many' | 'many-to-many';
// Auto-managed fields
autoGenerate?: 'uuid' | 'timestamp' | 'increment';
autoUpdate?: 'timestamp';
// Validation
validation?: {
min?: number;
max?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
custom?: string; // Expression
};
}
export interface EntityDefinition {
id: string;
name: string; // e.g., "User"
pluralName: string; // e.g., "Users"
description?: string;
icon?: string;
fields: EntityFieldDefinition[];
// Generated code preview
generatedInterface?: string;
generatedValidator?: string;
}
// Helper to generate TypeScript interface from entity
export function generateEntityInterface(entity: EntityDefinition): string {
const fieldLines = entity.fields.map(field => {
const optional = field.required ? '' : '?';
const type = mapFieldTypeToTS(field);
return ` ${field.name}${optional}: ${type};`;
}).join('\n');
return `export interface ${entity.name} {\n${fieldLines}\n}`;
}
function mapFieldTypeToTS(field: EntityFieldDefinition): string {
switch (field.type) {
case 'string': return 'string';
case 'number': return 'number';
case 'boolean': return 'boolean';
case 'date': return 'Date';
case 'enum': return field.enumValues?.map(v => `'${v}'`).join(' | ') ?? 'string';
case 'array': return `${mapFieldTypeToTS(field.arrayItemType as EntityFieldDefinition)}[]`;
case 'object': return 'Record<string, unknown>';
case 'reference': return field.referenceEntity ?? 'string';
default: return 'unknown';
}
}
1.3 State Definition Model
// libs/libs-structural/unified-module-workbench/src/lib/models/state-definition.model.ts
export interface StatePropertyDefinition {
name: string;
type: string; // TypeScript type
initialValue: unknown;
description?: string;
// Source
source: 'entity' | 'derived' | 'ui' | 'config';
sourceEntity?: string; // For entity source
derivedFrom?: string; // Expression for derived
}
export interface StateDefinition {
id: string;
name: string; // e.g., "UsersState"
description?: string;
// Associated entity (optional)
entityId?: string;
// Properties
properties: StatePropertyDefinition[];
// Persistence
persistence: {
enabled: boolean;
storageKey: string;
strategy: 'local' | 'backend' | 'hybrid';
offlineSupport: boolean;
syncMode: 'realtime' | 'on-demand' | 'manual';
};
// Generated service preview
generatedService?: string;
}
// Helper to generate state service from definition
export function generateStateService(state: StateDefinition, entity?: EntityDefinition): string {
const entityType = entity?.name ?? 'unknown';
const storageKey = state.persistence.storageKey;
return `
@Injectable({ providedIn: 'root' })
export class ${state.name}Service {
private readonly storage = inject(GgStorage);
// State signals
${state.properties.map(p => {
if (p.source === 'entity' && p.type.endsWith('[]')) {
return `readonly ${p.name} = autoFlashState<${p.type}>('${storageKey}:${p.name}', ${JSON.stringify(p.initialValue)});`;
}
return `readonly ${p.name} = signal<${p.type}>(${JSON.stringify(p.initialValue)});`;
}).join('\n ')}
// Loading states
readonly isLoading = signal(false);
readonly error = signal<string | null>(null);
}
`.trim();
}
2. Integrated Action-Flow System
2.1 Action Definition with Flow Reference
// libs/libs-structural/unified-module-workbench/src/lib/models/action-definition-v2.model.ts
export interface ActionInputDefinition {
name: string;
type: ParameterType;
label: string;
description?: string;
required: boolean;
defaultValue?: unknown;
validation?: ValidationRules;
}
export interface ActionOutputDefinition {
type: string; // TypeScript type or entity reference
description?: string;
isArray?: boolean;
}
export interface ActionDefinitionV2 {
id: string;
name: string;
displayName: string;
description?: string;
category: 'crud' | 'query' | 'mutation' | 'navigation' | 'utility' | 'custom';
icon?: string;
// Contract
inputs: ActionInputDefinition[];
output: ActionOutputDefinition;
// Implementation - ALWAYS a flow
flowId: string; // Every action has a Singularity flow
flowStatus: 'skeleton' | 'in-progress' | 'complete' | 'tested';
// Permissions
permissions: string[];
// Auto-generated
isAutoGenerated: boolean; // True for CRUD actions from entity
sourceEntityId?: string; // For auto-generated actions
// Testing
testCases: ActionTestCase[];
lastTestResult?: TestResult;
}
export interface ActionTestCase {
id: string;
name: string;
input: Record<string, unknown>;
expectedOutput?: unknown;
expectedError?: string;
}
2.2 Action-Flow Bridge Service
// libs/libs-structural/unified-module-workbench/src/lib/services/action-flow-bridge.service.ts
@Injectable({ providedIn: 'root' })
export class ActionFlowBridgeService {
private readonly storage = inject(GgStorage);
private readonly moduleContext = inject(ModuleContextService);
private readonly singularityService = inject(SingularityService);
private readonly logger = inject(LoggerService).child({ source: 'ActionFlowBridge' });
/**
* Create a new action with its flow skeleton
*/
createAction(definition: Omit<ActionDefinitionV2, 'flowId' | 'flowStatus'>): Observable<ActionDefinitionV2> {
const moduleId = this.moduleContext.moduleId();
if (!moduleId) throw new Error('No active module context');
const actionId = `${moduleId}:${this.toKebabCase(definition.name)}`;
const flowId = `flow-${actionId}`;
// Create flow skeleton based on action type
const flowSkeleton = this.createFlowSkeleton(definition, flowId);
return from(this.storage.put(`flow:${flowId}`, flowSkeleton)).pipe(
switchMap(() => {
const action: ActionDefinitionV2 = {
...definition,
id: actionId,
flowId,
flowStatus: 'skeleton'
};
// Add action to module context
return this.moduleContext.updateContext({
actions: [...this.moduleContext.actions(), action]
}).pipe(map(() => action));
})
);
}
/**
* Create flow skeleton based on action type
*/
private createFlowSkeleton(action: Omit<ActionDefinitionV2, 'flowId' | 'flowStatus'>, flowId: string): LogicFlow {
const inputBlock: LogicBlock = {
id: `${flowId}-input`,
type: 'trigger',
name: 'Action Input',
position: { x: 100, y: 200 },
config: {
triggerType: 'action',
actionId: action.id,
inputs: action.inputs.map(i => ({
name: i.name,
type: i.type,
required: i.required
}))
}
};
const outputBlock: LogicBlock = {
id: `${flowId}-output`,
type: 'output',
name: 'Action Output',
position: { x: 700, y: 200 },
config: {
outputType: action.output.type,
description: action.output.description
}
};
// Add skeleton blocks based on category
const skeletonBlocks = this.getCategorySkeletonBlocks(action.category, flowId);
return {
id: flowId,
name: `${action.displayName} Flow`,
blocks: [inputBlock, ...skeletonBlocks, outputBlock],
connections: this.createSkeletonConnections(inputBlock, skeletonBlocks, outputBlock),
metadata: {
created: new Date(),
modified: new Date(),
author: 'Module Builder',
description: `Implementation flow for ${action.displayName}`,
category: 'module-action',
status: 'skeleton',
moduleId: this.moduleContext.moduleId()!,
actionId: action.id
}
};
}
/**
* Get skeleton blocks based on action category
*/
private getCategorySkeletonBlocks(category: ActionDefinitionV2['category'], flowId: string): LogicBlock[] {
switch (category) {
case 'crud':
return [
{
id: `${flowId}-validate`,
type: 'function',
name: '✓ Validate Input',
position: { x: 250, y: 200 },
config: { placeholder: true, description: 'Add validation logic here' }
},
{
id: `${flowId}-db`,
type: 'action',
name: '💾 Database Operation',
position: { x: 400, y: 200 },
config: { placeholder: true, description: 'Add database/API call here' }
},
{
id: `${flowId}-transform`,
type: 'function',
name: '🔄 Transform Response',
position: { x: 550, y: 200 },
config: { placeholder: true, description: 'Transform data before output' }
}
];
case 'query':
return [
{
id: `${flowId}-fetch`,
type: 'action',
name: '📥 Fetch Data',
position: { x: 300, y: 200 },
config: { placeholder: true, description: 'Add data fetch logic' }
},
{
id: `${flowId}-filter`,
type: 'function',
name: '🔍 Filter/Transform',
position: { x: 500, y: 200 },
config: { placeholder: true, description: 'Apply filters and transforms' }
}
];
case 'navigation':
return [
{
id: `${flowId}-resolve`,
type: 'function',
name: '🔗 Resolve Route',
position: { x: 300, y: 200 },
config: { placeholder: true, description: 'Build navigation route' }
},
{
id: `${flowId}-navigate`,
type: 'action',
name: '🚀 Navigate',
position: { x: 500, y: 200 },
config: { placeholder: true, description: 'Perform navigation' }
}
];
default:
return [
{
id: `${flowId}-logic`,
type: 'function',
name: '⚙️ Custom Logic',
position: { x: 400, y: 200 },
config: { placeholder: true, description: 'Add your custom logic here' }
}
];
}
}
/**
* Open action flow in Singularity editor (embedded or full-screen)
*/
editActionFlow(actionId: string, mode: 'embedded' | 'fullscreen' = 'embedded'): Observable<void> {
const action = this.moduleContext.actions().find(a => a.id === actionId);
if (!action) throw new Error(`Action ${actionId} not found`);
// Set active action so Singularity knows the context
this.moduleContext.setActiveAction(actionId);
// Load flow into Singularity
return from(this.singularityService.loadFlow(action.flowId)).pipe(
tap(() => {
if (mode === 'fullscreen') {
// Navigate to Singularity with module context
// Singularity will show module-aware UI
}
})
);
}
/**
* Auto-generate CRUD actions from entity
*/
generateCrudActions(entityId: string): Observable<ActionDefinitionV2[]> {
const entity = this.moduleContext.entities().find(e => e.id === entityId);
if (!entity) throw new Error(`Entity ${entityId} not found`);
const crudActions: Omit<ActionDefinitionV2, 'flowId' | 'flowStatus'>[] = [
{
id: '', // Will be generated
name: `get${entity.pluralName}`,
displayName: `Get ${entity.pluralName}`,
description: `Retrieve list of ${entity.pluralName.toLowerCase()} with optional filtering`,
category: 'query',
icon: 'list',
inputs: [
{ name: 'limit', type: 'number', label: 'Limit', required: false, defaultValue: 20 },
{ name: 'offset', type: 'number', label: 'Offset', required: false, defaultValue: 0 },
{ name: 'filter', type: 'object', label: 'Filter', required: false }
],
output: { type: `${entity.name}[]`, isArray: true },
permissions: [`${entity.name.toLowerCase()}:read`],
isAutoGenerated: true,
sourceEntityId: entityId,
testCases: []
},
{
id: '',
name: `get${entity.name}ById`,
displayName: `Get ${entity.name} by ID`,
description: `Retrieve a single ${entity.name.toLowerCase()} by ID`,
category: 'query',
icon: 'search',
inputs: [
{ name: 'id', type: 'string', label: `${entity.name} ID`, required: true }
],
output: { type: entity.name },
permissions: [`${entity.name.toLowerCase()}:read`],
isAutoGenerated: true,
sourceEntityId: entityId,
testCases: []
},
{
id: '',
name: `create${entity.name}`,
displayName: `Create ${entity.name}`,
description: `Create a new ${entity.name.toLowerCase()}`,
category: 'crud',
icon: 'plus',
inputs: entity.fields
.filter(f => !f.autoGenerate) // Exclude auto-generated fields
.map(f => ({
name: f.name,
type: mapFieldTypeToParameterType(f.type),
label: f.label,
required: f.required,
defaultValue: f.default
})),
output: { type: entity.name },
permissions: [`${entity.name.toLowerCase()}:create`],
isAutoGenerated: true,
sourceEntityId: entityId,
testCases: []
},
{
id: '',
name: `update${entity.name}`,
displayName: `Update ${entity.name}`,
description: `Update an existing ${entity.name.toLowerCase()}`,
category: 'crud',
icon: 'edit',
inputs: [
{ name: 'id', type: 'string', label: `${entity.name} ID`, required: true },
...entity.fields
.filter(f => !f.autoGenerate && !f.autoUpdate)
.map(f => ({
name: f.name,
type: mapFieldTypeToParameterType(f.type),
label: f.label,
required: false // All optional for update
}))
],
output: { type: entity.name },
permissions: [`${entity.name.toLowerCase()}:update`],
isAutoGenerated: true,
sourceEntityId: entityId,
testCases: []
},
{
id: '',
name: `delete${entity.name}`,
displayName: `Delete ${entity.name}`,
description: `Delete a ${entity.name.toLowerCase()}`,
category: 'crud',
icon: 'trash',
inputs: [
{ name: 'id', type: 'string', label: `${entity.name} ID`, required: true }
],
output: { type: 'boolean', description: 'Success indicator' },
permissions: [`${entity.name.toLowerCase()}:delete`],
isAutoGenerated: true,
sourceEntityId: entityId,
testCases: []
}
];
// Create all actions
return forkJoin(crudActions.map(action => this.createAction(action)));
}
}
3. Embeddable Widget System
3.1 Enhanced Embeddable Definition
// libs/libs-structural/unified-module-workbench/src/lib/models/embeddable-v2.model.ts
export interface EmbeddableInputBinding {
source: 'manual' | 'route-param' | 'module-state' | 'config' | 'parent-output';
// For manual: the static value
manualValue?: unknown;
// For route-param: the param name
routeParam?: string;
// For module-state: path to state property
statePath?: string; // e.g., "usersState.currentUser.id"
// For config: config key
configKey?: string;
// For parent-output: connected widget output
parentOutputRef?: { widgetId: string; outputName: string };
}
export interface EmbeddableOutputBinding {
actionId: string; // Module action to trigger
parameterMapping: Record<string, string>; // Maps output data to action params
}
export interface EmbeddableDefinitionV2 {
id: string;
name: string;
displayName: string;
description?: string;
icon?: string;
category: 'widget' | 'form' | 'list' | 'card' | 'dashboard' | 'navigation' | 'custom';
// Inputs with binding configuration
inputs: Array<{
definition: ActionInputDefinition;
binding: EmbeddableInputBinding;
}>;
// Outputs with action bindings
outputs: Array<{
name: string;
label: string;
description?: string;
dataType: string;
binding: EmbeddableOutputBinding;
}>;
// Internal data bindings (from module state)
dataBindings: Array<{
propertyName: string;
statePath: string;
description?: string;
}>;
// Template configuration
template: {
source: 'visual-builder' | 'page-reference' | 'custom-html';
// For visual-builder
visualConfig?: VisualTemplateConfig;
// For page-reference
pageId?: string;
// For custom-html
customTemplate?: string;
};
// Permissions required to use this embeddable
permissions: string[];
// Preview configuration
preview: {
mockData: Record<string, unknown>;
defaultSize: { width: number; height: number };
};
}
// Visual template configuration (for simple embeddables)
export interface VisualTemplateConfig {
layout: 'vertical' | 'horizontal' | 'grid' | 'card';
elements: VisualElement[];
styling: {
padding: string;
gap: string;
backgroundColor?: string;
borderRadius?: string;
};
}
export interface VisualElement {
type: 'text' | 'image' | 'button' | 'input' | 'list' | 'condition' | 'slot';
id: string;
position: number;
// Type-specific config
config: Record<string, unknown>;
// Binding: where does this element get its data?
dataBinding?: string; // Path to data property
// Event: what action does this trigger?
onClick?: string; // Output name
onChange?: string; // Output name
}
3.2 Embeddable Builder Service
// libs/libs-structural/unified-module-workbench/src/lib/services/embeddable-builder.service.ts
@Injectable({ providedIn: 'root' })
export class EmbeddableBuilderService {
private readonly moduleContext = inject(ModuleContextService);
private readonly logger = inject(LoggerService).child({ source: 'EmbeddableBuilder' });
/**
* Create embeddable from entity (auto-generates list and card widgets)
*/
generateEntityEmbeddables(entityId: string): Observable<EmbeddableDefinitionV2[]> {
const entity = this.moduleContext.entities().find(e => e.id === entityId);
if (!entity) throw new Error(`Entity ${entityId} not found`);
const actions = this.moduleContext.actions();
const embeddables: EmbeddableDefinitionV2[] = [
// List widget
{
id: `${entity.name.toLowerCase()}-list-widget`,
name: `${entity.name}ListWidget`,
displayName: `${entity.name} List`,
description: `Displays a list of ${entity.pluralName.toLowerCase()}`,
icon: 'list',
category: 'list',
inputs: [
{
definition: { name: 'limit', type: 'number', label: 'Items per page', required: false, defaultValue: 10 },
binding: { source: 'manual', manualValue: 10 }
},
{
definition: { name: 'showSearch', type: 'boolean', label: 'Show Search', required: false, defaultValue: true },
binding: { source: 'manual', manualValue: true }
}
],
outputs: [
{
name: 'onItemClick',
label: 'Item Click',
description: `Triggered when a ${entity.name.toLowerCase()} is clicked`,
dataType: 'string', // Entity ID
binding: {
actionId: actions.find(a => a.name === `get${entity.name}ById`)?.id ?? '',
parameterMapping: { 'id': '$event' }
}
},
{
name: 'onDelete',
label: 'Delete',
description: `Triggered when delete is clicked`,
dataType: 'string',
binding: {
actionId: actions.find(a => a.name === `delete${entity.name}`)?.id ?? '',
parameterMapping: { 'id': '$event' }
}
}
],
dataBindings: [
{
propertyName: 'items',
statePath: `${entity.pluralName.toLowerCase()}State.items`,
description: `List of ${entity.pluralName.toLowerCase()}`
},
{
propertyName: 'isLoading',
statePath: `${entity.pluralName.toLowerCase()}State.isLoading`,
description: 'Loading state'
}
],
template: {
source: 'visual-builder',
visualConfig: this.generateListTemplate(entity)
},
permissions: [`${entity.name.toLowerCase()}:read`],
preview: {
mockData: this.generateMockData(entity, 5),
defaultSize: { width: 400, height: 300 }
}
},
// Card widget
{
id: `${entity.name.toLowerCase()}-card-widget`,
name: `${entity.name}CardWidget`,
displayName: `${entity.name} Card`,
description: `Displays a single ${entity.name.toLowerCase()} as a card`,
icon: 'card',
category: 'card',
inputs: [
{
definition: { name: 'entityId', type: 'string', label: `${entity.name} ID`, required: true },
binding: { source: 'route-param', routeParam: 'id' }
},
{
definition: { name: 'showActions', type: 'boolean', label: 'Show Actions', required: false, defaultValue: true },
binding: { source: 'manual', manualValue: true }
}
],
outputs: [
{
name: 'onEdit',
label: 'Edit',
description: `Navigate to edit ${entity.name.toLowerCase()}`,
dataType: 'string',
binding: {
actionId: '', // Navigate action
parameterMapping: { 'id': '$entity.id' }
}
},
{
name: 'onDelete',
label: 'Delete',
description: `Delete this ${entity.name.toLowerCase()}`,
dataType: 'string',
binding: {
actionId: actions.find(a => a.name === `delete${entity.name}`)?.id ?? '',
parameterMapping: { 'id': '$entity.id' }
}
}
],
dataBindings: [
{
propertyName: 'entity',
statePath: `${entity.pluralName.toLowerCase()}State.current`,
description: `Current ${entity.name.toLowerCase()}`
}
],
template: {
source: 'visual-builder',
visualConfig: this.generateCardTemplate(entity)
},
permissions: [`${entity.name.toLowerCase()}:read`],
preview: {
mockData: this.generateMockData(entity, 1)[0],
defaultSize: { width: 350, height: 200 }
}
}
];
return of(embeddables);
}
private generateListTemplate(entity: EntityDefinition): VisualTemplateConfig {
// Auto-generate visual template based on entity fields
const displayFields = entity.fields.slice(0, 4); // First 4 fields
return {
layout: 'vertical',
elements: [
{
type: 'input',
id: 'search',
position: 0,
config: { placeholder: 'Search...', type: 'search' },
onChange: 'onSearch'
},
{
type: 'list',
id: 'items-list',
position: 1,
config: {
itemTemplate: displayFields.map(f => ({
field: f.name,
label: f.label
}))
},
dataBinding: 'items',
onClick: 'onItemClick'
}
],
styling: {
padding: '16px',
gap: '12px'
}
};
}
private generateCardTemplate(entity: EntityDefinition): VisualTemplateConfig {
return {
layout: 'card',
elements: entity.fields.map((f, i) => ({
type: 'text',
id: f.name,
position: i,
config: { label: f.label },
dataBinding: `entity.${f.name}`
})),
styling: {
padding: '20px',
gap: '8px',
borderRadius: '12px'
}
};
}
private generateMockData(entity: EntityDefinition, count: number): Record<string, unknown>[] {
// Generate realistic mock data based on field types
return Array.from({ length: count }, (_, i) => {
const mock: Record<string, unknown> = {};
entity.fields.forEach(f => {
switch (f.type) {
case 'string':
mock[f.name] = f.name === 'name' ? `Sample ${entity.name} ${i + 1}` :
f.name === 'email' ? `user${i + 1}@example.com` :
`${f.name}-value-${i + 1}`;
break;
case 'number':
mock[f.name] = i + 1;
break;
case 'boolean':
mock[f.name] = i % 2 === 0;
break;
case 'date':
mock[f.name] = new Date().toISOString();
break;
case 'enum':
mock[f.name] = f.enumValues?.[i % (f.enumValues?.length ?? 1)] ?? 'default';
break;
default:
mock[f.name] = null;
}
});
mock['id'] = `mock-${i + 1}`;
return mock;
});
}
}
4. Unified Module Workbench Component
4.1 Main Workbench Component
// libs/libs-structural/unified-module-workbench/src/lib/components/module-workbench/module-workbench.component.ts
type WorkbenchStep = 'models' | 'state' | 'actions' | 'embeddables' | 'routes' | 'test' | 'deploy';
@Component({
selector: 'lib-module-workbench',
standalone: true,
imports: [
CommonModule,
FormsModule,
EntityBuilderComponent,
StateBuilderComponent,
ActionBuilderV2Component,
EmbeddableBuilderV2Component,
RouteBuilderComponent,
ModuleTestingComponent,
EmbeddedSingularityComponent,
ModulePreviewComponent
],
templateUrl: './module-workbench.component.html',
styleUrls: ['./module-workbench.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModuleWorkbenchComponent implements OnInit, OnDestroy {
private readonly moduleContext = inject(ModuleContextService);
private readonly actionFlowBridge = inject(ActionFlowBridgeService);
private readonly embeddableBuilder = inject(EmbeddableBuilderService);
private readonly router = inject(Router);
private readonly destroyRef = inject(DestroyRef);
// Step management
readonly steps: WorkbenchStep[] = ['models', 'state', 'actions', 'embeddables', 'routes', 'test', 'deploy'];
readonly currentStep = signal<WorkbenchStep>('models');
readonly completedSteps = signal<Set<WorkbenchStep>>(new Set());
// Context signals
readonly context = this.moduleContext.context;
readonly moduleId = this.moduleContext.moduleId;
readonly entities = this.moduleContext.entities;
readonly actions = this.moduleContext.actions;
readonly embeddables = this.moduleContext.embeddables;
// UI state
readonly showSingularity = signal(false);
readonly singularityMode = signal<'embedded' | 'fullscreen'>('embedded');
readonly activeActionForFlow = signal<string | null>(null);
// Step completion computed
readonly stepStatus = computed(() => {
const ctx = this.context();
if (!ctx) return {} as Record<WorkbenchStep, 'empty' | 'partial' | 'complete'>;
return {
models: ctx.entities.length > 0 ? 'complete' : 'empty',
state: ctx.stateDefinitions.length > 0 ? 'complete' : 'empty',
actions: this.getActionsStatus(ctx.actions),
embeddables: ctx.embeddables.length > 0 ? 'complete' : 'empty',
routes: ctx.routes.length > 0 ? 'complete' : 'empty',
test: 'empty', // Based on test results
deploy: 'empty'
};
});
ngOnInit(): void {
// Check for existing module or create new
const moduleId = this.router.snapshot?.params['moduleId'];
if (moduleId) {
this.moduleContext.loadModule(moduleId).subscribe();
}
}
// Step navigation
goToStep(step: WorkbenchStep): void {
this.currentStep.set(step);
this.closeSingularity();
}
nextStep(): void {
const currentIndex = this.steps.indexOf(this.currentStep());
if (currentIndex < this.steps.length - 1) {
this.currentStep.set(this.steps[currentIndex + 1]);
}
}
// Entity operations
async addEntity(entity: EntityDefinition): Promise<void> {
await firstValueFrom(this.moduleContext.updateContext({
entities: [...this.entities(), entity]
}));
// Auto-generate CRUD actions
await firstValueFrom(this.actionFlowBridge.generateCrudActions(entity.id));
// Auto-generate embeddables
const embeddables = await firstValueFrom(this.embeddableBuilder.generateEntityEmbeddables(entity.id));
await firstValueFrom(this.moduleContext.updateContext({
embeddables: [...this.embeddables(), ...embeddables]
}));
}
// Action operations
async editActionFlow(actionId: string): Promise<void> {
this.activeActionForFlow.set(actionId);
this.showSingularity.set(true);
await firstValueFrom(this.actionFlowBridge.editActionFlow(actionId, this.singularityMode()));
}
closeSingularity(): void {
this.showSingularity.set(false);
this.activeActionForFlow.set(null);
this.moduleContext.setActiveAction(null);
}
// Test action
async testAction(actionId: string): Promise<void> {
// Open action tester modal
}
private getActionsStatus(actions: ActionDefinitionV2[]): 'empty' | 'partial' | 'complete' {
if (actions.length === 0) return 'empty';
const completedCount = actions.filter(a => a.flowStatus === 'complete' || a.flowStatus === 'tested').length;
if (completedCount === actions.length) return 'complete';
return 'partial';
}
}
5. Key Integration Points
5.1 Singularity Context Awareness
// Modify singularity-lib to be context-aware
// In SingularityService:
readonly moduleContext = inject(ModuleContextService, { optional: true });
// When operating in module context:
readonly isModuleMode = computed(() => !!this.moduleContext?.moduleId());
// Get available data sources in module mode:
readonly availableDataSources = computed(() => {
if (!this.isModuleMode()) return [];
const ctx = this.moduleContext!.context();
if (!ctx) return [];
return [
// Module state
...ctx.stateDefinitions.flatMap(s =>
s.properties.map(p => ({
name: `${s.name}.${p.name}`,
type: p.type,
source: 'module-state'
}))
),
// Action inputs (when editing specific action)
...(this.moduleContext!.activeAction()?.inputs ?? []).map(i => ({
name: `input.${i.name}`,
type: i.type,
source: 'action-input'
})),
// Other module actions (can call them)
...ctx.actions.map(a => ({
name: `action.${a.name}`,
type: 'action',
source: 'module-action'
}))
];
});
5.2 Widget Registry Integration
// When module embeddables are defined, register with widget system
@Injectable({ providedIn: 'root' })
export class ModuleEmbeddableSync {
private readonly moduleContext = inject(ModuleContextService);
private readonly widgetRegistry = inject(WidgetRegistryService);
constructor() {
// Auto-sync embeddables to widget registry
effect(() => {
const embeddables = this.moduleContext.embeddables();
for (const emb of embeddables) {
const widget = this.convertToWidget(emb);
this.widgetRegistry.registerWidget(widget);
}
});
}
private convertToWidget(emb: EmbeddableDefinitionV2): GenericWidgetDefinition {
return {
id: emb.id,
name: emb.displayName,
type: 'module-embeddable',
category: emb.category,
description: emb.description,
icon: emb.icon,
properties: emb.inputs.map(i => ({
name: i.definition.name,
type: i.definition.type,
label: i.definition.label,
required: i.definition.required,
defaultValue: i.definition.defaultValue
})),
events: emb.outputs.map(o => ({
name: o.name,
label: o.label,
description: o.description
})),
source: 'module',
moduleId: this.moduleContext.moduleId()!,
// Render using EmbeddableHostComponent
componentRef: EmbeddableHostComponent
};
}
}
6. Migration Path
Phase 1: Foundation (Week 1-2)
- Create
unified-module-workbenchlibrary - Implement
ModuleContextService - Implement entity and state builders
- Create basic workbench shell
Phase 2: Action-Flow Integration (Week 3-4)
- Implement
ActionFlowBridgeService - Modify Singularity for context awareness
- Implement embedded Singularity view
- Auto-generate flow skeletons
Phase 3: Embeddable System (Week 5-6)
- Implement enhanced
EmbeddableBuilderService - Create visual template builder
- Implement input/output binding UI
- Sync to widget registry
Phase 4: Testing & Polish (Week 7-8)
- Module testing environment
- Live preview
- Export/import
- Documentation
Success Metrics
| Metric | Target |
|---|---|
| Can create CRUD module in 10 minutes | ✓ |
| Actions have real implementation (flows) | ✓ |
| Embeddables have working inputs/outputs | ✓ |
| Can replicate user-management module | ✓ |
| Zero disconnected flows | ✓ |
| Zero empty wrappers | ✓ |
Document: Technical Specification
Status: Ready for Implementation
Session: 12-02-1-2