Skip to content

Commit

Permalink
feat: computed fields and many to one relationships (#1704)
Browse files Browse the repository at this point in the history
  • Loading branch information
davenewza committed Jan 30, 2025
1 parent f1abe8b commit 9cb629b
Show file tree
Hide file tree
Showing 30 changed files with 733 additions and 117 deletions.
4 changes: 2 additions & 2 deletions expressions/resolve/field_lookups.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ type fieldLookupsGen struct {
anyNull bool
}

func (v *fieldLookupsGen) StartCondition(parenthesis bool) error {
func (v *fieldLookupsGen) StartTerm(parenthesis bool) error {
return nil
}

func (v *fieldLookupsGen) EndCondition(parenthesis bool) error {
func (v *fieldLookupsGen) EndTerm(parenthesis bool) error {
if v.operator == operators.Equals && !v.anyNull {
if v.operands != nil {
if len(v.uniqueLookupGroups) == 0 {
Expand Down
4 changes: 2 additions & 2 deletions expressions/resolve/ident.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ type identGen struct {
ident *parser.ExpressionIdent
}

func (v *identGen) StartCondition(parenthesis bool) error {
func (v *identGen) StartTerm(parenthesis bool) error {
return nil
}

func (v *identGen) EndCondition(parenthesis bool) error {
func (v *identGen) EndTerm(parenthesis bool) error {
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions expressions/resolve/ident_array.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ type identArrayGen struct {
idents []*parser.ExpressionIdent
}

func (v *identArrayGen) StartCondition(parenthesis bool) error {
func (v *identArrayGen) StartTerm(parenthesis bool) error {
return nil
}

func (v *identArrayGen) EndCondition(parenthesis bool) error {
func (v *identArrayGen) EndTerm(parenthesis bool) error {
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions expressions/resolve/operands.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ type operandsResolver struct {
idents []*parser.ExpressionIdent
}

func (v *operandsResolver) StartCondition(parenthesis bool) error {
func (v *operandsResolver) StartTerm(parenthesis bool) error {
return nil
}

func (v *operandsResolver) EndCondition(parenthesis bool) error {
func (v *operandsResolver) EndTerm(parenthesis bool) error {
return nil
}

Expand Down
16 changes: 8 additions & 8 deletions expressions/resolve/visitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ var (
)

type Visitor[T any] interface {
// StartCondition is called when a new condition is visited
StartCondition(nested bool) error
// EndCondition is called when a condition is finished
EndCondition(nested bool) error
// StartTerm is called when a new term is visited
StartTerm(nested bool) error
// EndTerm is called when a term is finished
EndTerm(nested bool) error
// VisitAnd is called when an 'and' operator is visited between conditions
VisitAnd() error
// VisitAnd is called when an 'or' operator is visited between conditions
Expand Down Expand Up @@ -87,7 +87,7 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, nested bool, inBinary bool) erro
switch expr.ExprKind.(type) {
case *exprpb.Expr_ConstExpr, *exprpb.Expr_ListExpr, *exprpb.Expr_SelectExpr, *exprpb.Expr_IdentExpr:
if !inBinary {
err := w.visitor.StartCondition(false)
err := w.visitor.StartTerm(false)
if err != nil {
return err
}
Expand All @@ -96,7 +96,7 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, nested bool, inBinary bool) erro

switch expr.ExprKind.(type) {
case *exprpb.Expr_CallExpr:
err = w.visitor.StartCondition(nested)
err = w.visitor.StartTerm(nested)
if err != nil {
return err
}
Expand All @@ -106,7 +106,7 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, nested bool, inBinary bool) erro
return err
}

err = w.visitor.EndCondition(nested)
err = w.visitor.EndTerm(nested)
if err != nil {
return err
}
Expand Down Expand Up @@ -137,7 +137,7 @@ func (w *CelVisitor[T]) eval(expr *exprpb.Expr, nested bool, inBinary bool) erro
switch expr.ExprKind.(type) {
case *exprpb.Expr_ConstExpr, *exprpb.Expr_ListExpr, *exprpb.Expr_SelectExpr, *exprpb.Expr_IdentExpr:
if !inBinary {
err := w.visitor.EndCondition(false)
err := w.visitor.EndTerm(false)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion integration/testdata/computed_fields/schema.keel
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ model ComputedDepends {
fields {
price Decimal
quantity Number
totalWithDiscount Decimal? @computed(computedDepends.totalWithShipping - (computedDepends.totalWithShipping / 100 * 10))
totalWithDiscount Decimal? @computed(computedDepends.totalWithShipping - (computedDepends.total / 100 * 10))
totalWithShipping Decimal? @computed(computedDepends.total + 5)
total Decimal? @computed(computedDepends.quantity * computedDepends.price)
}
Expand Down
16 changes: 8 additions & 8 deletions integration/testdata/computed_fields/tests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,21 @@ test("computed fields - with dependencies", async () => {
const item = await models.computedDepends.create({ price: 5, quantity: 2 });
expect(item.total).toEqual(10);
expect(item.totalWithShipping).toEqual(15);
expect(item.totalWithDiscount).toEqual(13.5);
expect(item.totalWithDiscount).toEqual(14);

const updatedQty = await models.computedDepends.update(
{ id: item.id },
{ quantity: 10 }
{ quantity: 11 }
);
expect(updatedQty.total).toEqual(50);
expect(updatedQty.totalWithShipping).toEqual(55);
expect(updatedQty.totalWithDiscount).toEqual(49.5);
expect(updatedQty.total).toEqual(55);
expect(updatedQty.totalWithShipping).toEqual(60);
expect(updatedQty.totalWithDiscount).toEqual(54.5);

const updatePrice = await models.computedDepends.update(
{ id: item.id },
{ price: 8 }
);
expect(updatePrice.total).toEqual(80);
expect(updatePrice.totalWithShipping).toEqual(85);
expect(updatePrice.totalWithDiscount).toEqual(76.5);
expect(updatePrice.total).toEqual(88);
expect(updatePrice.totalWithShipping).toEqual(93);
expect(updatePrice.totalWithDiscount).toEqual(84.2);
});
39 changes: 39 additions & 0 deletions integration/testdata/computed_fields_many_to_one/schema.keel
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
model Item {
fields {
product Product
quantity Number
total Decimal? @computed(item.quantity * item.product.standardPrice + item.product.agent.commission)
totalWithShipping Decimal? @computed(item.total + 5)
totalWithDiscount Decimal? @computed(item.totalWithShipping - (item.total / 100 * 10))
}
actions {
create createItem() with (quantity, product.standardPrice, product.agent.commission) {
@permission(expression: true)
}
}
}

model Product {
fields {
standardPrice Decimal
items Item[]
agent Agent?
}
actions {
create createProduct() with (standardPrice, items.quantity, agent.commission) {
@permission(expression: true)
}
}
}

model Agent {
fields {
commission Decimal
products Product[]
}
actions {
create createAgent() with (commission, products.standardPrice, products.items.quantity){
@permission(expression: true)
}
}
}
184 changes: 184 additions & 0 deletions integration/testdata/computed_fields_many_to_one/tests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { test, expect, beforeEach } from "vitest";
import { models, resetDatabase, actions } from "@teamkeel/testing";

beforeEach(resetDatabase);

test("computed fields - many to one", async () => {
const agent = await models.agent.create({ commission: 2.5 });
const product = await models.product.create({
standardPrice: 5,
agentId: agent.id,
});
const item = await models.item.create({
productId: product.id,
quantity: 2,
});
expect(item.total).toEqual(12.5);
expect(item.totalWithShipping).toEqual(17.5);
expect(item.totalWithDiscount).toEqual(16.25);

await models.product.update({ id: product.id }, { standardPrice: 10 });
const getUpdatedPrice = await models.item.findOne({ id: item.id });
expect(getUpdatedPrice!.total).toEqual(22.5);
expect(getUpdatedPrice!.totalWithShipping).toEqual(27.5);
expect(getUpdatedPrice!.totalWithDiscount).toEqual(25.25);

const updateQuantity = await models.item.update(
{ id: item.id },
{ quantity: 3 }
);
expect(updateQuantity.total).toEqual(32.5);
expect(updateQuantity.totalWithShipping).toEqual(37.5);
expect(updateQuantity.totalWithDiscount).toEqual(34.25);

await models.agent.update({ id: agent.id }, { commission: 10 });
const getUpdatedPrice2 = await models.item.findOne({ id: item.id });
expect(getUpdatedPrice2!.total).toEqual(40);
expect(getUpdatedPrice2!.totalWithShipping).toEqual(45);
expect(getUpdatedPrice2!.totalWithDiscount).toEqual(41);

const newAgent = await models.agent.create({ commission: 0.75 });
await models.product.update({ id: product.id }, { agentId: newAgent.id });
const getUpdatedPrice3 = await models.item.findOne({ id: item.id });
expect(getUpdatedPrice3!.total).toEqual(30.75);
expect(getUpdatedPrice3!.totalWithShipping).toEqual(35.75);
expect(getUpdatedPrice3!.totalWithDiscount).toEqual(32.675);

const newProduct = await models.product.create({
standardPrice: 8,
agentId: agent.id,
});
const getUpdatedPrice4 = await models.item.update(
{ id: item.id },
{ productId: newProduct.id }
);
expect(getUpdatedPrice4!.total).toEqual(34);
expect(getUpdatedPrice4!.totalWithShipping).toEqual(39);
expect(getUpdatedPrice4!.totalWithDiscount).toEqual(35.6);
});

test("computed fields - many to one cascading update", async () => {
const agent1 = await models.agent.create({ commission: 2.5 });
const agent2 = await models.agent.create({ commission: 0.75 });

const product1 = await models.product.create({
standardPrice: 5,
agentId: agent1.id,
});
const item1 = await models.item.create({
productId: product1.id,
quantity: 2,
});
expect(item1.total).toEqual(12.5);

const product2 = await models.product.create({
standardPrice: 7,
agentId: agent1.id,
});
const item2 = await models.item.create({
productId: product2.id,
quantity: 2,
});
expect(item2.total).toEqual(16.5);

const product3 = await models.product.create({
standardPrice: 9,
agentId: agent2.id,
});
const item3 = await models.item.create({
productId: product3.id,
quantity: 2,
});
expect(item3.total).toEqual(18.75);

const product4 = await models.product.create({
standardPrice: 10,
agentId: agent2.id,
});
const item4 = await models.item.create({
productId: product4.id,
quantity: 2,
});
expect(item4.total).toEqual(20.75);
const item5 = await models.item.create({
productId: product4.id,
quantity: 3,
});
expect(item5.total).toEqual(30.75);

await models.agent.update({ id: agent2.id }, { commission: 1 });

const getItem1 = await models.item.findOne({ id: item1.id });
expect(getItem1!.total).toEqual(12.5);

const getItem2 = await models.item.findOne({ id: item2.id });
expect(getItem2!.total).toEqual(16.5);

const getItem3 = await models.item.findOne({ id: item3.id });
expect(getItem3!.total).toEqual(19);

const getItem4 = await models.item.findOne({ id: item4.id });
expect(getItem4!.total).toEqual(21);

const getItem5 = await models.item.findOne({ id: item5.id });
expect(getItem5!.total).toEqual(31);
});

test("computed fields - many to one with nested create", async () => {
const item = await actions.createItem({
quantity: 2,
product: {
standardPrice: 5,
agent: {
commission: 2.5,
},
},
});

expect(item.total).toEqual(12.5);
});

test("computed fields - many to one with nested create from related model", async () => {
const product = await actions.createProduct({
standardPrice: 5,
items: [
{
quantity: 2,
},
{
quantity: 5,
},
],
agent: {
commission: 2.5,
},
});

const items = await models.item.findMany({
where: { productId: product.id },
orderBy: { total: "asc" },
});

expect(items[0].total).toEqual(12.5);
expect(items[1].total).toEqual(27.5);
expect(items).length(2);
});

test("computed fields - many to one with nested create from nested related model", async () => {
const agent = await actions.createAgent({
commission: 2.5,
products: [{ standardPrice: 5, items: [{ quantity: 2 }, { quantity: 5 }] }],
});
const products = await models.product.findMany({
where: { agentId: agent.id },
});

const items = await models.item.findMany({
where: { productId: products[0].id },
orderBy: { total: "asc" },
});

expect(items[0].total).toEqual(12.5);
expect(items[1].total).toEqual(27.5);
expect(items).length(2);
});
2 changes: 1 addition & 1 deletion migrations/computed_functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ FROM
WHERE
routine_type = 'FUNCTION'
AND
routine_schema = 'public' AND routine_name LIKE '%__computed';
routine_schema = 'public' AND routine_name LIKE '%__comp' OR routine_name LIKE '%__comp_dep';
Loading

0 comments on commit 9cb629b

Please sign in to comment.