DreamLake

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

QuestionGraphQLREST
Which projects can this viewer access?projectCatalogGET /namespaces/:namespace/projects?search=design&page=1&pageSize=50
Where is this Note actually associated?note.nodesGET /namespaces/:namespace/resources/note/:noteId/projects?view=nodes
Which Bindrs can the picker offer in this project?bindrsGET /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.