Skip to content

feat: update samples subgraph to 0.2.0#132

Merged
DominicOram merged 1 commit intomainfrom
samples-0.2.0
Mar 5, 2026
Merged

feat: update samples subgraph to 0.2.0#132
DominicOram merged 1 commit intomainfrom
samples-0.2.0

Conversation

@dls-graph-schema-federator
Copy link
Contributor

Repository

Subgraph maintainers

@DiamondLightSource/ulims

@github-actions
Copy link

@@ -78,11 +78,26 @@ input CreateOrValidateSampleInput
   """URL of the JSON schema the samples' `data` should be validated against"""
   dataSchemaUrl: String!
 
+  """Samples to be created"""
+  samples: [SampleIn!]!
+
+  """
+  Whether or not the provided samples should only be validated and not created
+  """
+  validateOnly: Boolean! = false
+
   """Number of the proposal the samples should be associated with"""
   proposalNumber: Int!
 
   """Number of the instrument session the samples should be associated with"""
   instrumentSessionNumber: Int!
+}
+
+input CreateOrValidateSampleInputBase
+  @join__type(graph: SAMPLES)
+{
+  """URL of the JSON schema the samples' `data` should be validated against"""
+  dataSchemaUrl: String!
 
   """Samples to be created"""
   samples: [SampleIn!]!
@@ -167,24 +182,53 @@ type Instrument
 
 type InstrumentSession
   @join__type(graph: INSTRUMENT_SESSIONS, key: "instrumentSessionNumber proposal {proposalNumber}")
+  @join__type(graph: SAMPLES, key: "instrumentSessionNumber proposal { proposalNumber }")
+{
+  instrumentSessionId: Int! @join__field(graph: INSTRUMENT_SESSIONS)
+  instrumentSessionNumber: Int! @join__field(graph: INSTRUMENT_SESSIONS) @join__field(graph: SAMPLES, external: true)
+  startTime: DateTime @join__field(graph: INSTRUMENT_SESSIONS)
+  endTime: DateTime @join__field(graph: INSTRUMENT_SESSIONS)
+  type: String @join__field(graph: INSTRUMENT_SESSIONS)
+  state: String @join__field(graph: INSTRUMENT_SESSIONS)
+  riskRating: String @join__field(graph: INSTRUMENT_SESSIONS)
+  proposal: Proposal @join__field(graph: INSTRUMENT_SESSIONS) @join__field(graph: SAMPLES, external: true)
+  instrument: Instrument! @join__field(graph: INSTRUMENT_SESSIONS)
+  roles: [InstrumentSessionRole!]! @join__field(graph: INSTRUMENT_SESSIONS)
+
+  """Samples associated with a given instrument session"""
+  samples(first: Int!, filter: SampleFilterInput! = {}, before: String = null, after: String = null, last: Int = null, orderBy: SampleOrder! = {}): SampleConnection! @join__field(graph: SAMPLES)
+}
+
+type InstrumentSessionConnection
+  @join__type(graph: SAMPLES)
 {
-  instrumentSessionId: Int!
+  edges: [InstrumentSessionEdge!]!
+  pageInfo: PageInfo!
+}
+
+type InstrumentSessionEdge
+  @join__type(graph: SAMPLES)
+{
+  cursor: String!
+  node: InstrumentSession!
+}
+
+input InstrumentSessionInput
+  @join__type(graph: SAMPLES)
+{
+  proposalNumber: Int!
   instrumentSessionNumber: Int!
-  startTime: DateTime
-  endTime: DateTime
-  type: String
-  state: String
-  riskRating: String
-  proposal: Proposal
-  instrument: Instrument!
-  roles: [InstrumentSessionRole!]!
 }
 
 type InstrumentSessionMutations
   @join__type(graph: INSTRUMENT_SESSIONS, key: "instrumentSessionNumber proposalNumber")
+  @join__type(graph: SAMPLES, key: "instrumentSessionNumber proposalNumber")
 {
-  instrumentSessionNumber: Int!
-  proposalNumber: Int!
+  instrumentSessionNumber: Int! @join__field(graph: INSTRUMENT_SESSIONS) @join__field(graph: SAMPLES, external: true)
+  proposalNumber: Int! @join__field(graph: INSTRUMENT_SESSIONS) @join__field(graph: SAMPLES, external: true)
+
+  """Create or validate samples associated with this instrument session"""
+  createOrValidateSamples(input: CreateOrValidateSampleInputBase!): CreateSamplesResponse! @join__field(graph: SAMPLES)
 }
 
 type InstrumentSessionRole
@@ -218,6 +262,24 @@ scalar JSON
 scalar JSONObject
   @join__type(graph: WORKFLOWS)
 
+input JSONOperator
+  @join__type(graph: SAMPLES)
+{
+  stringOperator: StringOperatorInput = null
+  datetimeOperator: DatetimeOperatorInput = null
+  numericOperator: NumericOperatorInput = null
+}
+
+input JSONOperatorInput
+  @join__type(graph: SAMPLES)
+{
+  """A JSON path specifying the value to filter. Must start with '$.'"""
+  path: String!
+
+  """The operator to apply to the JSON field"""
+  operator: JSONOperator!
+}
+
 scalar link__Import
 
 enum link__Purpose {
@@ -250,12 +312,26 @@ type Mutation
   @join__type(graph: WORKFLOWS)
 {
   instrumentSession(proposalNumber: Int!, instrumentSessionNumber: Int!): InstrumentSessionMutations @join__field(graph: INSTRUMENT_SESSIONS)
-  createSamples(input: CreateSampleInput!): [Sample!]! @join__field(graph: SAMPLES) @deprecated(reason: "Will be replaced by createOrValidateSamples")
   createOrValidateSamples(input: CreateOrValidateSampleInput!): CreateSamplesResponse! @join__field(graph: SAMPLES)
+  createSamples(input: CreateSampleInput!): [Sample!]! @join__field(graph: SAMPLES) @deprecated(reason: "Will be replaced by createOrValidateSamples")
   sample(sampleId: UUID!): SampleMutations @join__field(graph: SAMPLES)
   submitWorkflowTemplate(name: String!, visit: VisitInput!, parameters: JSON!): Workflow! @join__field(graph: WORKFLOWS)
 }
 
+input NumericOperatorInput
+  @join__type(graph: SAMPLES)
+{
+  """
+  Will filter to items where the numeric field is greater than the provided value
+  """
+  gt: Float = null
+
+  """
+  Will filter to items where the numeric field is less than the provided value
+  """
+  lt: Float = null
+}
+
 """Information about pagination in a connection"""
 type PageInfo
   @join__type(graph: INSTRUMENT_SESSIONS)
@@ -277,15 +353,16 @@ type PageInfo
 
 type Proposal
   @join__type(graph: INSTRUMENT_SESSIONS, key: "proposalNumber")
+  @join__type(graph: SAMPLES, key: "proposalNumber")
 {
-  proposalNumber: Int!
-  proposalCategory: String
-  title: String
-  summary: String
-  state: ProposalState!
-  instrumentSessions: [InstrumentSession!]!
-  instruments: [Instrument!]!
-  roles: [ProposalAccount!]!
+  proposalNumber: Int! @join__field(graph: INSTRUMENT_SESSIONS) @join__field(graph: SAMPLES, external: true)
+  proposalCategory: String @join__field(graph: INSTRUMENT_SESSIONS)
+  title: String @join__field(graph: INSTRUMENT_SESSIONS)
+  summary: String @join__field(graph: INSTRUMENT_SESSIONS)
+  state: ProposalState! @join__field(graph: INSTRUMENT_SESSIONS)
+  instrumentSessions: [InstrumentSession!]! @join__field(graph: INSTRUMENT_SESSIONS)
+  instruments: [Instrument!]! @join__field(graph: INSTRUMENT_SESSIONS)
+  roles: [ProposalAccount!]! @join__field(graph: INSTRUMENT_SESSIONS)
 }
 
 type ProposalAccount
@@ -345,12 +422,12 @@ type Query
   """Get an account"""
   account(username: String!): Account @join__field(graph: INSTRUMENT_SESSIONS)
 
+  """Get a list of samples associated with a given instrument session"""
+  samples(first: Int!, instrumentSessions: [InstrumentSessionInput!] = null, filter: SampleFilterInput! = {}, before: String = null, after: String = null, last: Int = null, orderBy: SampleOrder! = {}): SampleConnection! @join__field(graph: SAMPLES)
+
   """Get a sample by its id"""
   sample(sampleId: UUID!): Sample @join__field(graph: SAMPLES)
 
-  """Get a list of samples associated with a given instrument session"""
-  samples(proposalNumber: Int!, instrumentSessionNumber: Int!, first: Int!, filter: SampleFilterInput! = {}, before: String = null, after: String = null, last: Int = null, orderBy: SampleOrder! = {}): SampleConnection! @join__field(graph: SAMPLES)
-
   """Get a single [`Workflow`] by proposal, visit, and name"""
   workflow(visit: VisitInput!, name: String!): Workflow! @join__field(graph: WORKFLOWS)
   workflows(visit: VisitInput!, cursor: String, limit: Int, filter: WorkflowFilter): WorkflowConnection! @join__field(graph: WORKFLOWS)
@@ -379,6 +456,10 @@ type Sample
 
   """The JSON schema that the sample's `data` conforms to"""
   dataSchema: JSON!
+
+  """The instrument sessions that this sample is associated with"""
+  instrumentSessions: InstrumentSessionConnection! @join__field(graph: SAMPLES, provides: "edges{ node{ instrumentSessionNumber proposal{ proposalNumber }}}")
+  images: [SampleImage!]!
 }
 
 type SampleConnection
@@ -431,6 +512,16 @@ input SampleFilterInput
 
   """Filter on the `name` field of `Sample`"""
   name: StringOperatorInput = null
+
+  """Filter on the `data` field of `Sample`"""
+  data: [JSONOperatorInput!] = null
+}
+
+type SampleImage
+  @join__type(graph: SAMPLES)
+{
+  url: String!
+  filename: String!
 }
 
 input SampleIn
