# Project and Note queries

This reference describes the implementation branch. It does not establish that
these fields are deployed to staging or production. Verify the target server's
schema before switching a client. Existing mutation routes remain REST; Note
content, CRDT synchronization, and revision-protected editing are unchanged.

## Separate associations from choices

| Question | GraphQL | REST |
|---|---|---|
| Which projects can this viewer access? | `projectCatalog` | `GET /namespaces/:namespace/projects?search=design&page=1&pageSize=50` |
| Where is this Note actually associated? | `note.nodes` | `GET /namespaces/:namespace/resources/note/:noteId/projects?view=nodes` |
| Which Bindrs can the picker offer in this project? | `bindrs` | `GET /namespaces/:namespace/projects/:projectSlug/bindrs?view=choices&search=design&first=20` |

The GraphQL resolvers and these REST views use the same underlying visibility,
project permission, resource-readability, and association services. Their wire
shapes differ: GraphQL selects fields and wraps results in `data`; REST returns
its resource response directly. The `view=nodes` REST association view permits
authorized anonymous reads of public resources; the legacy default response
requires a signed-in caller. Unreadable resources are not exposed by the shared
service.

The existing resource-project REST response without `view=nodes` remains a
compatibility response that mixes candidate projects with `bound` and `canBind`.
New clients should use the views above instead. The existing Bindr response
without `view=choices` also keeps its prior tree-list contract.

## Query a Note's locations

```graphql
query NoteLocations($namespace: String!, $noteId: ID!) {
  note(namespaceSlug: $namespace, id: $noteId) {
    id
    name
    nodes {
      id
      name
      path
      project {
        id
        slug
        displayName
      }
      bindrs {
        id
        name
        parentId
      }
    }
  }
}
```

Variables below use illustrative values. Replace them with a namespace and Note
ID that the caller can read:

```json
{
  "namespace": "example-team",
  "noteId": "000000000000000000000001"
}
```

`nodes` contains actual resource Nodes in visible projects, never candidate
projects. An empty list means no visible associations, not proof that no hidden
association exists. An unreadable or missing Note returns `note: null`.

`Node.path` is the existing comma-delimited **ancestor path**, not the full file
path or a parent ID. For example, a Node named `API design` with
`path: ",example-project,design-docs,"` sits beneath that ancestor path. Project
ownership comes from `projectNodeId` and the resolved `project`, not parsing the
path. No folder or storage migration is part of these reads.

`node.bindrs` means Bindrs **tagging that Node**, not every Bindr owned by the
project. It is resolved through membership records. Project objects retain the
GraphQL `Node` type and stable ID for Apollo normalization.

The equivalent REST view returns `{ nodes, total }`; each Node includes its
project and attached Bindrs. No `bound` or `canBind` field is needed in that view.

## Search project choices

```graphql
query ProjectChoices($namespace: String!, $search: String, $page: Int, $pageSize: Int) {
  projectCatalog(namespaceSlug: $namespace, search: $search, page: $page, pageSize: $pageSize) {
    projects {
      id
      slug
      displayName
      permissions {
        role
        project { read update delete manageGrants }
        content { read create delete }
      }
    }
    total
    page
    pageSize
    totalPages
    canCreate
  }
}
```

```json
{
  "namespace": "example-team",
  "search": "design",
  "page": 1,
  "pageSize": 50
}
```

The catalog is scoped to one namespace and returns authorized projects,
including project-specific grants. Search is performed on the server. The
default page size is 50; the maximum is 200. Fetch subsequent pages when needed
instead of treating the first page as the complete set of projects.

`canCreate` is permission to create a project in the namespace.
`permissions.content.create` is permission to add content within a particular
project. For an add-project picker, exclude projects already present in
`note.nodes` and offer projects with content creation permission. The mutation
still checks both project authorization and resource readability; a successful
read is not a grant that remains valid indefinitely.

## Search Bindr choices on demand

```graphql
query BindrChoices($projectId: ID!, $search: String, $first: Int, $after: String) {
  bindrs(projectId: $projectId, search: $search, first: $first, after: $after) {
    nodes { id name parentId projectNodeId }
    pageInfo { endCursor hasNextPage }
  }
}
```

```json
{
  "projectId": "000000000000000000000002",
  "search": "design",
  "first": 20,
  "after": null
}
```

The default page size is 20; the maximum is 100. Run this only when the picker needs choices. For another page, pass the returned
`endCursor` as `after`; reset the cursor whenever the project or search changes.
Treat cursors as opaque. REST uses the same `{ nodes, pageInfo }` response shape
and accepts the cursor as an URL-encoded `after` query parameter. The REST path
identifies the project by namespace and slug; GraphQL uses its Node ID.

These are live listings, not transactionally frozen snapshots. Concurrent edits
may change choices between requests. Mutations remain authoritative and must
revalidate permissions and membership constraints.

## Execute a GraphQL request

Save any query above to `query.graphql` and its variables to `variables.json`.
Set the API base URL and an existing DreamLake token in your shell environment:

```bash
export DREAMLAKE_API_URL='http://localhost:3001'
# Set DREAMLAKE_TOKEN securely to your existing token; do not commit it.
python3 - <<'PY'
import json
import os
import urllib.request

with open('query.graphql') as source:
    query = source.read()
with open('variables.json') as source:
    variables = json.load(source)
request = urllib.request.Request(
    os.environ['DREAMLAKE_API_URL'].rstrip('/') + '/graphql',
    data=json.dumps({'query': query, 'variables': variables}).encode(),
    headers={
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + os.environ['DREAMLAKE_TOKEN'],
    },
)
with urllib.request.urlopen(request) as response:
    result = json.load(response)
if result.get('errors'):
    raise RuntimeError(result['errors'])
print(json.dumps(result['data'], indent=2))
PY
```

GraphQL errors can arrive with HTTP 200; inspect `errors` before using `data`.
For Apollo clients, select stable IDs and keep project objects as `Node`.
Refresh associations after existing REST mutations. Cancel superseded picker
requests, prevent stale identity responses from entering the cache, and isolate
explicit anonymous previews from authenticated cache entries.
