Nested create with Drizzle relations

Drizzle nested create in NestJS. Write a row and its related rows in one transaction, return writes with relations, and attach or sync links.

Drizzle nested create in NestJS works through writableRelations: list a relation there, put the related rows in the create body under the relation's name, and the package writes the parent and its children in one transaction. Every write method also takes options.relations and returns its rows with those relations loaded, and attach, detach and sync change many-to-many links with set-based statements.

Define the relations first

Every example here uses relations declared in forFeature config. If you have not set those up, start with Relations. Nested create needs RETURNING, so it runs on PostgreSQL only.

How do I get a created row back with its relations?

Pass relations (and counts, see Aggregates) as you would on find:

posts.create.ts
const post = await posts.create(
  { title: 'Hello', author_id: 2, status: 'draft' },
  { relations: ['author', 'tags'] },
);
post.author.name; // 'Bob'
post.tags;        // []

That is one INSERT plus one query per relation. A mass write loads the relations of every row it wrote in one query per relation too, so massCreate of 10 posts with relations: ['author'] is an INSERT and one SELECT, not ten.

posts.create.ts
const created = await posts.massCreate(dtos, { relations: ['author'], counts: ['tags'] });

These methods load relations onto what they return:

MethodLoads relations and counts
create, massCreateyes
update, massUpdateyes
upsert, massUpsertyes
restore, massRestoreyes
attach, detach, syncyes, plus the relation they changed

The relation paths are checked against loadableRelations before anything is written, so a bad path is a 400 and leaves the table untouched.

How do I create a row with its children in one call?

Allow the relation, then send the children in the body.

Declare the relation and allow writing it

List the relation in writableRelations next to its definition:

authors.module.ts
DrizzleCrudModule.forFeature([{
  service: AuthorsService,
  table: authors,
  config: {
    relations: {
      posts: { table: posts, type: 'hasMany', foreignKey: 'author_id' },
    },
    writableRelations: ['posts'],
  },
}]);

Put the children in the body under the relation's name

authors.create.ts
const author = await authors.create(
  {
    name: 'Dee',
    email: 'dee@x.io',
    posts: [
      { title: 'First', status: 'draft' },
      { title: 'Second', status: 'draft' },
    ],
  },
  { relations: ['posts'] },
);

The author and both posts are written in one transaction: one INSERT for the author and one multi-row INSERT for the posts, with author_id set for you.

Handle a failing child

If a child fails, the parent is rolled back too, and the call answers with the child's error: a bad enum value is a 400, a duplicate key a 409. If you passed your own transaction, the nested writes join it instead of opening a new one.

What shapes does a nested body take?

The body value depends on the relation type:

RelationBody valueWhat happens
has-manyan array of objectsOne multi-row INSERT, with the foreign key set to the new row's key.
belongs-toan objectThe related row is inserted first and its key goes on the new row (localKey).
many-to-manyan array of ids, objects, or bothObjects are inserted into the related table in one statement, then every id is linked through the join table.

A few rules apply to every shape:

  • Nested writes run on create only, and go one level deep.
  • Only the related table's own columns are written, never its soft delete column. Other keys in a child object are dropped.
  • The timestamps and the tenant scope are set on a child row when the related table has those columns.
  • A relation key set to null is skipped.
  • A wrong shape (an object for has-many, an array for belongs-to) is a 400.
  • On MySQL, which has no RETURNING, a nested create is a 400.

What if writableRelations is not set?

Then nested writes are off. A relation key in the body is left alone and Drizzle ignores it, which is how 3.4 behaved. With the list set, a relation key that is not in it is a 400, the same way writableColumns treats a column. A trusted server-side call can pass skipAllowlists: true to write any declared relation.

How do I attach or detach many-to-many rows?

Call attach, detach or sync with the row's id, the relation name and the related ids:

posts.tags.ts
await posts.attach(2, 'tags', [1, 2]);         // link tags 1 and 2
await posts.detach(2, 'tags', [1]);            // unlink tag 1
await posts.detach(2, 'tags');                 // unlink every tag
const post = await posts.sync(2, 'tags', [2]); // links are exactly [2] now
post.tags; // [{ id: 2, name: 'tech' }]
MethodSignatureWhat it does
attachattach(id, relation, relatedIds, options?)Adds links. Ids already linked are skipped.
detachdetach(id, relation, relatedIds?, options?)Removes the given links, or every link when relatedIds is left out.
syncsync(id, relation, relatedIds, options?)Adds the missing links and removes the rest, so the links match relatedIds exactly. [] removes every link.

Each method returns Promise<T>: the row with that relation loaded, plus anything in options.relations. Each runs in one transaction and is set based whatever the number of ids: one SELECT checks the targets exist, one finds the links already there, and one INSERT adds the rest (sync adds one DELETE). Duplicates in the list are ignored. The related rows themselves are never changed or deleted.

With global hooks on, they raise attach, detach and sync events.

What errors can attach, detach and sync return?

CauseResult
The row does not exist, or is outside the caller's scope404
A target id that does not exist or is soft-deleted400 BulkOperationException with one 404 entry per missing id
A target of another tenant, when the related table has the scope columnCounted as missing: a 404 entry
The relation is not a many-to-many400
writableRelations is set and does not list the relation400
relatedIds is not an array of plain ids400

When writableRelations is unset, attach, detach and sync work on any many-to-many relation.

Common questions

Next steps

Last updated View change history

On this page