Skip to content

Commit

Permalink
fix: updatedAt field not updating on mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
davenewza committed Oct 30, 2023
1 parent 54ca61d commit 2f2076f
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 5 deletions.
24 changes: 24 additions & 0 deletions integration/testdata/built_in_actions/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,26 @@ test("update action", async () => {
expect(updatedPost.subTitle).toEqual("opm");
});

test("update action - updatedAt set", async () => {
const post = await models.post.create({
title: "watermelon",
subTitle: "opm",
});

expect(post.updatedAt).not.toBeNull();
expect(post.updatedAt).toEqual(post.createdAt);

await delay(100);

const updatedPost = await actions.updatePost({
where: { id: post.id },
values: { title: "big watermelon" },
});

expect(updatedPost.updatedAt.valueOf()).toBeGreaterThanOrEqual(post.createdAt.valueOf() + 100);
expect(updatedPost.updatedAt.valueOf()).toBeLessThan(post.createdAt.valueOf() + 1000);
});

test("update action - explicit set / args", async () => {
const post = await models.post.create({
title: "watermelon",
Expand All @@ -260,3 +280,7 @@ test("update action - explicit set / args", async () => {

expect(updatedPost.title).toEqual("a really cool title");
});

function delay(ms: number) {
return new Promise( resolve => setTimeout(resolve, ms) );
}
41 changes: 41 additions & 0 deletions integration/testdata/with_custom_function_hooks/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,47 @@ describe("write hooks", () => {
})
).toHaveAuthorizationError();
});


test("update action - updatedAt set", async () => {
const identity = await models.identity.create({
email: "[email protected]",
});
const name = "Alice";

const person = await models.person.create({
sex: Sex.Female,
title: name,
});

expect(person.updatedAt).not.toBeNull();
expect(person.updatedAt).toEqual(person.createdAt);

await delay(100);

const record = await actions
.withIdentity(identity)
.updatePersonWithBeforeWrite({
where: { id: person.id },
values: {
title: "Alice",
sex: Sex.Female,
},
});
console.log(person);
console.log(record);

const person2 = await models.person.findOne({id: person.id});
console.log(person2);


expect(record.updatedAt.valueOf()).toBeGreaterThanOrEqual(person.createdAt.valueOf() + 100);
expect(record.updatedAt.valueOf()).toBeLessThan(person.createdAt.valueOf() + 1000);
});

function delay(ms: number) {
return new Promise( resolve => setTimeout(resolve, ms) );
}
});

