Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,13 @@ Add global hooks for pre/post save and post load operations by passing a third a

```typescript
@Persistence(adapter, fieldSpecs, {
preSave: async (context, model) => {
preSave: async (context, model, type) => {
// Modify model before saving
// type will be "insert" or "update"
},
postSave: async (context, model) => {
postSave: async (context, model, type) => {
// Handle post-save operations
// type will be "insert" or "update"
},
postLoad: async (context, model) => {
// Process model after loading
Expand Down
17 changes: 14 additions & 3 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,21 @@ export class Model<T extends ModelAttributes> {

const context = await adapter.getContext();

let type: "insert" | "update";
if (this.persisted) {
type = "update";
} else {
type = "insert";
}

if (globalSpec?.preSave) {
await globalSpec.preSave(context, this);
await globalSpec.preSave(context, this, type);
}

// pre-save hook may have changed additional fields
fields = this.getChangedFields().filter(field => fieldSpecs?.[field]?.persist !== false);
fields = this.getChangedFields().filter(
(field) => fieldSpecs?.[field]?.persist !== false
);
if (this.persisted && fields.length === 0) return this;

const data: Partial<T> = {};
Expand All @@ -252,16 +261,18 @@ export class Model<T extends ModelAttributes> {
if (this.persisted) {
const { success } = await adapter.update(context, this, data);
if (!success) throw new Error("Failed to save model to database");
type = "update";
} else {
const { success } = await adapter.insert(context, this, data);
if (!success) throw new Error("Failed to save model to database");
type = "insert";
}

this._persisted = true;
this.clearChangedFields();

if (globalSpec?.postSave) {
await globalSpec.postSave(context, this);
await globalSpec.postSave(context, this, type);
}
return this;
}
Expand Down
12 changes: 10 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,16 @@ export type FieldSpecs<T> = {
}

export interface GlobalSpec<T extends ModelAttributes> {
preSave?: (context: any, model: Model<T>) => Promise<void>;
postSave?: (context: any, model: Model<T>) => Promise<void>;
preSave?: (
context: any,
model: Model<T>,
type: "insert" | "update"
) => Promise<void>;
postSave?: (
context: any,
model: Model<T>,
type: "insert" | "update"
) => Promise<void>;
postLoad?: (context: any, model: Model<T>) => Promise<void>;
postDelete?: (context: any, model: Model<T>) => Promise<void>;
}
Expand Down
70 changes: 35 additions & 35 deletions test/advanced-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,30 @@ const dateEncoder: ValueEncoder<Date, string> = {
decode: (value) => new Date(value)
};

let preSaveCalled = false;
let postSaveCalled = false;
let postLoadCalled = false;
let postDeleteCalled = false;
let preSaveCalled: any = false;
let postSaveCalled: any = false;
let postLoadCalled: any = false;
let postDeleteCalled: any = false;

@Persistence<ComplexAttrs>(
createSqliteAdapter({
dbName: "test_db2",
tableName: "complex_models",
primaryKeyField: "id"
primaryKeyField: "id",
}),
{
secretKey: { persist: false },
metadata: { encoder: jsonEncoder },
lastUpdated: { encoder: dateEncoder }
lastUpdated: { encoder: dateEncoder },
},
{
preSave: async (_context, model) => {
preSaveCalled = true;
preSave: async (_context, model, type) => {
preSaveCalled = type;
const count = model.get("count");
model.set("count", count + 1);
},
postSave: async (_context, _model) => {
postSaveCalled = true;
postSave: async (_context, _model, type) => {
postSaveCalled = type;
},
postLoad: async (_context, _model) => {
postLoadCalled = true;
Expand All @@ -57,16 +57,16 @@ let postDeleteCalled = false;
},
}
)
class ComplexModel extends Model<ComplexAttrs> { }
class ComplexModel extends Model<ComplexAttrs> {}

describe('Advanced Model Features', () => {
const dbPath = '/tmp/test_db2.db';
describe("Advanced Model Features", () => {
const dbPath = "/tmp/test_db2.db";

beforeAll(async () => {
const adapter = createSqliteAdapter({
dbName: "test_db2",
tableName: "complex_models",
primaryKeyField: "id"
primaryKeyField: "id",
});
const ctx = await adapter.getContext();
await ctx.db.run(
Expand All @@ -87,13 +87,13 @@ describe('Advanced Model Features', () => {
postDeleteCalled = false;
});

it('should handle non-persisted fields', async () => {
it("should handle non-persisted fields", async () => {
const model = new ComplexModel({
name: "Test Model",
metadata: { foo: "bar" },
secretKey: "secret123",
count: 1,
lastUpdated: new Date()
lastUpdated: new Date(),
});

await model.save();
Expand All @@ -103,14 +103,14 @@ describe('Advanced Model Features', () => {
expect(loaded?.get("name")).toBe("Test Model");
});

it('should encode and decode JSON fields', async () => {
it("should encode and decode JSON fields", async () => {
const metadata = { foo: "bar", num: 123, nested: { test: true } };
const model = new ComplexModel({
name: "JSON Test",
metadata,
secretKey: "secret123",
count: 1,
lastUpdated: new Date()
lastUpdated: new Date(),
});

await model.save();
Expand All @@ -119,14 +119,14 @@ describe('Advanced Model Features', () => {
expect(loaded?.get("metadata")).toEqual(metadata);
});

it('should encode and decode Date fields', async () => {
it("should encode and decode Date fields", async () => {
const date = new Date();
const model = new ComplexModel({
name: "Date Test",
metadata: {},
secretKey: "secret123",
count: 1,
lastUpdated: date
lastUpdated: date,
});

await model.save();
Expand All @@ -136,18 +136,18 @@ describe('Advanced Model Features', () => {
expect(loaded?.get("lastUpdated").getTime()).toBe(date.getTime());
});

it('should call lifecycle hooks', async () => {
it("should call lifecycle hooks", async () => {
const model = new ComplexModel({
name: "Hooks Test",
metadata: {},
secretKey: "secret123",
count: 1,
lastUpdated: new Date()
lastUpdated: new Date(),
});

await model.save();
expect(preSaveCalled).toBe(true);
expect(postSaveCalled).toBe(true);
expect(preSaveCalled).toBe("insert");
expect(postSaveCalled).toBe("insert");
expect(model.get("count")).toBe(2);
preSaveCalled = false;
postSaveCalled = false;
Expand All @@ -161,13 +161,13 @@ describe('Advanced Model Features', () => {
expect(postLoadCalled).toBe(true);
});

it('should only encode changed fields on update', async () => {
it("should only encode changed fields on update", async () => {
const model = new ComplexModel({
name: "Update Test",
metadata: { initial: true },
count: 1,
secretKey: "secret123",
lastUpdated: new Date()
lastUpdated: new Date(),
});

await model.save();
Expand All @@ -183,11 +183,11 @@ describe('Advanced Model Features', () => {
const loaded = await ComplexModel.get(model.get("id"));
expect(loaded?.get("name")).toBe("Updated Name");
expect(loaded?.get("metadata")).toEqual({ initial: true });
expect(preSaveCalled).toBe(true);
expect(postSaveCalled).toBe(true);
expect(preSaveCalled).toBe("update");
expect(postSaveCalled).toBe("update");
});

it('should retrieve all models and handle encoders', async () => {
it("should retrieve all models and handle encoders", async () => {
const date1 = new Date();
const date2 = new Date();

Expand All @@ -196,15 +196,15 @@ describe('Advanced Model Features', () => {
metadata: { type: "test1" },
secretKey: "secret1",
count: 1,
lastUpdated: date1
lastUpdated: date1,
}).save();

await new ComplexModel({
name: "Model 2",
metadata: { type: "test2" },
secretKey: "secret2",
count: 1,
lastUpdated: date2
lastUpdated: date2,
}).save();

const allModels = await ComplexModel.all();
Expand All @@ -214,14 +214,14 @@ describe('Advanced Model Features', () => {
expect(postLoadCalled).toBe(true);
});

it('should retrieve models by criteria with encoded fields', async () => {
it("should retrieve models by criteria with encoded fields", async () => {
const date = new Date();
await new ComplexModel({
name: "Search Test",
metadata: { searchKey: "findMe" },
secretKey: "secret",
count: 1,
lastUpdated: date
lastUpdated: date,
}).save();

const foundModel = await ComplexModel.getBy({ name: "Search Test" });
Expand All @@ -231,13 +231,13 @@ describe('Advanced Model Features', () => {
expect(postLoadCalled).toBe(true);
});

it('should delete a model and handle lifecycle hooks', async () => {
it("should delete a model and handle lifecycle hooks", async () => {
const model = new ComplexModel({
name: "Delete Test",
metadata: { toDelete: true },
secretKey: "secret123",
count: 1,
lastUpdated: new Date()
lastUpdated: new Date(),
});
await model.save();
const id = model.get("id");
Expand Down
Loading