Skip to content

Commit 2a9bc8e

Browse files
authored
Merge pull request #734 from contember/fix/connect-entity
binding: connect entity at field fix
2 parents a3e9f50 + f100368 commit 2a9bc8e

File tree

5 files changed

+219
-79
lines changed

5 files changed

+219
-79
lines changed

packages/binding/src/core/operations/EntityOperations.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class EntityOperations {
6767
}
6868

6969
for (const state of StateIterator.eachSiblingRealm(outerState)) {
70-
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'connect')
70+
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'connect', state === outerState)
7171
for (const targetHasOneMarker of targetHasOneMarkers) {
7272
const previouslyConnectedState = state.children.get(targetHasOneMarker.placeholderName)
7373

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

167167
for (const state of StateIterator.eachSiblingRealm(outerState)) {
168-
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'disconnect')
168+
const targetHasOneMarkers = this.resolveHasOneRelationMarkers(getEntityMarker(state).fields, fieldName, 'disconnect', state === outerState)
169169
for (const targetHasOneMarker of targetHasOneMarkers) {
170170
const stateToDisconnect = state.children.get(targetHasOneMarker.placeholderName)
171171

@@ -286,10 +286,14 @@ export class EntityOperations {
286286
container: EntityFieldMarkersContainer,
287287
field: FieldName,
288288
type: 'connect' | 'disconnect',
289+
mustExists: boolean,
289290
): IterableIterator<HasOneRelationMarker> {
290291
const placeholders = container.placeholders.get(field)
291292

292293
if (placeholders === undefined) {
294+
if (!mustExists) {
295+
return
296+
}
293297
throw new BindingError(`Cannot ${type} at field '${field}' as it wasn't registered during static render.`)
294298
}
295299
const normalizedPlaceholders = placeholders instanceof Set ? placeholders : [placeholders]
Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,80 @@
11
import { describe, expect, it } from 'vitest'
2-
import { EntitySubTree, Field } from '../../../../src'
3-
import { createBindingWithEntitySubtree } from './bindingFactory'
2+
import { EntitySubTree, Field, HasOne } from '../../../../src'
3+
import { createBinding } from '../../../lib/bindingFactory'
4+
import { c, createSchema } from '@contember/schema-definition'
5+
import { convertModelToAdminSchema } from '../../../lib/convertModelToAdminSchema'
6+
import assert from 'assert'
7+
8+
9+
namespace TrackChangesModel {
10+
export class Foo {
11+
fooField = c.stringColumn()
12+
}
13+
}
414

515
describe('entity operations', () => {
616
it('tracks unpersisted changes count', () => {
7-
const { entity } = createBindingWithEntitySubtree({
17+
const { treeStore } = createBinding({
818
node: (
919
<EntitySubTree entity="Foo(bar = 123)">
1020
<Field field={'fooField'} />
1121
</EntitySubTree>
1222
),
13-
schema: {
14-
enums: [],
15-
entities: [{
16-
name: 'Foo',
17-
customPrimaryAllowed: false,
18-
unique: [],
19-
fields: [
20-
{
21-
__typename: '_Column',
22-
name: 'id',
23-
nullable: false,
24-
defaultValue: null,
25-
type: 'Uuid',
26-
enumName: null,
27-
},
28-
{
29-
__typename: '_Column',
30-
type: 'String',
31-
enumName: null,
32-
nullable: true,
33-
defaultValue: null,
34-
name: 'fooField',
35-
},
36-
],
37-
}],
38-
},
23+
schema: convertModelToAdminSchema(createSchema(TrackChangesModel).model),
3924
})
4025

26+
const entity = Array.from(treeStore.subTreeStatesByRoot.get(undefined)!.values())[0]
27+
assert(entity.type === 'entityRealm')
28+
4129
expect(entity.unpersistedChangesCount).eq(0)
4230
entity.getAccessor().getField('fooField').updateValue('bar')
4331
expect(entity.unpersistedChangesCount).eq(1)
4432
entity.getAccessor().getField('fooField').updateValue(null)
4533
expect(entity.unpersistedChangesCount).eq(0)
4634
})
35+
36+
it('fails when relation not defined in static render', () => {
37+
const { treeStore, environment } = createBinding({
38+
node: (<>
39+
<EntitySubTree entity="Article(id = 'cfb8d0ae-c892-4047-acfb-a89adab2371d')" alias="article">
40+
</EntitySubTree>
41+
<EntitySubTree entity="Category(id = '89560cfa-f874-42b6-ace3-35a8ebcbba15')" alias="category">
42+
</EntitySubTree>
43+
</>),
44+
schema: convertModelToAdminSchema(createSchema(ModelWithRelation).model),
45+
})
46+
const article = treeStore.getSubTreeState('entity', undefined, 'article', environment)
47+
const category = treeStore.getSubTreeState('entity', undefined, 'category', environment)
48+
expect(() => {
49+
article.getAccessor().connectEntityAtField('category', category.getAccessor())
50+
}).toThrowError('Cannot connect at field \'category\' as it wasn\'t registered during static render.')
51+
})
52+
53+
it('ok when relation defined in static render', () => {
54+
const { treeStore, environment } = createBinding({
55+
node: (<>
56+
<EntitySubTree entity="Article(id = 'cfb8d0ae-c892-4047-acfb-a89adab2371d')" alias="article">
57+
<HasOne field="category" />
58+
</EntitySubTree>
59+
<EntitySubTree entity="Category(id = '89560cfa-f874-42b6-ace3-35a8ebcbba15')" alias="category">
60+
</EntitySubTree>
61+
</>),
62+
schema: convertModelToAdminSchema(createSchema(ModelWithRelation).model),
63+
})
64+
const article = treeStore.getSubTreeState('entity', undefined, 'article', environment)
65+
const category = treeStore.getSubTreeState('entity', undefined, 'category', environment)
66+
article.getAccessor().connectEntityAtField('category', category.getAccessor())
67+
})
4768
})
69+
70+
71+
72+
namespace ModelWithRelation {
73+
export class Category {
74+
articles = c.oneHasMany(Article, 'category')
75+
}
76+
77+
export class Article {
78+
category = c.manyHasOne(Category, 'articles')
79+
}
80+
}

packages/react-binding/tests/cases/unit/core/eventManager.test.tsx

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,28 @@
11
import { describe, expect, it } from 'vitest'
22
import { EntityAccessor, EntitySubTree, Field } from '../../../../src'
3-
import { createBindingWithEntitySubtree } from './bindingFactory'
3+
import { createBinding } from '../../../lib/bindingFactory'
4+
import { c, createSchema } from '@contember/schema-definition'
5+
import { convertModelToAdminSchema } from '../../../lib/convertModelToAdminSchema'
6+
import assert from 'assert'
7+
8+
namespace EventManagerModel {
9+
export class Foo {
10+
fooField = c.stringColumn()
11+
}
12+
}
413

514
const prepareBeforePersistTest = ({ event }: { event: (getAccessor: () => EntityAccessor) => any }) => {
6-
return createBindingWithEntitySubtree({
15+
const { treeStore, eventManager } = createBinding({
716
node: (
817
<EntitySubTree entity="Foo(bar = 123)" onBeforePersist={event}>
918
<Field field={'fooField'} />
1019
</EntitySubTree>
1120
),
12-
schema: {
13-
enums: [],
14-
entities: [{
15-
name: 'Foo',
16-
customPrimaryAllowed: false,
17-
unique: [],
18-
fields: [
19-
{
20-
__typename: '_Column',
21-
name: 'id',
22-
nullable: false,
23-
defaultValue: null,
24-
type: 'Uuid',
25-
enumName: null,
26-
},
27-
{
28-
__typename: '_Column',
29-
type: 'String',
30-
enumName: null,
31-
nullable: true,
32-
defaultValue: null,
33-
name: 'fooField',
34-
},
35-
],
36-
}],
37-
},
21+
schema: convertModelToAdminSchema(createSchema(EventManagerModel).model),
3822
})
23+
const entity = Array.from(treeStore.subTreeStatesByRoot.get(undefined)!.values())[0]
24+
assert(entity.type === 'entityRealm')
25+
return { entity, eventManager }
3926
}
4027

4128
describe('event manager', () => {

packages/react-binding/tests/cases/unit/core/bindingFactory.ts renamed to packages/react-binding/tests/lib/bindingFactory.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
1-
import assert from 'assert'
21
import { ReactNode } from 'react'
3-
import {
4-
Config,
5-
DirtinessTracker,
6-
Environment,
7-
EventManager,
8-
RawSchema,
9-
Schema,
10-
SchemaPreprocessor,
11-
StateInitializer,
12-
TreeAugmenter,
13-
TreeStore,
14-
} from '@contember/binding'
15-
import { MarkerTreeGenerator } from '../../../../src'
2+
import { Config, DirtinessTracker, Environment, EventManager, Schema, SchemaStore, StateInitializer, TreeAugmenter, TreeStore } from '@contember/binding'
3+
import { MarkerTreeGenerator } from '../../src'
164

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

34-
const entity = Array.from(treeStore.subTreeStatesByRoot.get(undefined)!.values())[0]
35-
assert(entity.type === 'entityRealm')
36-
37-
return { entity, eventManager }
22+
return { eventManager, treeStore, environment }
3823
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { SchemaEntities, SchemaField, SchemaStore } from '@contember/binding'
2+
import { Model } from '@contember/schema'
3+
import { acceptFieldVisitor } from '@contember/schema-utils'
4+
5+
export const convertModelToAdminSchema = (model: Model.Schema): SchemaStore => {
6+
const enums: SchemaStore['enums'] = new Map()
7+
for (const [name, values] of Object.entries(model.enums)) {
8+
enums.set(name, new Set(values))
9+
}
10+
const entities: SchemaEntities = new Map()
11+
for (const entity of Object.values(model.entities)) {
12+
entities.set(entity.name, {
13+
name: entity.name,
14+
customPrimaryAllowed: false, // todo
15+
unique: Object.values(entity.unique).map(it => ({
16+
fields: new Set(it.fields),
17+
})),
18+
fields: new Map(Object.values(entity.fields).map((it): [string, SchemaField] => {
19+
const schemaField = acceptFieldVisitor<SchemaField>(model, entity, it, {
20+
visitColumn: ({ column }) => {
21+
return {
22+
__typename: '_Column',
23+
name: column.name,
24+
nullable: column.nullable,
25+
type: column.type,
26+
defaultValue: column.default ?? null,
27+
enumName: column.type === Model.ColumnType.Enum ? column.columnType : null,
28+
}
29+
},
30+
visitManyHasManyInverse: ({ relation }) => {
31+
return {
32+
__typename: '_Relation',
33+
type: 'ManyHasMany',
34+
name: relation.name,
35+
side: 'inverse',
36+
ownedBy: relation.ownedBy,
37+
targetEntity: relation.target,
38+
nullable: null,
39+
onDelete: null,
40+
orphanRemoval: null,
41+
// todo
42+
orderBy: null,
43+
}
44+
},
45+
visitManyHasManyOwning: ({ relation }) => {
46+
return {
47+
__typename: '_Relation',
48+
type: 'ManyHasMany',
49+
name: relation.name,
50+
side: 'owning',
51+
inversedBy: relation.inversedBy ?? null,
52+
targetEntity: relation.target,
53+
nullable: null,
54+
onDelete: null,
55+
orphanRemoval: null,
56+
// todo
57+
orderBy: null,
58+
}
59+
},
60+
visitManyHasOne: ({ relation }) => {
61+
return {
62+
__typename: '_Relation',
63+
type: 'ManyHasOne',
64+
name: relation.name,
65+
side: 'owning',
66+
inversedBy: relation.inversedBy ?? null,
67+
targetEntity: relation.target,
68+
nullable: relation.nullable,
69+
orphanRemoval: null,
70+
orderBy: null,
71+
// todo
72+
onDelete: null,
73+
}
74+
},
75+
visitOneHasMany: ({ relation }) => {
76+
return {
77+
__typename: '_Relation',
78+
type: 'OneHasMany',
79+
name: relation.name,
80+
side: 'inverse',
81+
ownedBy: relation.ownedBy,
82+
targetEntity: relation.target,
83+
nullable: null,
84+
orphanRemoval: null,
85+
onDelete: null,
86+
// todo
87+
orderBy: null,
88+
}
89+
},
90+
visitOneHasOneInverse: ({ relation }) => {
91+
return {
92+
__typename: '_Relation',
93+
type: 'ManyHasOne',
94+
name: relation.name,
95+
side: 'inverse',
96+
ownedBy: relation.ownedBy,
97+
targetEntity: relation.target,
98+
nullable: relation.nullable,
99+
orderBy: null,
100+
orphanRemoval: null,
101+
onDelete: null,
102+
}
103+
},
104+
visitOneHasOneOwning: ({ relation }) => {
105+
return {
106+
__typename: '_Relation',
107+
type: 'ManyHasOne',
108+
name: relation.name,
109+
side: 'owning',
110+
inversedBy: relation.inversedBy ?? null,
111+
targetEntity: relation.target,
112+
nullable: relation.nullable,
113+
orderBy: null,
114+
// todo
115+
onDelete: null,
116+
orphanRemoval: null,
117+
}
118+
},
119+
})
120+
return [
121+
it.name,
122+
schemaField,
123+
]
124+
})),
125+
})
126+
}
127+
return {
128+
enums,
129+
entities,
130+
}
131+
}

0 commit comments

Comments
 (0)