Auto-indexing service and GraphQL API for AT Protocol Records

Array Fields in Object Types Implementation Plan#

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Make array fields in object types (like AppBskyEmbedImages.images) resolve to proper array types instead of String.

Architecture: Pass lexicon ID context through object type building so shorthand refs (#image) can be expanded to fully-qualified refs (app.bsky.embed.images#image) before lookup in the object types dictionary.

Tech Stack: Gleam, swell (GraphQL library), lexicon_graphql package


Task 1: Add ref expansion helper to object_builder#

Files:

  • Modify: lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam

Step 1: Write the helper function

Add at the bottom of the file before the closing:

/// Expand a shorthand ref to fully-qualified ref
/// "#image" with lexicon_id "app.bsky.embed.images" -> "app.bsky.embed.images#image"
fn expand_ref(ref: String, lexicon_id: String) -> String {
  case string.starts_with(ref, "#") {
    True -> lexicon_id <> ref
    False -> ref
  }
}

Step 2: Add string import if not present

Check imports at top of file, add if missing:

import gleam/string

Step 3: Run build to verify it compiles

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam build Expected: Compiles with no errors (warning about unused function is OK)

Step 4: Commit

git add lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam
git commit -m "feat(object_builder): add expand_ref helper for shorthand refs"

Task 2: Add lexicon ID parser to registry#

Files:

  • Modify: lexicon_graphql/src/lexicon_graphql/internal/lexicon/registry.gleam

Step 1: Add the helper function

Add after the existing parse_ref function:

/// Extract lexicon ID from a fully-qualified ref
/// "app.bsky.embed.images#image" -> "app.bsky.embed.images"
/// "app.bsky.embed.images" -> "app.bsky.embed.images"
pub fn lexicon_id_from_ref(ref: String) -> String {
  case string.split(ref, "#") {
    [lexicon_id, _] -> lexicon_id
    _ -> ref
  }
}

Step 2: Run build to verify it compiles

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam build Expected: Compiles with no errors

Step 3: Commit

git add lexicon_graphql/src/lexicon_graphql/internal/lexicon/registry.gleam
git commit -m "feat(registry): add lexicon_id_from_ref helper"

Task 3: Add array items expansion helper to object_builder#

Files:

  • Modify: lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam

Step 1: Add the helper function

Add after expand_ref:

/// Expand shorthand refs in ArrayItems
fn expand_array_items(
  items: types.ArrayItems,
  lexicon_id: String,
) -> types.ArrayItems {
  types.ArrayItems(
    type_: items.type_,
    ref: option.map(items.ref, fn(r) { expand_ref(r, lexicon_id) }),
    refs: option.map(items.refs, fn(rs) {
      list.map(rs, fn(r) { expand_ref(r, lexicon_id) })
    }),
  )
}

Step 2: Run build to verify it compiles

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam build Expected: Compiles with no errors (warning about unused function is OK)

Step 3: Commit

git add lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam
git commit -m "feat(object_builder): add expand_array_items helper"

Task 4: Update build_object_fields to accept lexicon_id#

Files:

  • Modify: lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam

Step 1: Update function signature

Change build_object_fields from:

fn build_object_fields(
  properties: List(#(String, types.Property)),
  object_types_dict: Dict(String, schema.Type),
) -> List(schema.Field) {

To:

fn build_object_fields(
  properties: List(#(String, types.Property)),
  lexicon_id: String,
  object_types_dict: Dict(String, schema.Type),
) -> List(schema.Field) {

Step 2: Run build to see call site errors

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam build Expected: Error about missing argument in build_object_type

Step 3: Commit (WIP)

git add lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam
git commit -m "wip: update build_object_fields signature to accept lexicon_id"

Task 5: Update build_object_type to pass lexicon_id#

Files:

  • Modify: lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam

Step 1: Update function signature

Change build_object_type from:

pub fn build_object_type(
  obj_def: types.ObjectDef,
  type_name: String,
  object_types_dict: Dict(String, schema.Type),
) -> schema.Type {
  let fields = build_object_fields(obj_def.properties, object_types_dict)

To:

pub fn build_object_type(
  obj_def: types.ObjectDef,
  type_name: String,
  lexicon_id: String,
  object_types_dict: Dict(String, schema.Type),
) -> schema.Type {
  let fields = build_object_fields(obj_def.properties, lexicon_id, object_types_dict)

Step 2: Run build to see call site errors

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam build Expected: Error about missing argument in build_all_object_types

Step 3: Commit (WIP)

git add lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam
git commit -m "wip: update build_object_type signature to accept lexicon_id"

Task 6: Update build_all_object_types to extract and pass lexicon_id#

Files:

  • Modify: lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam

Step 1: Update the fold body

Change in build_all_object_types:

list.fold(object_refs, dict.new(), fn(acc, ref) {
  case lexicon_registry.get_object_def(registry, ref) {
    option.Some(obj_def) -> {
      let type_name = ref_to_type_name(ref)
      let object_type = build_object_type(obj_def, type_name, acc)
      dict.insert(acc, ref, object_type)
    }
    option.None -> acc
  }
})

To:

list.fold(object_refs, dict.new(), fn(acc, ref) {
  case lexicon_registry.get_object_def(registry, ref) {
    option.Some(obj_def) -> {
      let type_name = ref_to_type_name(ref)
      let lexicon_id = lexicon_registry.lexicon_id_from_ref(ref)
      let object_type = build_object_type(obj_def, type_name, lexicon_id, acc)
      dict.insert(acc, ref, object_type)
    }
    option.None -> acc
  }
})

Step 2: Run build to verify it compiles

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam build Expected: Compiles with no errors

Step 3: Commit

git add lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam
git commit -m "feat(object_builder): pass lexicon_id through build chain"

Task 7: Handle array types in build_object_fields#

Files:

  • Modify: lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam

Step 1: Update the property mapping

Change in build_object_fields:

list.map(properties, fn(prop) {
  let #(name, types.Property(type_, required, format, ref, _refs, _items)) =
    prop

  // Map the type, using the object_types_dict to resolve refs
  let graphql_type =
    type_mapper.map_type_with_registry(type_, format, ref, object_types_dict)

To:

list.map(properties, fn(prop) {
  let #(name, types.Property(type_, required, format, ref, _refs, items)) =
    prop

  // Map the type, handling arrays specially to resolve item refs
  let graphql_type = case type_ {
    "array" -> {
      let expanded_items = case items {
        option.Some(arr_items) ->
          option.Some(expand_array_items(arr_items, lexicon_id))
        option.None -> option.None
      }
      type_mapper.map_array_type(expanded_items, object_types_dict)
    }
    _ -> type_mapper.map_type_with_registry(type_, format, ref, object_types_dict)
  }

Step 2: Run build to verify it compiles

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam build Expected: Compiles with no errors

Step 3: Run tests

Run: cd /Users/chadmiller/code/quickslice/lexicon_graphql && gleam test Expected: All tests pass

Step 4: Commit

git add lexicon_graphql/src/lexicon_graphql/internal/graphql/object_builder.gleam
git commit -m "feat(object_builder): handle array types with expanded refs"

Task 8: Rebuild server and verify fix#

Files:

  • None (verification only)

Step 1: Rebuild server

Run: cd /Users/chadmiller/code/quickslice/server && gleam build Expected: Compiles with no errors

Step 2: Restart server and test via MCP

After restarting the server, run introspection query:

{
  __type(name: "AppBskyEmbedImages") {
    fields {
      name
      type { kind name ofType { kind name } }
    }
  }
}

Expected: images field should have type LIST with ofType containing AppBskyEmbedImagesImage

Step 3: Verify nested type exists

{
  __type(name: "AppBskyEmbedImagesImage") {
    name
    fields { name }
  }
}

Expected: Type exists with fields alt, image, aspectRatio

Step 4: Final commit

git add -A
git commit -m "feat: array fields in object types resolve to proper types

- Pass lexicon ID through object type building chain
- Expand shorthand refs (#image -> lexicon#image) at build time
- Handle array types by calling map_array_type with expanded items
- Enables querying embed images with proper nested types"

Verification Query#

After implementation, this query should work:

{
  appBskyFeedPost {
    edges {
      node {
        embed {
          ... on AppBskyEmbedImages {
            images {
              alt
              aspectRatio
            }
          }
        }
      }
    }
  }
}