Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 22 additions & 0 deletions docs/content/2.models/3.inserting-and-updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ You can also do this in a single command by using `create()` to create a new mod
const pet = await Pet.create({ name: "Zuko", type: "cat" });
```

## Bulk Inserting

Pass an array to `create()` to insert multiple records at once. The records are inserted with a single query, and an array of saved model instances is returned.

```ts
const pets = await Pet.create([
{ name: "Zuko", type: "cat" },
{ name: "Appa", type: "dog" },
]);
```

Default attributes and mutators are applied to each record, and the `saving`/`creating` events are dispatched for each model before the insert, followed by `created`/`saved` after.

You can also call `createMany()` directly, which always takes an array:

```ts
const pets = await Pet.createMany([
{ name: "Zuko", type: "cat" },
{ name: "Appa", type: "dog" },
]);
```

## Setting Attributes

You can set attributes directly on a model instance using property accessors and then save the model instance.
Expand Down
52 changes: 51 additions & 1 deletion src/model/StaticForwarder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,62 @@ export abstract class StaticForwarder {
static async create<T extends AnyModelConstructor>(
this: T,
attributes: ConstructorParameters<T>[0],
): Promise<InstanceType<T>> {
): Promise<InstanceType<T>>;

static async create<T extends AnyModelConstructor>(
this: T,
attributes: ConstructorParameters<T>[0][],
): Promise<InstanceType<T>[]>;

static async create<T extends AnyModelConstructor>(
this: T,
attributes: ConstructorParameters<T>[0] | ConstructorParameters<T>[0][],
): Promise<InstanceType<T> | InstanceType<T>[]> {
if (Array.isArray(attributes)) {
return (this as any).createMany(attributes);
}
Comment on lines +179 to +181
const instance = new (this as any)(attributes);
await instance.save();
return instance;
}

static async createMany<T extends AnyModelConstructor>(
this: T,
attributes: ConstructorParameters<T>[0][],
): Promise<InstanceType<T>[]> {
const instances = attributes.map((attrs) => new (this as any)(attrs)) as InstanceType<T>[];
if (instances.length === 0) {
return instances;
}

// Dispatch "saving" and "creating" events for each instance before inserting into the database
for (const instance of instances) {
await instance.dispatchEvent("saving");
await instance.dispatchEvent("creating");
}

const { db, table } = instances[0];
const rows = await (db as any)
.insertInto(table)
.values(instances.map((instance) => instance.getRawAttributes()))
.returningAll()
.execute();

Comment on lines +202 to +208
// The isntances have been saved to the database, so we should dispatch the "created" and "saved" events for each instance and set their attributes accordingly
Comment thread
Copilot marked this conversation as resolved.
Outdated
for (const [index, instance] of instances.entries()) {
const row = rows[index];
if (row) {
instance.setRawAttributes(row);
instance.originalAttributes = { ...instance.attributes };
instance.exists = true;
await instance.dispatchEvent("created");
}
await instance.dispatchEvent("saved");
}

return instances;
}

static select<T extends AnyModelConstructor, const K extends Selection<InstanceType<T>>>(
this: T,
columns: K[] | ((eb: ExpressionBuilder<ExtractDB<InstanceType<T>>, ExtractTB<InstanceType<T>>>) => K[]),
Expand Down
97 changes: 97 additions & 0 deletions test/tests/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,103 @@ describe("create", () => {
void Pet.create({ name: "Fluffy", type: "cat", invalidAttribute: "oops" });
}
});

it("should bulk insert an array of records in a single query", async () => {
resetQueryCount();

const pets = await Pet.create([
{ name: "Bulk One", type: "dog", counter: 1 },
{ name: "Bulk Two", type: "cat", counter: 2 },
{ name: "Bulk Three", type: "bird", counter: 3 },
]);

expect(getQueryCount()).toBe(1);
expect(pets).toHaveLength(3);

for (const pet of pets) {
expect(pet).toBeInstanceOf(Pet);
expect(pet.attributes.id).toBeGreaterThan(0);
expect(pet.exists).toBe(true);
expect(pet.isDirty()).toBe(false);
}

expect(pets.map((pet) => pet.attributes.name)).toEqual(["Bulk One", "Bulk Two", "Bulk Three"]);

const fetched = await Pet.findOrFail(pets.map((pet) => pet.attributes.id));
expect(fetched).toHaveLength(3);

for (const pet of pets) {
await pet.delete();
}
});

it("should apply default attributes and mutators when bulk inserting", async () => {
const pets = await SuperPet.create([
{ name: "quiet", type: "cat" },
{ name: "loud", type: "dog" },
]);

expect(pets.map((pet) => pet.attributes.name)).toEqual(["QUIET", "LOUD"]);

for (const pet of pets) {
await pet.delete();
}
});

it("should dispatch lifecycle events for each model when bulk inserting", async () => {
const eventNames: string[] = [];

class EventedPet extends defineModel({
db,
table: "pets",
attributes: {
counter: { default: 0 },
},
events: {
saving: () => void eventNames.push("saving"),
creating: () => void eventNames.push("creating"),
created: () => void eventNames.push("created"),
saved: () => void eventNames.push("saved"),
},
}) {}

const pets = await EventedPet.create([
{ name: "Evented One", type: "cat" },
{ name: "Evented Two", type: "dog" },
]);

expect(eventNames).toEqual(["saving", "creating", "saving", "creating", "created", "saved", "created", "saved"]);

for (const pet of pets) {
await pet.delete();
}
});

it("should return an empty array when bulk inserting an empty array", async () => {
const pets = await Pet.create([]);
expect(pets).toEqual([]);
});

it("should enforce type safety on bulk insert attributes", () => {
if (false) {
void Pet.create([{ name: "Fluffy", type: "cat" }]).then((pets) => {
void pets.map((pet) => pet.attributes.name);
});

void Pet.create({ name: "Fluffy", type: "cat" }).then((pet) => {
void pet.attributes.name;
});

// @ts-expect-error - type is required on every record
void Pet.create([{ name: "Fluffy", type: "cat" }, { name: "Missing" }]);

// @ts-expect-error - name must be a string
void Pet.create([{ name: 123, type: "cat" }]);

// @ts-expect-error - unknown attribute
void Pet.create([{ name: "Fluffy", type: "cat", invalidAttribute: "oops" }]);
}
});
});

describe("lifecycle events", () => {
Expand Down
Loading