describe("query hooks", () => {
Expand Down
11 changes: 11 additions & 0 deletions migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ var (

//go:embed set_trace_id.sql
setTraceId string

//go:embed set_updated_at.sql
setUpdatedAt string
)

type DatabaseChange struct {
Expand Down Expand Up @@ -95,6 +98,8 @@ func (m *Migrations) Apply(ctx context.Context) error {
sql.WriteString("\n")
sql.WriteString(setTraceId)
sql.WriteString("\n")
sql.WriteString(setUpdatedAt)
sql.WriteString("\n")

sql.WriteString("CREATE TABLE IF NOT EXISTS keel_schema ( schema TEXT NOT NULL );\n")
sql.WriteString("DELETE FROM keel_schema;\n")
Expand Down Expand Up @@ -223,6 +228,12 @@ func New(ctx context.Context, schema *proto.Schema, database db.Database) (*Migr
return nil, err
}
statements = append(statements, stmt)

stmt, err = createUpdatedAtTriggerStmts(triggers, schema, model)
if err != nil {
return nil, err
}
statements = append(statements, stmt)
}
}

Expand Down
6 changes: 6 additions & 0 deletions migrations/set_updated_at.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END
$$ LANGUAGE plpgsql;
15 changes: 15 additions & 0 deletions migrations/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,18 @@ func createAuditTriggerStmts(triggers []*TriggerRow, schema *proto.Schema, model

return strings.Join(statements, "\n"), nil
}

// createUpdatedAtTriggerStmts generates the CREATE TRIGGER statements for automatically updating each model's updatedAt column.
// Only creates a trigger if the trigger does not already exist in the database.
func createUpdatedAtTriggerStmts(triggers []*TriggerRow, schema *proto.Schema, model *proto.Model) (string, error) {
modelLower := casing.ToSnake(model.Name)
statements := []string{}

updatedAt := fmt.Sprintf("%s_updated_at", modelLower)
if _, found := lo.Find(triggers, func(t *TriggerRow) bool { return t.TriggerName == updatedAt && t.TableName == modelLower }); !found {
statements = append(statements, fmt.Sprintf(
`CREATE TRIGGER %s BEFORE UPDATE ON %s FOR EACH ROW EXECUTE PROCEDURE set_updated_at();`, updatedAt, Identifier(model.Name)))
}

return strings.Join(statements, "\n"), nil
}
2 changes: 2 additions & 0 deletions migrations/testdata/composite_unique_initial.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ ALTER TABLE "person" ADD CONSTRAINT person_thing_a_thing_b_thing_c_udx UNIQUE ("
CREATE TRIGGER person_create AFTER INSERT ON "person" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_update AFTER UPDATE ON "person" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_delete AFTER DELETE ON "person" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_updated_at BEFORE UPDATE ON "person" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
CREATE TRIGGER identity_create AFTER INSERT ON "identity" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_update AFTER UPDATE ON "identity" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_delete AFTER DELETE ON "identity" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_updated_at BEFORE UPDATE ON "identity" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();

===

Expand Down
4 changes: 3 additions & 1 deletion migrations/testdata/default_value_initial.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ model Person {
===

CREATE TABLE "identity" (
"email" TEXT,
"email" TEXT,pauthen
"email_verified" BOOL NOT NULL DEFAULT false,
"password" TEXT,
"external_id" TEXT,
Expand Down Expand Up @@ -60,9 +60,11 @@ ALTER TABLE "person" ADD CONSTRAINT person_id_pkey PRIMARY KEY ("id");
CREATE TRIGGER person_create AFTER INSERT ON "person" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_update AFTER UPDATE ON "person" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_delete AFTER DELETE ON "person" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_updated_at BEFORE UPDATE ON "person" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
CREATE TRIGGER identity_create AFTER INSERT ON "identity" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_update AFTER UPDATE ON "identity" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_delete AFTER DELETE ON "identity" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_updated_at BEFORE UPDATE ON "identity" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();

===

Expand Down
2 changes: 2 additions & 0 deletions migrations/testdata/initial.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ ALTER TABLE "person" ADD CONSTRAINT person_id_pkey PRIMARY KEY ("id");
CREATE TRIGGER person_create AFTER INSERT ON "person" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_update AFTER UPDATE ON "person" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_delete AFTER DELETE ON "person" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_updated_at BEFORE UPDATE ON "person" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
CREATE TRIGGER identity_create AFTER INSERT ON "identity" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_update AFTER UPDATE ON "identity" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_delete AFTER DELETE ON "identity" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_updated_at BEFORE UPDATE ON "identity" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();

===

Expand Down
1 change: 1 addition & 0 deletions migrations/testdata/model_added.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ ALTER TABLE "animal" ADD FOREIGN KEY ("human_friend_id") REFERENCES "person"("id
CREATE TRIGGER animal_create AFTER INSERT ON "animal" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER animal_update AFTER UPDATE ON "animal" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER animal_delete AFTER DELETE ON "animal" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER animal_updated_at BEFORE UPDATE ON "animal" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();

===

Expand Down
2 changes: 2 additions & 0 deletions migrations/testdata/simple_field_types.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ ALTER TABLE "person" ADD CONSTRAINT person_id_pkey PRIMARY KEY ("id");
CREATE TRIGGER person_create AFTER INSERT ON "person" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_update AFTER UPDATE ON "person" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_delete AFTER DELETE ON "person" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER person_updated_at BEFORE UPDATE ON "person" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
CREATE TRIGGER identity_create AFTER INSERT ON "identity" REFERENCING NEW TABLE AS new_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_update AFTER UPDATE ON "identity" REFERENCING NEW TABLE AS new_table OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_delete AFTER DELETE ON "identity" REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE PROCEDURE process_audit();
CREATE TRIGGER identity_updated_at BEFORE UPDATE ON "identity" FOR EACH ROW EXECUTE PROCEDURE set_updated_at();

===

Expand Down
19 changes: 15 additions & 4 deletions packages/testing-runtime/src/Executor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class Executor {
method: "POST",
body: JSON.stringify(params),
headers,
}).then((r) => {
}).then(async (r) => {
if (r.status !== 200) {
// For non-200 first read the response as text
return r.text().then((t) => {
Expand All @@ -101,9 +101,20 @@ export class Executor {
});
}

if (this._parseJsonResult) {
return r.json();
}
return r.text().then((t) => {
if (this._parseJsonResult) {
return JSON.parse(t, reviver);
}
});
});
}
}

const dateFormat = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:(\d{2}(?:\.\d*))Z$/;

function reviver(key, value) {
if (typeof value === "string" && dateFormat.test(value)) {
return new Date(value);
}
return value;
}
4 changes: 4 additions & 0 deletions runtime/actions/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"sort"
"strings"
"time"

"github.com/samber/lo"
"github.com/teamkeel/keel/casing"
Expand Down Expand Up @@ -811,6 +812,9 @@ func orderGraphNodes(graph *Row) []*Row {

// Generates an executable UPDATE statement with the list of arguments.
func (query *QueryBuilder) UpdateStatement() *Statement {
// Implicitly update the updatedAt column for any update to the model
query.AddWriteValue(Field(parser.ImplicitFieldNameUpdatedAt), Value(time.Now().UTC()))

queryFilters := query.filters

joins := ""
Expand Down

0 comments on commit 2f2076f

Please sign in to comment.