Skip to content

Item tags

Backend domain: app/graphql/items/ (Tag, item_tag_table, TagService, TagRepository) Migration: tag_module

Overview

items.tags was a varchar[] column on items. It is now a proper module: a tags table (id, tagName, createdById, createdAt, unique on tagName) plus an item_tags association table (item_id, tag_id, both ON DELETE CASCADE). Items can have any number of tags; tags are tenant-wide and reusable across items.

The migration backfills existing tag values: every distinct string previously stored on items.tags becomes a Tag row (createdById copied from one of the items using it), and the item/tag links populate item_tags. The old column is then dropped.

GraphQL surface

Type

type Tag {
  id: UUID!
  tagName: String!
}

Item.tags and ItemLite.tags return [Tag!] | null (resolved lazily off the item's relationship; the previous [String!] shape is gone).

Queries

tag(id: UUID!): Tag!
tags: [Tag!]!
tagSearch(search: String!): [Tag!]!

Mutations

createTag(tag: TagInput!): Tag!
updateTag(tag: TagInput!): Tag!
deleteTag(tagId: UUID!): Boolean!
input TagInput {
  id: UUID            # null on create
  tagName: String!
}

There is no longer a dedicated setItemTags mutation or ItemTagsAssignmentInput. Tag assignment happens through the standard createItem / updateItem mutations.

Item create / update

ItemInput carries an inline tags: [TagInput!] list (a richer shape than the old tagIds: [UUID!]):

  • Omit tags (leave null) on update to leave the item's tag set untouched.
  • Pass an empty list [] to clear all tags on the item.
  • Pass TagInput objects with id set to attach existing tags.
  • Pass TagInput objects with id: null and only tagName to auto-create-then-attach — the service creates the tag if no row matches the name, then links it.

This means the UI no longer needs a separate "create tag" round-trip before saving the item.

Behaviour to handle in the UI

  • The tag picker is a multi-select that loads from tags (or tagSearch for incremental search) and emits TagInput objects.
  • For known tags, emit { id, tagName }. For freshly typed new tags, emit { id: null, tagName } — the backend will create them on save.
  • Editing an item: read existing item.tags (objects with id + tagName) and pre-select; preserve the existing id when re-submitting so the link is kept.
  • deleteTag cascades to item_tags so previously tagged items lose the link silently.
  • For a tag chip editor on a detail page (no full item edit form), reuse updateItem and pass only the changed tags list.

Notes

  • tagName is unique across the tenant — createTag returns DuplicateRowError on collision; surface that as a validation error on the input.
  • The previous tags: [String!] field is removed from the schema; any frontend code reading item.tags[i] as a string must be updated to item.tags[i].tagName.