@@ -459,6 +550,7 @@ type SampleMutations
   sampleId: UUID!
   linkInstrumentSessionToSample(proposalNumber: Int!, instrumentSessionNumber: Int!): Void
   addSampleEvent(sampleEvent: AddSampleEventInput!): SampleEvent!
+  createSampleImageUploadUrl(filename: String!, contentType: String!, contentLength: Int!): String!
 }
 
 input SampleOrder

@MattPrit
Copy link

MattPrit commented Mar 4, 2026

The PR essentially adds three things:

1. Getting samples from multiple sessions in a single query

Previously, samples could only be queried for a single session at a time, e.g.:

query Q {
  samples(
    instrumentSession: {proposalNumber: 123, instrumentSessionNumber: 123}
    first: 5
  ) {
    edges {
      node {
        id
        name
        data
      }
    }
  }
}

Now, samples can be queried across multiple sessions:

query Q {
  samples(
    instrumentSessions: [
      { proposalNumber: 123, instrumentSessionNumber: 123 },
      { proposalNumber: 123, instrumentSessionNumber: 456 }
    ]
    first:5
  ) {
    edges {
      node {
        id
        name
        data
      }
    }
  }
}

Or across all sessions:

query Q {
  samples(first:5) {
    edges {
      node {
        id
        name
        data
      }
    }
  }
}

