~/gqlith — docs/annotations.md
Early access gqlith is in private early access — the code is not publicly available yet. These docs preview the surface so you know what to expect. Join the list →

Docs / Reference

Annotations cheatsheet

The @gqlith.* SQL-comment tags, table by table and column by column.

Annotations live in COMMENT ON TABLE, COMMENT ON COLUMN, COMMENT ON CONSTRAINT, COMMENT ON FUNCTION and COMMENT ON VIEW. Multiple tags on one object are space-separated in a single comment string — and any plain prose in the comment becomes the GraphQL description.

Table-level

TagEffect
@gqlith.rbac(<role>: <ops>; …)Access control. Roles are any string; ops are read, write, findOne, findMany, readComputed, *. Wildcard role * matches any authenticated user. Missing tag is fail-closed.
@gqlith.rowFilter(<role>: <predicate>; …)Injects a WHERE clause per role. Variables: $userId, $tenantId, $role, $claims.<key>. * is pass-through.
@gqlith.softDelete(column: "col", value: null)Marks the tombstone column. DELETE sets it instead of removing the row.
@gqlith.omit / @gqlith.omit(mutations) / @gqlith.omit(queries)Hide the table, or hide just one side of it.
@gqlith.rename(TypeName)Override the GraphQL type name.
@gqlith.audit or @gqlith.audit(columns: "a,b,c")Emit full or column-filtered before/after diffs.
@gqlith.upsert(by: "col") or (by: "col1,col2")Enable upsert against a UNIQUE constraint.
@gqlith.rateLimit(limit: N, windowMs: M)Per-entity rate limit override. limit: 0 is deny-all.
@gqlith.skipTenancyBypass the automatic tenant filter and auto-fill.
@gqlith.skipOptimisticLockOpt out of OCC auto-detection.
@gqlith.manual(query.findOne) / @gqlith.manual(mutation.create)Emit a stub resolver you implement.
@gqlith.nestedMutation(create, connect, …)Default nested operators for all FKs on this table.
@gqlith.customFieldsEnable tenant-defined custom fields on this entity — catalog, typed values, filtering, discovery.

Column-level

TagEffect
@gqlith.fieldVisibility(role1, role2, …)Column returns null for roles not listed.
@gqlith.fieldName(newName)Rename the GraphQL field.
@gqlith.preset(value: "$userId" | "$now" | "$tenantId")Auto-fill on write from the JWT; the field is removed from the input type.
@gqlith.validate(min: N, max: M, regex: "…")Zod validation pre-query. At least one attribute required.
@gqlith.versionExplicit optimistic-lock version column.
@gqlith.jsonSchema(fields: "…" | ref: "…", name:, open:, sortable:, filterable:)Declare a JSONB column’s shape — typed reads, validated writes, typed filtering.
@gqlith.file(profile: "…", allowedContentTypes: ["image/*"], maxSizeBytes:, allowedExtensions:, variants:)Turn a column into a governed file attachment — uploads, signed downloads, variants.

Constraint-level

COMMENT ON CONSTRAINT recipe_steps_recipe_id_fkey ON recipe_steps
  IS '@gqlith.nestedMutation(create, update, delete)';

Overrides nested mutation operators for one specific FK.

Function-level

TagEffect
@gqlith.computedExpose a stored function as a computed field on its first argument’s type.
@gqlith.fieldName(newName)Rename the computed field.
@gqlith.rootKind(query) / @gqlith.rootKind(mutation)Expose the function as a root Query or Mutation field.
@gqlith.rbac(role1, role2)Required on every exposed function — a comma-separated role list (function form, no ops). Fail-closed without it.
@gqlith.filterPredicate(selector: "col")Register a SETOF <table> function as a filter predicate usable inside filter expressions.
@gqlith.rateLimit(…) / @gqlith.audit / @gqlith.manualPer-function rate limits, audit and manual stubs — same semantics as on tables.

Views

Postgres views are exposed read-only. @gqlith.rbac is required on every exposed view (fail-closed), and @gqlith.omit hides one entirely.

Worked example

COMMENT ON TABLE recipes IS '
  @gqlith.rbac(admin: *; chef: read, write; member: read; guest: findMany, findOne)
  @gqlith.rowFilter(
    chef:   author_id = $userId OR status = ''published'';
    member: status = ''published'';
    guest:  status = ''published'';
    admin:  *
  )
  @gqlith.audit
  @gqlith.softDelete(column: "deleted_at")
  @gqlith.upsert(by: "tenant_id,slug")
';

COMMENT ON COLUMN recipes.created_by IS '@gqlith.preset(value: "$userId")';
COMMENT ON COLUMN recipes.created_at IS '@gqlith.preset(value: "$now")';
COMMENT ON COLUMN recipes.slug IS '@gqlith.validate(min: 1, max: 80, regex: "^[a-z0-9-]+$")';

That single block governs GraphQL, REST and MCP for recipes — RBAC, per-role row filters, audit logging, soft delete with restore, upsert by (tenant_id, slug), auto-fill of audit columns and slug validation.