// ─────────────────────────────────────────────────────────────────────────────
// VICE · Editor Store
//
// Store principal del editor de catálogos.
// Gestiona el estado de edición de una Collection completa.
// Usa runas de Svelte 5 ($state, $derived).
// ─────────────────────────────────────────────────────────────────────────────
import type {
Collection , CollectionID ,
Entity , EntityID ,
Group , GroupID ,
Attribute , AttrID ,
AttributeOption ,
OptionDefinition , OptionID ,
Rule ,
EntityPath ,
EntityPathKey ,
LiteralValue ,
} from '@/vice/libs/types'
import type { FixedGroup , OptionalGroup } from "@/vice/libs" ;
import type { LingString } from "@/ling/lib" ;
// ─────────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────────
function uid ( prefix : string ) : string {
return ` ${ prefix } _ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } `
}
function emptyLing ( es = '' , en = '' ) : LingString {
return { es , en }
}
function emptyCollection ( ) : Collection {
return {
id : uid ( 'col' ) as CollectionID ,
code : '' ,
name : emptyLing ( ) ,
description : emptyLing ( ) ,
langs : [ 'es' ] ,
attributes : [ ] ,
options : { } as Record < OptionID , OptionDefinition > ,
entities : { } as Record < EntityID , Entity > ,
}
}
function emptyEntity ( id : EntityID ) : Entity {
return {
id ,
code : '' ,
name : emptyLing ( ) ,
description : emptyLing ( ) ,
attributes : [ ] ,
groups : { } as Record < GroupID , Group > ,
rules : [ ] ,
}
}
function emptyGroup ( id : GroupID , kind : 'fixed' | 'optional' = 'fixed' ) : Group {
const base = {
id ,
code : '' ,
name : emptyLing ( ) ,
description : emptyLing ( ) ,
attributes : [ ] ,
}
if ( kind === 'optional' ) {
return { . . . base , kind : 'optional' , rules : [ ] } satisfies OptionalGroup
}
return { . . . base , kind : 'fixed' } satisfies FixedGroup
}
function emptyAttribute ( id : AttrID ) : import ( '@/vice/libs/types.ts' ) . RequiredAttribute {
return {
id ,
code : '' ,
name : emptyLing ( ) ,
description : emptyLing ( ) ,
type : 'required' ,
domain : { kind : 'options' , options : [ ] , default : '' as OptionID } ,
rules : [ ] ,
display : { } ,
}
}
function emptyFixedAttribute ( id : AttrID ) : import ( '@/vice/libs/types.ts' ) . FixedAttribute {
return {
id ,
code : '' ,
name : emptyLing ( ) ,
description : emptyLing ( ) ,
type : 'fixed' ,
domain : { kind : 'fixed' , value : { kind : 'string' , value : '' } } ,
display : { } ,
}
}
function emptyOption ( id : OptionID ) : OptionDefinition {
return {
id ,
code : '' ,
name : emptyLing ( ) ,
description : emptyLing ( ) ,
tags : [ ] ,
}
}
// ─────────────────────────────────────────────────────────────────────────────
// VALIDATION
// ─────────────────────────────────────────────────────────────────────────────
export type EditorPanel = 'catalog' | 'objects' | 'options' | 'rules' | 'preview' | 'export'
export interface EditorIssue {
severity : 'error' | 'warning'
message : string
path : string
}
export interface EditorValidation {
valid : boolean
errors : EditorIssue [ ]
warnings : EditorIssue [ ]
}
function validateCollection ( collection : Collection ) : EditorValidation {
const errors : EditorIssue [ ] = [ ]
const warnings : EditorIssue [ ] = [ ]
// Catálogo
if ( ! collection . code ) errors . push ( { severity : 'error' , message : 'El catálogo necesita un código' , path : 'collection.code' } )
// Validar nombre en todos los langs declarados
const name = collection . name as any
for ( const lang of collection . langs ) {
if ( ! name ? . [ lang ] ) {
errors . push ( { severity : 'error' , message : ` El catálogo necesita nombre en " ${ lang } " ` , path : ` collection.name. ${ lang } ` } )
}
}
// Opciones globales
for ( const opt of Object . values ( collection . options ) ) {
if ( ! opt . code ) errors . push ( { severity : 'error' , message : ` Opción " ${ opt . id } " sin código ` , path : ` options. ${ opt . id } .code ` } )
const optName = opt . name as any
for ( const lang of collection . langs ) {
if ( ! optName ? . [ lang ] ) {
warnings . push ( { severity : 'warning' , message : ` Opción " ${ opt . id } " sin nombre en " ${ lang } " ` , path : ` options. ${ opt . id } .name. ${ lang } ` } )
}
}
}
// Entidades
for ( const entity of Object . values ( collection . entities ) ) {
if ( ! entity . code ) errors . push ( { severity : 'error' , message : ` Entidad " ${ entity . id } " sin código ` , path : ` entities. ${ entity . id } .code ` } )
const entityName = entity . name as any
for ( const lang of collection . langs ) {
if ( ! entityName ? . [ lang ] ) {
warnings . push ( { severity : 'warning' , message : ` Entidad " ${ entity . id } " sin nombre en " ${ lang } " ` , path : ` entities. ${ entity . id } .name. ${ lang } ` } )
}
}
// Grupos
for ( const [ groupId , group ] of Object . entries ( entity . groups ) ) {
if ( ! group . code ) errors . push ( { severity : 'error' , message : ` Grupo " ${ groupId } " sin código ` , path : ` entities. ${ entity . id } .groups. ${ groupId } .code ` } )
const groupName = group . name as any
for ( const lang of collection . langs ) {
if ( ! groupName ? . [ lang ] ) {
warnings . push ( { severity : 'warning' , message : ` Grupo " ${ groupId } " sin nombre en " ${ lang } " ` , path : ` entities. ${ entity . id } .groups. ${ groupId } .name. ${ lang } ` } )
}
}
// Atributos del grupo
for ( const attr of group . attributes ) {
validateAttribute ( attr , ` entities. ${ entity . id } .groups. ${ groupId } ` , collection , errors , warnings )
}
}
// Atributos globales de la entidad
for ( const attr of entity . attributes ) {
validateAttribute ( attr , ` entities. ${ entity . id } ` , collection , errors , warnings )
}
}
// Atributos globales del catálogo
for ( const attr of collection . attributes ) {
validateAttribute ( attr , 'collection' , collection , errors , warnings )
}
return { valid : errors.length === 0 , errors , warnings }
}
function validateAttribute (
attr : Attribute ,
scope : string ,
collection : Collection ,
errors : EditorIssue [ ] ,
warnings : EditorIssue [ ] ,
) : void {
const path = ` ${ scope } .attributes. ${ attr . id } `
if ( ! attr . code ) errors . push ( { severity : 'error' , message : ` Atributo " ${ attr . id } " sin código ` , path : ` ${ path } .code ` } )
const attrName = attr . name as any
for ( const lang of collection . langs ) {
if ( ! attrName ? . [ lang ] ) {
warnings . push ( { severity : 'warning' , message : ` Atributo " ${ attr . id } " sin nombre en " ${ lang } " ` , path : ` ${ path } .name. ${ lang } ` } )
}
}
if ( attr . type !== 'fixed' ) {
const domain = attr . domain
if ( domain . kind === 'options' ) {
if ( domain . options . length === 0 ) {
warnings . push ( { severity : 'warning' , message : ` Atributo " ${ attr . id } " sin opciones ` , path : ` ${ path } .domain.options ` } )
} else {
const hasDefault = domain . options . some ( o = > o . optionId === domain . default )
if ( ! hasDefault ) {
errors . push ( { severity : 'error' , message : ` Atributo " ${ attr . id } ": el valor por defecto no está en las opciones ` , path : ` ${ path } .domain.default ` } )
}
for ( const attrOpt of domain . options ) {
if ( ! collection . options [ attrOpt . optionId ] ) {
errors . push ( { severity : 'error' , message : ` Atributo " ${ attr . id } ": opción " ${ attrOpt . optionId } " no existe en la librería ` , path : ` ${ path } .domain.options. ${ attrOpt . optionId } ` } )
}
}
}
} else if ( domain . kind === 'literals' ) {
if ( domain . literals . length === 0 ) {
warnings . push ( { severity : 'warning' , message : ` Atributo " ${ attr . id } " sin literales en el dominio ` , path : ` ${ path } .domain.literals ` } )
}
}
}
}
function getLing ( l : LingString ) : string {
return typeof l === 'string' ? l : ( l as any ) ? . es ? ? ''
}
// ─────────────────────────────────────────────────────────────────────────────
// EDITOR STORE
// ─────────────────────────────────────────────────────────────────────────────
export function createEditorStore ( initial? : Collection ) {
// ── State ──────────────────────────────────────────────────────────────────
let collection = $state < Collection > ( initial ? ? emptyCollection ( ) )
let activeEntityId = $state < EntityID | null > ( null )
let activeGroupId = $state < GroupID | null > ( null )
let activeAttrId = $state < AttrID | null > ( null )
let activeOptionId = $state < OptionID | null > ( null )
let activeRuleIdx = $state < number | null > ( null )
let activePanel = $state < EditorPanel > ( 'objects' )
let isDirty = $state ( false )
function markDirty() { isDirty = true }
// ── Derived ────────────────────────────────────────────────────────────────
const validation = $derived ( validateCollection ( collection ) )
const canPreview = $derived (
validation . valid && Object . keys ( collection . entities ) . length > 0
)
const activeEntity = $derived ( activeEntityId ? collection . entities [ activeEntityId ] ? ? null : null )
const activeGroup = $derived (
activeEntity && activeGroupId ? ( activeEntity . groups [ activeGroupId ] ? ? null ) : null
)
const activeAttribute = $derived ( ( ( ) = > {
if ( ! activeAttrId ) return null
if ( activeGroup ) return activeGroup . attributes . find ( a = > a . id === activeAttrId ) ? ? null
if ( activeEntity ) return activeEntity . attributes . find ( a = > a . id === activeAttrId ) ? ? null
return collection . attributes . find ( a = > a . id === activeAttrId ) ? ? null
} ) ( ) )
const activeOption = $derived ( activeOptionId ? collection . options [ activeOptionId ] ? ? null : null )
// ── Navigation ─────────────────────────────────────────────────────────────
function setPanel ( panel : EditorPanel ) {
activePanel = panel
}
function setActiveEntity ( id : EntityID | null ) {
activeEntityId = id
activeGroupId = null
activeAttrId = null
}
function setActiveGroup ( id : GroupID | null ) {
activeGroupId = id
activeAttrId = null
}
function setActiveAttribute ( id : AttrID | null ) {
activeAttrId = id
}
function setActiveEntityAttribute ( id : AttrID | null ) {
activeGroupId = null
activeAttrId = id
}
function setActiveOption ( id : OptionID | null ) {
activeOptionId = id
}
// ── Collection meta ────────────────────────────────────────────────────────
function updateCollectionMeta ( patch : Partial < Pick < Collection , ' code ' | ' name ' | ' description ' | ' basePath ' | ' langs ' > > ) {
collection = { . . . collection , . . . patch }
isDirty = true
}
// ── Options library ────────────────────────────────────────────────────────
function addOption ( ) : OptionID {
const id = uid ( 'op' ) as OptionID
collection = {
. . . collection ,
options : { . . . collection . options , [ id ] : emptyOption ( id ) } ,
}
activeOptionId = id
markDirty ( )
return id
}
function updateOption ( id : OptionID , patch : Partial < OptionDefinition > ) {
const opt = collection . options [ id ]
if ( ! opt ) return
collection = {
. . . collection ,
options : { . . . collection . options , [ id ] : { . . . opt , . . . patch } } ,
}
markDirty ( )
}
function deleteOption ( id : OptionID ) {
const { [ id ] : _ , . . . rest } = collection . options
collection = { . . . collection , options : rest as Record < OptionID , OptionDefinition > }
if ( activeOptionId === id ) activeOptionId = null
markDirty ( )
}
function duplicateOption ( id : OptionID ) : OptionID {
const opt = collection . options [ id ]
if ( ! opt ) return id
const newId = uid ( 'op' ) as OptionID
const name = typeof opt . name === 'string'
? opt . name + ' (copia)'
: { . . . ( opt . name as any ) , es : ( ( opt . name as any ) . es ? ? '' ) + ' (copia)' }
collection = {
. . . collection ,
options : { . . . collection . options , [ newId ] : { . . . opt , id : newId , code : opt.code + '_copy' , name } } ,
}
activeOptionId = newId
return newId
}
// ── Entities ───────────────────────────────────────────────────────────────
function addEntity ( ) : EntityID {
const id = uid ( 'en' ) as EntityID
collection = {
. . . collection ,
entities : { . . . collection . entities , [ id ] : emptyEntity ( id ) } ,
}
activeEntityId = id
activeGroupId = null
activeAttrId = null
markDirty ( )
return id
}
function updateEntity ( id : EntityID , patch : Record < string , unknown > ) {
const entity = collection . entities [ id ]
if ( ! entity ) return
collection = {
. . . collection ,
entities : { . . . collection . entities , [ id ] : { . . . entity , . . . patch } as Entity } ,
}
markDirty ( )
}
function deleteEntity ( id : EntityID ) {
const { [ id ] : _ , . . . rest } = collection . entities
collection = { . . . collection , entities : rest as Record < EntityID , Entity > }
if ( activeEntityId === id ) setActiveEntity ( null )
markDirty ( )
}
// ── Groups ─────────────────────────────────────────────────────────────────
function addGroup ( entityId : EntityID , kind : 'fixed' | 'optional' = 'fixed' ) : GroupID {
const entity = collection . entities [ entityId ]
if ( ! entity ) return '' as GroupID
const id = uid ( 'gr' ) as GroupID
collection = {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : {
. . . entity ,
groups : { . . . entity . groups , [ id ] : emptyGroup ( id , kind ) } ,
} ,
} ,
}
activeGroupId = id
activeAttrId = null
markDirty ( )
return id
}
function updateGroup ( entityId : EntityID , groupId : GroupID , patch : Record < string , unknown > ) {
const entity = collection . entities [ entityId ]
const group = entity ? . groups [ groupId ]
if ( ! entity || ! group ) return
collection = {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : {
. . . entity ,
groups : { . . . entity . groups , [ groupId ] : { . . . group , . . . patch } as Group } ,
} ,
} ,
}
markDirty ( )
}
function deleteGroup ( entityId : EntityID , groupId : GroupID ) {
const entity = collection . entities [ entityId ]
if ( ! entity ) return
const { [ groupId ] : _ , . . . rest } = entity . groups
collection = {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : { . . . entity , groups : rest as Record < GroupID , Group > } ,
} ,
}
if ( activeGroupId === groupId ) { activeGroupId = null ; activeAttrId = null }
markDirty ( )
}
// ── Attributes ─────────────────────────────────────────────────────────────
function addAttribute ( entityId : EntityID , groupId? : GroupID ) : AttrID {
const id = uid ( 'at' ) as AttrID
const attr = emptyAttribute ( id )
if ( groupId ) {
const entity = collection . entities [ entityId ]
const group = entity ? . groups [ groupId ]
if ( entity && group ) {
collection = {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : {
. . . entity ,
groups : {
. . . entity . groups ,
[ groupId ] : { . . . group , attributes : [ . . . group . attributes , attr ] } ,
} ,
} ,
} ,
}
}
} else {
const entity = collection . entities [ entityId ]
if ( entity ) {
collection = {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : { . . . entity , attributes : [ . . . entity . attributes , attr ] } ,
} ,
}
}
}
activeAttrId = id
markDirty ( )
return id
}
function updateAttribute ( entityId : EntityID , groupId : GroupID | undefined , attrId : AttrID , patch : Record < string , unknown > ) {
collection = produceAttr ( collection , entityId , groupId , attrId , attr = > ( { . . . attr , . . . patch } ) as Attribute )
markDirty ( )
}
function deleteAttribute ( entityId : EntityID , groupId : GroupID | undefined , attrId : AttrID ) {
collection = produceAttr ( collection , entityId , groupId , attrId , null )
if ( activeAttrId === attrId ) activeAttrId = null
markDirty ( )
}
// ── Attribute options ──────────────────────────────────────────────────────
function addOptionToAttribute ( entityId : EntityID , groupId : GroupID | undefined , attrId : AttrID , optionId : OptionID ) {
collection = produceAttr ( collection , entityId , groupId , attrId , attr = > {
if ( attr . type === 'fixed' || attr . type === 'literal' ) return attr
if ( attr . options . some ( o = > o . optionId === optionId ) ) return attr
const newOpt : AttributeOption = { optionId }
const options = [ . . . attr . options , newOpt ]
const updated = { . . . attr , options }
if ( ! attr . default && options . length === 1 ) ( updated as any ) . default = optionId
return updated
} )
markDirty ( )
}
function removeOptionFromAttribute ( entityId : EntityID , groupId : GroupID | undefined , attrId : AttrID , optionId : OptionID ) {
collection = produceAttr ( collection , entityId , groupId , attrId , attr = > {
if ( attr . type === 'fixed' || attr . type === 'literal' ) return attr
const options = attr . options . filter ( o = > o . optionId !== optionId )
const updated = { . . . attr , options }
if ( ( attr as any ) . default === optionId ) ( updated as any ) . default = options [ 0 ] ? . optionId ? ? ''
return updated
} )
markDirty ( )
}
// ── Catalog attributes ─────────────────────────────────────────────────────
function addCatalogAttribute ( ) : AttrID {
const id = uid ( 'at' ) as AttrID
const attr = emptyLiteralAttribute ( id )
collection = { . . . collection , attributes : [ . . . collection . attributes , attr ] }
activeAttrId = id
activeEntityId = null
activeGroupId = null
markDirty ( )
return id
}
function updateCatalogAttribute ( attrId : AttrID , patch : Record < string , unknown > ) {
collection = {
. . . collection ,
attributes : collection.attributes.map ( a = >
a . id === attrId ? { . . . a , . . . patch } as Attribute : a
) ,
}
}
function deleteCatalogAttribute ( attrId : AttrID ) {
collection = { . . . collection , attributes : collection.attributes.filter ( a = > a . id !== attrId ) }
if ( activeAttrId === attrId ) activeAttrId = null
}
// ── Rules ──────────────────────────────────────────────────────────────────
function addRule (
entityId : EntityID ,
groupId : GroupID | undefined ,
attrId : AttrID | undefined ,
rule : Rule ,
) {
if ( attrId ) {
collection = produceAttr ( collection , entityId , groupId , attrId , ( a ) = > ( {
. . . a ,
rules : [ . . . ( ( a as any ) . rules ? ? [ ] ) , rule ] ,
} ) )
} else if ( groupId ) {
collection = produceGroup ( collection , entityId , groupId , ( g ) = > ( {
. . . g ,
rules : [ . . . ( ( g as any ) . rules ? ? [ ] ) , rule ] ,
} ) )
} else {
collection = produceEntity ( collection , entityId , ( e ) = > ( {
. . . e ,
rules : [ . . . ( e . rules ? ? [ ] ) , rule ] ,
} ) )
}
isDirty = true
}
function updateRule (
entityId : EntityID ,
groupId : GroupID | undefined ,
attrId : AttrID | undefined ,
ruleId : string ,
patch : Partial < Rule > ,
) {
const mapRules = ( rules : Rule [ ] ) = >
rules . map ( r = > ( r . id === ruleId ? { . . . r , . . . patch } : r ) )
if ( attrId ) {
collection = produceAttr ( collection , entityId , groupId , attrId , ( a ) = > ( {
. . . a ,
rules : mapRules ( ( a as any ) . rules ? ? [ ] ) ,
} ) )
} else if ( groupId ) {
collection = produceGroup ( collection , entityId , groupId , ( g ) = > ( {
. . . g ,
rules : mapRules ( ( g as any ) . rules ? ? [ ] ) ,
} ) )
} else {
collection = produceEntity ( collection , entityId , ( e ) = > ( {
. . . e ,
rules : mapRules ( e . rules ? ? [ ] ) ,
} ) )
}
isDirty = true
}
function deleteRule (
entityId : EntityID ,
groupId : GroupID | undefined ,
attrId : AttrID | undefined ,
ruleId : string ,
) {
const filterRules = ( rules : Rule [ ] ) = >
rules . filter ( r = > r . id !== ruleId )
if ( attrId ) {
collection = produceAttr ( collection , entityId , groupId , attrId , ( a ) = > ( {
. . . a ,
rules : filterRules ( ( a as any ) . rules ? ? [ ] ) ,
} ) )
} else if ( groupId ) {
collection = produceGroup ( collection , entityId , groupId , ( g ) = > ( {
. . . g ,
rules : filterRules ( ( g as any ) . rules ? ? [ ] ) ,
} ) )
} else {
collection = produceEntity ( collection , entityId , ( e ) = > ( {
. . . e ,
rules : filterRules ( e . rules ? ? [ ] ) ,
} ) )
}
isDirty = true
}
// ── Import / Export ────────────────────────────────────────────────────────
function exportToJson ( ) : string {
return JSON . stringify ( collection , null , 2 )
}
function importFromJson ( json : string ) {
const parsed = JSON . parse ( json )
collection = parsed
activeEntityId = null
activeGroupId = null
activeAttrId = null
activeOptionId = null
isDirty = false
}
function reset() {
collection = emptyCollection ( )
activeEntityId = null
activeGroupId = null
activeAttrId = null
activeOptionId = null
isDirty = false
}
// ── Public API ─────────────────────────────────────────────────────────────
return {
// State (getters)
get collection ( ) { return collection } ,
get validation ( ) { return validation } ,
get activePanel ( ) { return activePanel } ,
get isDirty ( ) { return isDirty } ,
get canPreview ( ) { return canPreview } ,
get activeEntityId ( ) { return activeEntityId } ,
get activeGroupId ( ) { return activeGroupId } ,
get activeAttrId ( ) { return activeAttrId } ,
get activeOptionId ( ) { return activeOptionId } ,
get activeEntity ( ) { return activeEntity } ,
get activeGroup ( ) { return activeGroup } ,
get activeAttribute ( ) { return activeAttribute } ,
get activeOption ( ) { return activeOption } ,
// Navigation
setPanel ,
setActiveEntity ,
setActiveGroup ,
setActiveAttribute ,
setActiveEntityAttribute ,
setActiveOption ,
// Collection
updateCollectionMeta ,
// Options
addOption ,
updateOption ,
deleteOption ,
duplicateOption ,
// Entities
addEntity ,
updateEntity ,
deleteEntity ,
// Groups
addGroup ,
updateGroup ,
deleteGroup ,
// Attributes
addAttribute ,
updateAttribute ,
deleteAttribute ,
addCatalogAttribute ,
updateCatalogAttribute ,
deleteCatalogAttribute ,
// Attribute options
addOptionToAttribute ,
removeOptionFromAttribute ,
// Rules
addRule ,
updateRule ,
deleteRule ,
// Import/Export
exportToJson ,
importFromJson ,
reset ,
}
}
export type EditorStore = ReturnType < typeof createEditorStore >
// ─────────────────────────────────────────────────────────────────────────────
// HELPERS INTERNOS — produce inmutable
// ─────────────────────────────────────────────────────────────────────────────
function produceAttr (
collection : Collection ,
entityId : EntityID ,
groupId : GroupID | undefined ,
attrId : AttrID ,
fn : ( ( attr : Attribute ) = > Attribute ) | null ,
) : Collection {
const entity = collection . entities [ entityId ]
if ( ! entity ) return collection
function mapAttrs ( attrs : Attribute [ ] ) : Attribute [ ] {
if ( fn === null ) return attrs . filter ( a = > a . id !== attrId )
return attrs . map ( a = > a . id === attrId ? fn ( a ) as Attribute : a )
}
if ( groupId ) {
const group = entity . groups [ groupId ]
if ( ! group ) return collection
return {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : {
. . . entity ,
groups : {
. . . entity . groups ,
[ groupId ] : { . . . group , attributes : mapAttrs ( group . attributes ) } ,
} ,
} ,
} ,
}
}
return {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : { . . . entity , attributes : mapAttrs ( entity . attributes ) } ,
} ,
}
}
function produceEntity (
collection : Collection ,
entityId : EntityID ,
fn : ( entity : Entity ) = > Entity ,
) : Collection {
const entity = collection . entities [ entityId ]
if ( ! entity ) return collection
return {
. . . collection ,
entities : { . . . collection . entities , [ entityId ] : fn ( entity ) } ,
}
}
function produceGroup (
collection : Collection ,
entityId : EntityID ,
groupId : GroupID ,
fn : ( group : Group ) = > Group ,
) : Collection {
const entity = collection . entities [ entityId ]
if ( ! entity ) return collection
const group = entity . groups [ groupId ]
if ( ! group ) return collection
return {
. . . collection ,
entities : {
. . . collection . entities ,
[ entityId ] : {
. . . entity ,
groups : { . . . entity . groups , [ groupId ] : fn ( group ) } ,
} ,
} ,
}
}