2. Sample-InstrumentSession 'federation'

Associated samples can now be queried for a given instrument session alongside other fields:

query Q {
  instrumentSession(proposalNumber: 123, instrumentSessionNumber: 123) {
    startTime
    endTime
    samples(first: 5) {
      edges {
        node {
          id
          name
          data
        }
      }
    }
  }
}

Associated instrument sessions can now be queried for a given sample alongside other fields:

query Q4 {
  sample(sampleId: "01968200-edea-76d3-8979-7a27c6b27785") {
    id
    name
    data
    instrumentSessions {
      edges {
        node {
          startTime
          endTime
        }
      }
    }
  }
}

In addition, samples can now be created/validated via a session (i.e. passing first through the sessions subgraph to verify that the session specified exists):

mutation M2 {
  instrumentSession(proposalNumber: 123, instrumentSessionNumber: 123) {
    createOrValidateSamples(
      input: {
        samples:[{name: "foo", data: {}}],
        dataSchemaUrl: "https://raw.githubusercontent.com/DiamondLightSource/ulims-json-schemas/refs/heads/main/schemas/samples/i15-1/powder/0.0.1.json"
        validateOnly: true
      }
    ) {
      success
      errors {
        index
        errors {
          location
          type
          message
        }
      }
    }
  }
}

3. Creating a URL that can be used to upload sample images

Image upload URLs can now be created for a given sample:

mutation M3 {
  sample(sampleId: "01968200-edea-76d3-8979-7a27c6b27785") {
    createSampleImageUploadUrl(
      filename: "foo.jpg",
      contentType: "image/jpeg",
      contentLength: 12345
    )
  }
}

@DominicOram DominicOram self-requested a review March 5, 2026 10:23
Copy link

@DominicOram DominicOram left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I tested against http://ulims-dev.diamond.ac.uk/ and the queries make sense for common things that we may want to do at the beamline

@DominicOram DominicOram merged commit 36af300 into main Mar 5, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants