From 7f29873712d974b234c97866351105d69e996b2b Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 1 Jul 2024 14:03:07 +0200 Subject: [PATCH 1/3] tests(react-binding): refactor --- .../cases/unit/core/entityOperations.test.tsx | 46 +++--- .../cases/unit/core/eventManager.test.tsx | 43 ++---- .../unit/core => lib}/bindingFactory.ts | 25 +--- .../tests/lib/convertModelToAdminSchema.ts | 131 ++++++++++++++++++ 4 files changed, 168 insertions(+), 77 deletions(-) rename packages/react-binding/tests/{cases/unit/core => lib}/bindingFactory.ts (50%) create mode 100644 packages/react-binding/tests/lib/convertModelToAdminSchema.ts diff --git a/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx b/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx index bd27e87a17..40b76a096e 100644 --- a/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx +++ b/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx @@ -1,43 +1,31 @@ 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: ( ), - 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) diff --git a/packages/react-binding/tests/cases/unit/core/eventManager.test.tsx b/packages/react-binding/tests/cases/unit/core/eventManager.test.tsx index 6ff578ac34..325c5b033b 100644 --- a/packages/react-binding/tests/cases/unit/core/eventManager.test.tsx +++ b/packages/react-binding/tests/cases/unit/core/eventManager.test.tsx @@ -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: ( ), - 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', () => { diff --git a/packages/react-binding/tests/cases/unit/core/bindingFactory.ts b/packages/react-binding/tests/lib/bindingFactory.ts similarity index 50% rename from packages/react-binding/tests/cases/unit/core/bindingFactory.ts rename to packages/react-binding/tests/lib/bindingFactory.ts index 1ca2f744b7..db64a3dfe3 100644 --- a/packages/react-binding/tests/cases/unit/core/bindingFactory.ts +++ b/packages/react-binding/tests/lib/bindingFactory.ts @@ -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) @@ -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 } } diff --git a/packages/react-binding/tests/lib/convertModelToAdminSchema.ts b/packages/react-binding/tests/lib/convertModelToAdminSchema.ts new file mode 100644 index 0000000000..90538b9447 --- /dev/null +++ b/packages/react-binding/tests/lib/convertModelToAdminSchema.ts @@ -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(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, + } +} From 395612b27bbe3549059a2584ec5303df8c32f745 Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 1 Jul 2024 14:17:10 +0200 Subject: [PATCH 2/3] tests(react-binding): add tests for connectEntityAtField --- .../cases/unit/core/entityOperations.test.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx b/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx index 40b76a096e..a3e404da82 100644 --- a/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx +++ b/packages/react-binding/tests/cases/unit/core/entityOperations.test.tsx @@ -32,4 +32,49 @@ describe('entity operations', () => { 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: (<> + + + + + ), + 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: (<> + + + + + + ), + 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') + } +} From f10036889a58f0595351fa605ba9257e431e355e Mon Sep 17 00:00:00 2001 From: David Matejka Date: Mon, 1 Jul 2024 14:17:59 +0200 Subject: [PATCH 3/3] fix(binding): in connectEntityAtField require marker for input state only --- packages/binding/src/core/operations/EntityOperations.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/binding/src/core/operations/EntityOperations.ts b/packages/binding/src/core/operations/EntityOperations.ts index 96d5d2175c..e065346b1b 100644 --- a/packages/binding/src/core/operations/EntityOperations.ts +++ b/packages/binding/src/core/operations/EntityOperations.ts @@ -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) @@ -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) @@ -286,10 +286,14 @@ export class EntityOperations { container: EntityFieldMarkersContainer, field: FieldName, type: 'connect' | 'disconnect', + mustExists: boolean, ): IterableIterator { 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]