Skip to content

Commit

Permalink
Merge pull request #734 from contember/fix/connect-entity
Browse files Browse the repository at this point in the history
binding: connect entity at field fix
  • Loading branch information
matej21 committed Jul 1, 2024
2 parents a3e9f50 + f100368 commit 2a9bc8e
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 79 deletions.
8 changes: 6 additions & 2 deletions packages/binding/src/core/operations/EntityOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class EntityOperations {
}

for (const state of StateIterator.eachSiblingRealm(outerState)) {
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'connect')
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'connect', state === outerState)
for (const targetHasOneMarker of targetHasOneMarkers) {
const previouslyConnectedState = state.children.get(targetHasOneMarker.placeholderName)

Expand Down Expand Up @@ -165,7 +165,7 @@ export class EntityOperations {
const persistedData = this.treeStore.persistedEntityData.get(outerState.entity.id.uniqueValue)

for (const state of StateIterator.eachSiblingRealm(outerState)) {
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'disconnect')
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'disconnect', state === outerState)
for (const targetHasOneMarker of targetHasOneMarkers) {
const stateToDisconnect = state.children.get(targetHasOneMarker.placeholderName)

Expand Down Expand Up @@ -286,10 +286,14 @@ export class EntityOperations {
container: EntityFieldMarkersContainer,
field: FieldName,
type: 'connect' | 'disconnect',
mustExists: boolean,
): IterableIterator<HasOneRelationMarker> {
const placeholders = container.placeholders.get(field)

if (placeholders === undefined) {
if (!mustExists) {
return
}
throw new BindingError(`Cannot ${type} at field '${field}' as it wasn't registered during static render.`)
}
const normalizedPlaceholders = placeholders instanceof Set ? placeholders : [placeholders]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,80 @@
import { describe, expect, it } from 'vitest'
import { EntitySubTree, Field } from '../../../../src'
import { createBindingWithEntitySubtree } from './bindingFactory'
import { EntitySubTree, Field, HasOne } from '../../../../src'
import { createBinding } from '../../../lib/bindingFactory'
import { c, createSchema } from '@contember/schema-definition'
import { convertModelToAdminSchema } from '../../../lib/convertModelToAdminSchema'
import assert from 'assert'


namespace TrackChangesModel {
export class Foo {
fooField = c.stringColumn()
}
}

describe('entity operations', () => {
it('tracks unpersisted changes count', () => {
const { entity } = createBindingWithEntitySubtree({
const { treeStore } = createBinding({
node: (
<EntitySubTree entity="Foo(bar = 123)">
<Field field={'fooField'} />
</EntitySubTree>
),
schema: {
enums: [],
entities: [{
name: 'Foo',
customPrimaryAllowed: false,
unique: [],
fields: [
{
__typename: '_Column',
name: 'id',
nullable: false,
defaultValue: null,
type: 'Uuid',
enumName: null,
},
{
__typename: '_Column',
type: 'String',
enumName: null,
nullable: true,
defaultValue: null,
name: 'fooField',
},
],
}],
},
schema: convertModelToAdminSchema(createSchema(TrackChangesModel).model),
})

const entity = Array.from(treeStore.subTreeStatesByRoot.get(undefined)!.values())[0]
assert(entity.type === 'entityRealm')

expect(entity.unpersistedChangesCount).eq(0)
entity.getAccessor().getField('fooField').updateValue('bar')
expect(entity.unpersistedChangesCount).eq(1)
entity.getAccessor().getField('fooField').updateValue(null)
expect(entity.unpersistedChangesCount).eq(0)
})

it('fails when relation not defined in static render', () => {
const { treeStore, environment } = createBinding({
node: (<>
<EntitySubTree entity="Article(id = 'cfb8d0ae-c892-4047-acfb-a89adab2371d')" alias="article">
</EntitySubTree>
<EntitySubTree entity="Category(id = '89560cfa-f874-42b6-ace3-35a8ebcbba15')" alias="category">
</EntitySubTree>
</>),
schema: convertModelToAdminSchema(createSchema(ModelWithRelation).model),
})
const article = treeStore.getSubTreeState('entity', undefined, 'article', environment)
const category = treeStore.getSubTreeState('entity', undefined, 'category', environment)
expect(() => {
article.getAccessor().connectEntityAtField('category', category.getAccessor())
}).toThrowError('Cannot connect at field \'category\' as it wasn\'t registered during static render.')
})

it('ok when relation defined in static render', () => {
const { treeStore, environment } = createBinding({
node: (<>
<EntitySubTree entity="Article(id = 'cfb8d0ae-c892-4047-acfb-a89adab2371d')" alias="article">
<HasOne field="category" />
</EntitySubTree>
<EntitySubTree entity="Category(id = '89560cfa-f874-42b6-ace3-35a8ebcbba15')" alias="category">
</EntitySubTree>
</>),
schema: convertModelToAdminSchema(createSchema(ModelWithRelation).model),
})
const article = treeStore.getSubTreeState('entity', undefined, 'article', environment)
const category = treeStore.getSubTreeState('entity', undefined, 'category', environment)
article.getAccessor().connectEntityAtField('category', category.getAccessor())
})
})



namespace ModelWithRelation {
export class Category {
articles = c.oneHasMany(Article, 'category')
}

export class Article {
category = c.manyHasOne(Category, 'articles')
}
}
43 changes: 15 additions & 28 deletions packages/react-binding/tests/cases/unit/core/eventManager.test.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,28 @@
import { describe, expect, it } from 'vitest'
import { EntityAccessor, EntitySubTree, Field } from '../../../../src'
import { createBindingWithEntitySubtree } from './bindingFactory'
import { createBinding } from '../../../lib/bindingFactory'
import { c, createSchema } from '@contember/schema-definition'
import { convertModelToAdminSchema } from '../../../lib/convertModelToAdminSchema'
import assert from 'assert'

namespace EventManagerModel {
export class Foo {
fooField = c.stringColumn()
}
}

const prepareBeforePersistTest = ({ event }: { event: (getAccessor: () => EntityAccessor) => any }) => {
return createBindingWithEntitySubtree({
const { treeStore, eventManager } = createBinding({
node: (
<EntitySubTree entity="Foo(bar = 123)" onBeforePersist={event}>
<Field field={'fooField'} />
</EntitySubTree>
),
schema: {
enums: [],
entities: [{
name: 'Foo',
customPrimaryAllowed: false,
unique: [],
fields: [
{
__typename: '_Column',
name: 'id',
nullable: false,
defaultValue: null,
type: 'Uuid',
enumName: null,
},
{
__typename: '_Column',
type: 'String',
enumName: null,
nullable: true,
defaultValue: null,
name: 'fooField',
},
],
}],
},
schema: convertModelToAdminSchema(createSchema(EventManagerModel).model),
})
const entity = Array.from(treeStore.subTreeStatesByRoot.get(undefined)!.values())[0]
assert(entity.type === 'entityRealm')
return { entity, eventManager }
}

describe('event manager', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import assert from 'assert'
import { ReactNode } from 'react'
import {
Config,
DirtinessTracker,
Environment,
EventManager,
RawSchema,
Schema,
SchemaPreprocessor,
StateInitializer,
TreeAugmenter,
TreeStore,
} from '@contember/binding'
import { MarkerTreeGenerator } from '../../../../src'
import { Config, DirtinessTracker, Environment, EventManager, Schema, SchemaStore, StateInitializer, TreeAugmenter, TreeStore } from '@contember/binding'
import { MarkerTreeGenerator } from '../../src'

export const createBindingWithEntitySubtree = ({ node, schema }: {node: ReactNode, schema: RawSchema}) => {
const finalSchema = new Schema(SchemaPreprocessor.processRawSchema(schema))
export const createBinding = ({ node, schema }: { node: ReactNode, schema: SchemaStore }) => {
const finalSchema = new Schema(schema)
const treeStore = new TreeStore(finalSchema)
const environment = Environment.create().withSchema(finalSchema)
const generator = new MarkerTreeGenerator(node, environment)
Expand All @@ -31,8 +19,5 @@ export const createBindingWithEntitySubtree = ({ node, schema }: {node: ReactNod
const treeAugmenter = new TreeAugmenter(eventManager, stateInitializer, treeStore)
treeAugmenter.extendTreeStates(undefined, generator.generate())

const entity = Array.from(treeStore.subTreeStatesByRoot.get(undefined)!.values())[0]
assert(entity.type === 'entityRealm')

return { entity, eventManager }
return { eventManager, treeStore, environment }
}
131 changes: 131 additions & 0 deletions packages/react-binding/tests/lib/convertModelToAdminSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { SchemaEntities, SchemaField, SchemaStore } from '@contember/binding'
import { Model } from '@contember/schema'
import { acceptFieldVisitor } from '@contember/schema-utils'

export const convertModelToAdminSchema = (model: Model.Schema): SchemaStore => {
const enums: SchemaStore['enums'] = new Map()
for (const [name, values] of Object.entries(model.enums)) {
enums.set(name, new Set(values))
}
const entities: SchemaEntities = new Map()
for (const entity of Object.values(model.entities)) {
entities.set(entity.name, {
name: entity.name,
customPrimaryAllowed: false, // todo
unique: Object.values(entity.unique).map(it => ({
fields: new Set(it.fields),
})),
fields: new Map(Object.values(entity.fields).map((it): [string, SchemaField] => {
const schemaField = acceptFieldVisitor<SchemaField>(model, entity, it, {
visitColumn: ({ column }) => {
return {
__typename: '_Column',
name: column.name,
nullable: column.nullable,
type: column.type,
defaultValue: column.default ?? null,
enumName: column.type === Model.ColumnType.Enum ? column.columnType : null,
}
},
visitManyHasManyInverse: ({ relation }) => {
return {
__typename: '_Relation',
type: 'ManyHasMany',
name: relation.name,
side: 'inverse',
ownedBy: relation.ownedBy,
targetEntity: relation.target,
nullable: null,
onDelete: null,
orphanRemoval: null,
// todo
orderBy: null,
}
},
visitManyHasManyOwning: ({ relation }) => {
return {
__typename: '_Relation',
type: 'ManyHasMany',
name: relation.name,
side: 'owning',
inversedBy: relation.inversedBy ?? null,
targetEntity: relation.target,
nullable: null,
onDelete: null,
orphanRemoval: null,
// todo
orderBy: null,
}
},
visitManyHasOne: ({ relation }) => {
return {
__typename: '_Relation',
type: 'ManyHasOne',
name: relation.name,
side: 'owning',
inversedBy: relation.inversedBy ?? null,
targetEntity: relation.target,
nullable: relation.nullable,
orphanRemoval: null,
orderBy: null,
// todo
onDelete: null,
}
},
visitOneHasMany: ({ relation }) => {
return {
__typename: '_Relation',
type: 'OneHasMany',
name: relation.name,
side: 'inverse',
ownedBy: relation.ownedBy,
targetEntity: relation.target,
nullable: null,
orphanRemoval: null,
onDelete: null,
// todo
orderBy: null,
}
},
visitOneHasOneInverse: ({ relation }) => {
return {
__typename: '_Relation',
type: 'ManyHasOne',
name: relation.name,
side: 'inverse',
ownedBy: relation.ownedBy,
targetEntity: relation.target,
nullable: relation.nullable,
orderBy: null,
orphanRemoval: null,
onDelete: null,
}
},
visitOneHasOneOwning: ({ relation }) => {
return {
__typename: '_Relation',
type: 'ManyHasOne',
name: relation.name,
side: 'owning',
inversedBy: relation.inversedBy ?? null,
targetEntity: relation.target,
nullable: relation.nullable,
orderBy: null,
// todo
onDelete: null,
orphanRemoval: null,
}
},
})
return [
it.name,
schemaField,
]
})),
})
}
return {
enums,
entities,
}
}

0 comments on commit 2a9bc8e

Please sign in to comment.