import {statefulEntity} from "./stateful-zentity"; import {z, ZodDate, ZodDiscriminatedUnion, ZodError, ZodLiteral, ZodObject, ZodString} from "zod"; import {isZentity} from "./zentity"; describe("StatefulZentity", () => { const TestClazz = statefulEntity({ discriminator: "state", base: { createTime: z.string() }, states: { "initial": { startTime: z.string() }, "final": { endTime: z.string() } }, extend: { initial: (e) => ({ initialExtension: 'ie' }), final:(e)=>({ finalExtension:"fe" }) }, transitions: { initial: ['final'], final: [], }, }) function validateTestClazzInstance(instance: any, options: { id?: string, state: string, createTime: string, unsafe?: boolean, }) { expect(instance).toBeDefined() if (!options.unsafe) expect(isZentity(instance)).toBeTruthy() expect(instance.id).toBe(options.id ?? "id") expect(instance.state).toBe(options.state) expect(instance.createTime).toBe(options.createTime) expect(instance.createdAt).toBeDefined() expect(instance.updatedAt).toBeDefined() expect(instance.copy).toBeDefined() expect(instance.validate).toBeDefined() } it("create fn should return a new instance with valid data", () => { const instance = TestClazz.create({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "initial", startTime: "start", createTime: "create", }) validateTestClazzInstance(instance, { state: 'initial', createTime: 'create', }) expect(instance.startTime).toBe("start") expect(instance.initialExtension).toBe('ie') }) it("create fn should throw with invalid data", () => { expect(() => TestClazz.create({ state: "final", startTime: "start" } as any)).toThrowError(ZodError) }) it("new fn should return a new instance with valid data", () => { const instance = TestClazz.new({ id:"id", state: "initial", startTime: "start", createTime: "create" }) validateTestClazzInstance(instance, { state: 'initial', createTime: 'create', }) expect(instance.startTime).toBe("start") expect(instance.initialExtension).toBe('ie') const finalInstance = TestClazz.new({ id:"id", state: "final", endTime: "start", createTime: "create" }) validateTestClazzInstance(finalInstance, { state: 'final', createTime: 'create', }) expect(finalInstance.endTime).toBe("start") expect((finalInstance as any).initialExtension).toBeUndefined() }) it("should have the proper base shape", () => { const shape = TestClazz.baseShape as any expect(Object.keys(shape)).toEqual(['id', 'createdAt', 'updatedAt', 'createTime']) expect(shape.id).toBeInstanceOf(ZodString) expect(shape.createTime).toBeInstanceOf(ZodString) expect(shape.createdAt).toBeInstanceOf(ZodDate) expect(shape.updatedAt).toBeInstanceOf(ZodDate) }) it("should have the proper schema", () => { const schema = TestClazz.schema expect(schema).toBeInstanceOf(ZodDiscriminatedUnion) expect(schema.discriminator).toEqual("state") expect(() => schema.parse({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "other", startTime: "start", createTime: "create", })).toThrowError(ZodError) expect(schema.parse({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "initial", startTime: "start", createTime: "create", })).toBeDefined() expect(schema.parse({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "final", endTime: "start", createTime: "create", })).toBeDefined() }) it("should have createFn for each state", () => { const initial = TestClazz.createInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), startTime: "start", createTime: "create", }) validateTestClazzInstance(initial, { state: 'initial', createTime: 'create', }) expect(initial.startTime).toBe("start") expect(initial.initialExtension).toBe('ie') const final = TestClazz.createFinal({ id: "id", createdAt: new Date(), updatedAt: new Date(), endTime: "end", createTime: "create", }) validateTestClazzInstance(final, { state: 'final', createTime: 'create', }) expect(final.endTime).toBe("end") expect((final as any).initialExtension).toBeUndefined() }) it("should have newFn for each state", () => { const initial = TestClazz.newInitial({ id:"id", startTime: "start", createTime: "create", }) validateTestClazzInstance(initial, { state: 'initial', createTime: 'create', }) expect(initial.startTime).toBe("start") expect(initial.initialExtension).toBe('ie') const final = TestClazz.newFinal({ id:"id", endTime: "end", createTime: "create", }) validateTestClazzInstance(final, { state: 'final', createTime: 'create', }) expect(final.endTime).toBe("end") expect((final as any).initialExtension).toBeUndefined() }) it("should have unsafeFn for each state, and it should allow invalid value, and should not be branded safe", () => { const initial = TestClazz.unsafeInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), createTime: undefined, startTime: "start", } as any) validateTestClazzInstance(initial, { state: 'initial', createTime: undefined, unsafe: true, } as any) expect(initial.startTime).toBe("start") expect(initial.initialExtension).toBe('ie') const final = TestClazz.unsafeFinal({ id: "id", createdAt: new Date(), updatedAt: new Date(), endTime: undefined, createTime: "create", } as any) validateTestClazzInstance(final, { state: 'final', createTime: 'create', unsafe: true, }) expect(final.endTime).toBeUndefined() expect((final as any).initialExtension).toBeUndefined() }) it("copy should work properly when using create fn", () => { let instance = TestClazz.create({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "initial", startTime: "start", createTime: "create", }) instance = instance.copy({createTime: "newCreate"}) expect(instance.createTime).toBe("newCreate") instance = instance.copy({startTime: "newStart", id: ''}) expect(instance.startTime).toBe("newStart") expect(instance.id).toBe("") }) it("copy should work properly when using new fn", () => { let instance = TestClazz.new({ id:"id", state: "initial", startTime: "start", createTime: "create" }) instance = instance.copy({createTime: "newCreate"}) expect(instance.createTime).toBe("newCreate") instance = instance.copy({startTime: "newStart", id: ''}) expect(instance.startTime).toBe("newStart") expect(instance.id).toBe("") }) it("copy should work properly when using unsafe fn", () => { let instance = TestClazz.unsafeInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), createTime: "create", startTime: "start", } as any) instance = instance.copy({createTime: "newCreate"}) expect(instance.createTime).toBe("newCreate") instance = instance.copy({startTime: "newStart", id: ''}) expect(instance.startTime).toBe("newStart") expect(instance.id).toBe("") }) it("copy should work stateful create fn", () => { let instance = TestClazz.createInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), createTime: "create", startTime: "start", }) instance = instance.copy({createTime: "newCreate"}) expect(instance.createTime).toBe("newCreate") instance = instance.copy({startTime: "newStart", id: ''}) expect(instance.startTime).toBe("newStart") expect(instance.id).toBe("") }) it("changing state with copy should not work", () => { let instance = TestClazz.createInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), createTime: "create", startTime: "start", }) expect(() => instance.copy({state: "final", endTime: ""} as any)) .toThrowError(ZodError) }) it("guard fns should work properly", () => { expect(TestClazz.isInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), createTime: "create", startTime: "start", })).toBe(false) const instance = TestClazz.newInitial({ id:"id", createTime: "create", startTime: "start", }) expect(TestClazz.isInitial(instance)).toBe(true) expect(TestClazz.isFinal(instance)).toBe(false) const finalInstance = TestClazz.newFinal({ id:"id", createTime: "create", endTime: "end", }) expect(TestClazz.isFinal(finalInstance)).toBe(true) expect(TestClazz.isInitial(finalInstance)).toBe(false) }) it("each state should have valid shape", () => { const initialShape = TestClazz.initialShape expect(Object.keys(initialShape)).toEqual(['id', 'createdAt', 'updatedAt', 'createTime', 'startTime', 'state']) expect(initialShape.startTime).toBeInstanceOf(ZodString) expect(initialShape.state).toBeInstanceOf(ZodLiteral) expect(initialShape.id).toBeInstanceOf(ZodString) expect(initialShape.createTime).toBeInstanceOf(ZodString) expect(initialShape.createdAt).toBeInstanceOf(ZodDate) expect(initialShape.updatedAt).toBeInstanceOf(ZodDate) const finalShape = TestClazz.finalShape expect(Object.keys(finalShape)).toEqual(['id', 'createdAt', 'updatedAt', 'createTime', 'endTime', 'state']) expect(finalShape.endTime).toBeInstanceOf(ZodString) expect(finalShape.state).toBeInstanceOf(ZodLiteral) expect(finalShape.id).toBeInstanceOf(ZodString) expect(finalShape.createTime).toBeInstanceOf(ZodString) expect(finalShape.createdAt).toBeInstanceOf(ZodDate) expect(finalShape.updatedAt).toBeInstanceOf(ZodDate) }) it("each state should have valid schema", () => { const initialSchema = TestClazz.initialSchema expect(initialSchema).toBeInstanceOf(ZodObject) expect(initialSchema.parse({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "initial", startTime: "start", createTime: "create", })).toBeDefined() expect(() => initialSchema.parse({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "final", endTime: "start", createTime: "create", })).toThrowError(ZodError) const finalSchema = TestClazz.finalSchema expect(finalSchema).toBeInstanceOf(ZodObject) expect(finalSchema.parse({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "final", endTime: "start", createTime: "create", })).toBeDefined() expect(() => finalSchema.parse({ id: "id", createdAt: new Date(), updatedAt: new Date(), state: "initial", startTime: "start", createTime: "create", })).toThrowError(ZodError) }) it("validate should work properly", () => { const instance = TestClazz.newInitial({ id:"id", createTime: "create", startTime: "start", }) expect(JSON.stringify(instance.validate())).toEqual(JSON.stringify(instance)) expect(instance.copy({startTime: "aa"}).validate()).toBeDefined() const unsafeInstance = TestClazz.unsafeInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), createTime: "create", startTime: "start", } as any) expect(isZentity(unsafeInstance.validate())).toBe(true) expect(() => TestClazz.unsafeInitial({ id: "id", createdAt: new Date(), updatedAt: new Date(), createTime: "create", endTime: "start", } as any).validate()).toThrowError(ZodError) }) it("transitions should be defined properly", () => { const initial1 = TestClazz.newInitial({id:"id",startTime: "start", createTime: 'create'}) expect(initial1.toFinal).toBeDefined() expect(initial1.copy({}).toFinal).toBeDefined() const initial2 = TestClazz.createInitial({ id: "", createdAt: new Date(), updatedAt: new Date(), startTime: "start", createTime: 'create' }) expect(initial2.toFinal).toBeDefined() expect(initial2.copy({}).toFinal).toBeDefined() const final = TestClazz.newFinal({id:"id",endTime: "end", createTime: 'create'}) expect((final as any).toInitial).toBeUndefined() }) it("transition of initial to final should work properly", () => { const initial = TestClazz.newInitial({id:"id",startTime: "start", createTime: 'create'}) const final = initial.toFinal({endTime: "end"}) validateTestClazzInstance(final, { state: 'final', createTime: 'create', }) }) it("should access extension after transition", ()=>{ const initial = TestClazz.newInitial({id:"id",startTime: "start", createTime: 'create'}) const final = initial.toFinal({endTime: "end"}) expect(final.finalExtension).toBe('fe') }) it("toPlain should return entity core props and state data only without extension", () => { const initial = TestClazz.newInitial({id:"id",startTime: "start", createTime: 'create'}) const plain = initial.toPlain() expect(plain).toEqual({ id: initial.id, createdAt: initial.createdAt, updatedAt: initial.updatedAt, createTime: initial.createTime, startTime: initial.startTime, state: initial.state, }) expect((plain as any).initialExtension).toBeUndefined() }) })