From 2946c99bd6f816b05b905a9bcb8cb546524256af Mon Sep 17 00:00:00 2001 From: Carlo van Driesten Date: Thu, 12 Nov 2026 15:51:49 +0100 Subject: [PATCH] fix(graphqlgen): emit a document that parses and defines every type it names A generated document failed to parse, or failed schema validation, in eight independent ways. A class resolving to no slot closed its body immediately, which GraphQL has no syntax for. A type the schema defined itself was named by a field but never declared. An enumeration drawing its values from an ontology was dropped while fields kept naming it; an enum type defines one or more values (GraphQL spec, October 2021, section 3.9), so such an enumeration is carried as a custom scalar instead. A type whose name landed on that of a class or an enumeration was declared alongside it, although every type of a schema shares one namespace in which a name is unique (section 3.3); such a name is now left undeclared, with a warning. The list of implemented interfaces was joined with a comma rather than an ampersand. A linkml:Any class was declared under its raw name while the fields referring to it used the camelcased one. A mixin carrying mixins of its own stayed an object type although other types implemented it. And the set of linkml:Any names lived on a mutable class attribute shared by every instance, so generating a second schema in the same process truncated it. The reference snapshot of the biolink model in this repository did not parse before this change and parses after it, with no type and no field lost. The docs gain sections on mixins, types, enumerations and fieldless classes, and record the gaps left open: no root operation type, names encoded with the reserved double underscore, interface field covariance, and implements being derived from mixins alone. Signed-off-by: jdsika --- docs/generators/graphql.rst | 100 ++++++ .../src/linkml/generators/graphqlgen.py | 122 ++++++- .../__snapshots__/biolink.graphql | 199 ++++++++--- tests/linkml/test_generators/test_graphql.py | 338 +++++++++++++++++- 4 files changed, 688 insertions(+), 71 deletions(-) diff --git a/docs/generators/graphql.rst b/docs/generators/graphql.rst index e65a75ce74..e0457a0131 100644 --- a/docs/generators/graphql.rst +++ b/docs/generators/graphql.rst @@ -47,6 +47,106 @@ Inheritance } +Mixins +^^^^^^ + +A class that cannot be instantiated on its own, because it is ``abstract`` or a +``mixin``, becomes an interface, and a class mixing it in implements it. An +interface may implement an interface, so a mixin keeps that role when it is +itself mixed into another one, and a class declares the interfaces that the +interfaces it implements declare in turn:: + + interface Base + { + a: String + } + + interface Nested implements Base + { + b: String + a: String + } + + type Consumer implements Nested & Base + { + c: String + b: String + a: String + } + + +Types +^^^^^ + +A type imported from ``linkml:types`` maps onto one of GraphQL's built-in +scalars. A type the schema brings itself has no built-in to map onto and is +declared as a custom scalar, so that the document defines every type it names:: + + types: + city code: + typeof: string + +becomes:: + + type Place + { + code: CityCode + } + + scalar CityCode + +Only a type that a field actually names is declared. A type whose name lands on +that of a class or of an enumeration is left undeclared, with a warning: GraphQL +carries every type of a schema in one namespace, in which a name is unique, +while LinkML holds its types, classes and enumerations apart. + + +Enumerations +^^^^^^^^^^^^ + +An enumeration listing ``permissible_values`` becomes a GraphQL enum. A GraphQL +enum defines one or more values, so an enumeration that draws its values from an +ontology through ``reachable_from`` cannot be one: its values are not known at +generation time. It is declared as a custom scalar instead, with a warning, so +that the fields naming it stay defined. + + +Classes without a slot +^^^^^^^^^^^^^^^^^^^^^^ + +GraphQL has no empty type: an object type and an interface each define at least +one field. A class resolving to no slot therefore carries a single +``_placeholder`` field. A class implementing an interface in that position +carries it as well, because it repeats the fields of the interfaces it +implements:: + + interface Annotation + { + _placeholder: Boolean + } + + +Known gaps +^^^^^^^^^^ + +The generated document describes types only: it names no root operation type, +which a schema has to provide, so it is not servable on its own. + +A name that is not alphanumeric throughout is encoded with ``__``, a prefix +GraphQL reserves for introspection: an enum value starting with a digit is +prefixed with it, and any other character is replaced by its Unicode name +wrapped in it, as in ``__PLUS_SIGN__``. + +A class narrowing the range of an inherited slot keeps the narrowed type, which +an interface it implements may disagree with. + +``implements`` is derived from ``mixins`` alone, never from ``is_a``, so an +abstract class that is never mixed into another becomes an interface that no +object type implements. A class listed in another class's ``mixins`` without +being declared ``abstract`` or ``mixin`` stays an object type, which the class +mixing it in may then not implement. + + Docs ---- diff --git a/packages/linkml/src/linkml/generators/graphqlgen.py b/packages/linkml/src/linkml/generators/graphqlgen.py index 609d148a51..3d6fcfdfb7 100644 --- a/packages/linkml/src/linkml/generators/graphqlgen.py +++ b/packages/linkml/src/linkml/generators/graphqlgen.py @@ -11,6 +11,19 @@ from linkml_runtime.linkml_model.meta import ClassDefinition, EnumDefinition, SlotDefinition from linkml_runtime.utils.formatutils import camelcase, lcamelcase +#: The schema that holds the LinkML built-in types, the ones that map onto GraphQL's scalars. +LINKML_TYPES_SCHEMA = "https://w3id.org/linkml/types" + +#: The scalars every GraphQL service defines (GraphQL spec, October 2021, section 3.5). +GRAPHQL_BUILTIN_SCALARS = ("Int", "Float", "String", "Boolean", "ID") + +#: An object type and an interface each have to define at least one field (GraphQL spec, +#: October 2021, sections 3.6 and 3.7), while a LinkML class may resolve to no slot. Such a +#: class carries this field instead. It is emitted only for a class that contributes no +#: field of its own, or that implements an interface in that position, so it never collides +#: with a field generated from a slot. +PLACEHOLDER_FIELD_NAME = "_placeholder" + @dataclass class GraphqlGenerator(Generator): @@ -24,13 +37,25 @@ class GraphqlGenerator(Generator): strict_naming: bool = False _permissible_value_valid_characters = re.compile("^[_A-Za-z][_0-9A-Za-z]*?$") - _types_any = [] def __post_init__(self): self.name_compatiblity = NameCompatibility(profile=NamingProfiles.graphql, do_not_fix=self.strict_naming) + self._reset_visit_state() super().__post_init__() + def _reset_visit_state(self) -> None: + """Clear the names the visitor collects, so that a run never sees those of another. + + Kept on the class, a name outlived the generator that recorded it: the next schema + holding a class of that name was then closed as if it were a scalar, leaving the + document a brace short. + """ + self._types_any: list[str] = [] + self._custom_scalars: set[str] = set() + self._declared_names: set[str] = set() + def visit_schema(self, **kwargs) -> str: + self._reset_visit_state() return self.generate_header() def generate_header(self) -> str: @@ -39,36 +64,98 @@ def generate_header(self) -> str: out += f"# version: {self.schema.version}\n" return out + def end_schema(self, **kwargs) -> str: + """Declare the types the schema brings itself, which GraphQL carries as custom scalars. + + ``visit_class_slot`` names such a type as the type of a field, and a document has + to define every type it names; a custom scalar is how GraphQL carries a type that + it has no built-in for (GraphQL spec, October 2021, section 3.5, Custom Scalars). + + A name the document already carries is left out. Every type of a GraphQL schema + shares one namespace, in which a name is unique (GraphQL spec, October 2021, + section 3.3), while LinkML holds its types, classes and enumerations apart and + lets one name stand in several of them. A built-in scalar is left out as well, + since every service defines it already (GraphQL spec, October 2021, section 3.5). + """ + for name in sorted(self._custom_scalars & self._declared_names): + logging.warning( + f"The name {name} is already declared by a class or an enumeration of the " + "schema, so the type of that name is not declared as a scalar. A field " + "naming it resolves to the class or the enumeration instead." + ) + declared = self._declared_names.union(GRAPHQL_BUILTIN_SCALARS) + return "".join(f"scalar {name}\n\n" for name in sorted(self._custom_scalars - declared)) + + def implemented_interfaces(self, cls: ClassDefinition) -> list[str]: + """Return the mixins of a class, including the ones its mixins carry themselves. + + An object type or an interface declares every interface that the interfaces it + implements declare in turn (GraphQL spec, October 2021, sections 3.6 and 3.7), so + the direct mixins alone leave out a mixin that is mixed into a mixin. + """ + interfaces: list[str] = [] + + def collect(mixins: list[str]) -> None: + for mixin in mixins: + if mixin not in interfaces: + interfaces.append(mixin) + collect(self.schema.classes[mixin].mixins) + + collect(cls.mixins) + return interfaces + def visit_class(self, cls: ClassDefinition) -> str: # no type can be declared for subtypes of "Any" if cls.class_uri == "linkml:Any": self._types_any.append(cls.name) - return f"scalar {cls.name}" - etype = "interface" if (cls.abstract or cls.mixin) and not cls.mixins else "type" - mixins = ", ".join([camelcase(mixin) for mixin in cls.mixins]) + self._declared_names.add(camelcase(cls.name)) + return f"scalar {camelcase(cls.name)}" + self._declared_names.add(camelcase(cls.name)) + # A class that cannot be instantiated is an interface, including when it is mixed + # into another one: an interface may implement an interface (GraphQL spec, October + # 2021, section 3.7), and a type implementing it needs it to be one. + etype = "interface" if cls.abstract or cls.mixin else "type" + # "implements A & B": the interfaces are separated by "&", a comma does not parse + # (GraphQL spec, October 2021, section 3.6, ImplementsInterfaces). + mixins = " & ".join([camelcase(mixin) for mixin in self.implemented_interfaces(cls)]) out = f"{etype} {camelcase(cls.name)}" + (f" implements {mixins}" if mixins else "") out = "\n".join([out, " {"]) return out + def placeholder_field(self, cls: ClassDefinition) -> str: + """Return the placeholder field a class needs to be well formed, else the empty string. + + A class resolving to no slot would close its body without a field. A class + implementing an interface in that position carries the field as well, because an + object type or interface repeats every field of the interfaces it implements + (GraphQL spec, October 2021, sections 3.6 and 3.7). + """ + interfaces = (self.all_slots(mixin) for mixin in self.implemented_interfaces(cls)) + needs_placeholder = not self.all_slots(cls) or not all(interfaces) + return f"\n {PLACEHOLDER_FIELD_NAME}: Boolean" if needs_placeholder else "" + def end_class(self, cls: ClassDefinition) -> str: if cls.name in self._types_any: return "\n\n" else: - return "\n }\n\n" + return f"{self.placeholder_field(cls)}\n }}\n\n" def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> str: if slot.range in self.schema.classes or slot.range in self.schema.slots or slot.range in self.schema.enums: slotrange = camelcase(slot.range) elif slot.range in self.schema.types: - if self.schema.types[slot.range].from_schema != "https://w3id.org/linkml/types": + if self.schema.types[slot.range].from_schema != LINKML_TYPES_SCHEMA: slotrange = camelcase(slot.range) + # A type the schema brings itself has no built-in to map onto, so the + # document has to declare it; ``end_schema`` does, where the names the + # document already carries are known. + self._custom_scalars.add(slotrange) else: - graphql_scalars = ["Int", "Float", "String", "Boolean", "ID"] if slot.range == "integer": slotrange = "Int" elif slot.range == "decimal": slotrange = "Float" - elif camelcase(slot.range) in graphql_scalars: + elif camelcase(slot.range) in GRAPHQL_BUILTIN_SCALARS: slotrange = camelcase(slot.range) else: if self.schema.types[slot.range].repr: @@ -86,19 +173,30 @@ def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: S slotrange = slotrange + "!" return f"\n {lcamelcase(aliased_slot_name)}: {slotrange}" - def visit_enum(self, enum: EnumDefinition): + def enum_name(self, enum: EnumDefinition) -> str: + """Return the name under which an enumeration is carried in the document.""" + return camelcase(enum.name).replace(" ", "") + + def visit_enum(self, enum: EnumDefinition) -> str: + name = self.enum_name(enum) if enum.permissible_values: permissible_values = [] for value in enum.permissible_values: permissible_values.append(self.name_compatiblity.compatible(value)) values = "\n ".join(permissible_values) - return f"enum {camelcase(enum.name).replace(' ', '')}\n {{\n {values}\n }}\n\n" + self._declared_names.add(name) + return f"enum {name}\n {{\n {values}\n }}\n\n" else: + # An enum type defines one or more values (GraphQL spec, October 2021, section + # 3.9), so an enumeration whose values are not known at generation time cannot + # be one. It is carried as a custom scalar instead, which leaves the fields + # naming it defined rather than pointing at a type the document never declares. logging.warning( f"Enumeration {enum.name} using `reachable_from` instead of `permissible_values` " - + "to specify permissible values is not supported yet." - + "Enumeration {enum.name} will be silently ignored!!" + "to specify permissible values is not supported yet. " + f"Enumeration {enum.name} is generated as the custom scalar {name} instead." ) + self._custom_scalars.add(name) return "" diff --git a/tests/linkml/test_biolink_model/__snapshots__/biolink.graphql b/tests/linkml/test_biolink_model/__snapshots__/biolink.graphql index b388884789..b5429984a4 100644 --- a/tests/linkml/test_biolink_model/__snapshots__/biolink.graphql +++ b/tests/linkml/test_biolink_model/__snapshots__/biolink.graphql @@ -256,7 +256,7 @@ enum StrandEnum __QUESTION_MARK__ } -type AccessibleDnaRegion implements GenomicEntity, ChemicalEntityOrGeneOrGeneProduct, PhysicalEssence, OntologyClass +type AccessibleDnaRegion implements GenomicEntity & ChemicalEntityOrGeneOrGeneProduct & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -273,6 +273,7 @@ type AccessibleDnaRegion implements GenomicEntity, ChemicalEntityOrGeneOrGenePro inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } type Activity implements ActivityAndBehavior @@ -289,10 +290,12 @@ type Activity implements ActivityAndBehavior fullName: LabelType synonym: [LabelType] category: [String]! + _placeholder: Boolean } interface ActivityAndBehavior { + _placeholder: Boolean } interface AdministrativeEntity @@ -345,6 +348,7 @@ type AnatomicalEntity implements PhysicalEssence inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } interface AnatomicalEntityToAnatomicalEntityAssociation @@ -478,6 +482,7 @@ type AnatomicalEntityToAnatomicalEntityPartOfAssociation interface Annotation { + _placeholder: Boolean } type Article @@ -590,7 +595,7 @@ type Bacterium hasAttribute: [Attribute] } -type Behavior implements OntologyClass, ActivityAndBehavior +type Behavior implements OntologyClass & ActivityAndBehavior { id: String! iri: IriType @@ -609,9 +614,10 @@ type Behavior implements OntologyClass, ActivityAndBehavior hasInput: [NamedThing] hasOutput: [NamedThing] enabledBy: [PhysicalEntity] + _placeholder: Boolean } -type BehaviorToBehavioralFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin +type BehaviorToBehavioralFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin & FrequencyQuantifier { id: String! iri: IriType @@ -709,9 +715,10 @@ type BehavioralFeature type BehavioralOutcome implements Outcome { + _placeholder: Boolean } -type BiologicalEntity implements ThingWithTaxon +interface BiologicalEntity implements ThingWithTaxon { id: String! iri: IriType @@ -729,7 +736,7 @@ type BiologicalEntity implements ThingWithTaxon inTaxonLabel: LabelType } -type BiologicalProcess implements Occurrent, OntologyClass +type BiologicalProcess implements Occurrent & OntologyClass { id: String! iri: IriType @@ -748,9 +755,10 @@ type BiologicalProcess implements Occurrent, OntologyClass hasInput: [NamedThing] hasOutput: [NamedThing] enabledBy: [PhysicalEntity] + _placeholder: Boolean } -type BiologicalProcessOrActivity implements Occurrent, OntologyClass +type BiologicalProcessOrActivity implements Occurrent & OntologyClass { id: String! iri: IriType @@ -769,6 +777,7 @@ type BiologicalProcessOrActivity implements Occurrent, OntologyClass hasInput: [NamedThing] hasOutput: [NamedThing] enabledBy: [PhysicalEntity] + _placeholder: Boolean } type BiologicalSex @@ -881,6 +890,7 @@ type Case implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } interface CaseToEntityAssociationMixin @@ -890,7 +900,7 @@ interface CaseToEntityAssociationMixin object: NamedThing! } -type CaseToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin, CaseToEntityAssociationMixin +type CaseToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin & FrequencyQuantifier & CaseToEntityAssociationMixin { id: String! iri: IriType @@ -948,7 +958,7 @@ type CaseToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAsso hasPercentage: Float } -type CausalGeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin, GeneToEntityAssociationMixin +type CausalGeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin & GeneToEntityAssociationMixin { id: String! iri: IriType @@ -1040,9 +1050,10 @@ type CellLine implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } -type CellLineAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin, EntityToDiseaseAssociationMixin +type CellLineAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin & EntityToDiseaseAssociationMixin { id: String! iri: IriType @@ -1092,7 +1103,7 @@ type CellLineAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMi diseaseContextQualifier: Disease } -type CellLineToDiseaseOrPhenotypicFeatureAssociation implements CellLineToEntityAssociationMixin, EntityToDiseaseOrPhenotypicFeatureAssociationMixin +type CellLineToDiseaseOrPhenotypicFeatureAssociation implements CellLineToEntityAssociationMixin & EntityToDiseaseOrPhenotypicFeatureAssociationMixin { id: String! iri: IriType @@ -1176,6 +1187,7 @@ type CellularOrganism implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } type ChemicalAffectsGeneAssociation @@ -1236,7 +1248,7 @@ type ChemicalAffectsGeneAssociation object: GeneOrGeneProduct! } -type ChemicalEntity implements PhysicalEssence, ChemicalOrDrugOrTreatment, ChemicalEntityOrGeneOrGeneProduct, ChemicalEntityOrProteinOrPolypeptide +type ChemicalEntity implements PhysicalEssence & ChemicalOrDrugOrTreatment & ChemicalEntityOrGeneOrGeneProduct & ChemicalEntityOrProteinOrPolypeptide { id: String! iri: IriType @@ -1255,6 +1267,7 @@ type ChemicalEntity implements PhysicalEssence, ChemicalOrDrugOrTreatment, Chemi maxToleratedDose: String isToxic: Boolean hasChemicalRole: [ChemicalRole] + _placeholder: Boolean } type ChemicalEntityAssessesNamedThingAssociation @@ -1302,6 +1315,7 @@ type ChemicalEntityAssessesNamedThingAssociation interface ChemicalEntityOrGeneOrGeneProduct { + _placeholder: Boolean } type ChemicalEntityOrGeneOrGeneProductRegulatesGeneAssociation @@ -1350,6 +1364,7 @@ type ChemicalEntityOrGeneOrGeneProductRegulatesGeneAssociation interface ChemicalEntityOrProteinOrPolypeptide { + _placeholder: Boolean } interface ChemicalEntityToEntityAssociationMixin @@ -1457,9 +1472,10 @@ type ChemicalMixture interface ChemicalOrDrugOrTreatment { + _placeholder: Boolean } -type ChemicalOrDrugOrTreatmentSideEffectDiseaseOrPhenotypicFeatureAssociation implements ChemicalToEntityAssociationMixin, EntityToDiseaseOrPhenotypicFeatureAssociationMixin +type ChemicalOrDrugOrTreatmentSideEffectDiseaseOrPhenotypicFeatureAssociation implements ChemicalToEntityAssociationMixin & EntityToDiseaseOrPhenotypicFeatureAssociationMixin { id: String! iri: IriType @@ -1510,7 +1526,7 @@ type ChemicalOrDrugOrTreatmentSideEffectDiseaseOrPhenotypicFeatureAssociation im predicate: PredicateType! } -type ChemicalOrDrugOrTreatmentToDiseaseOrPhenotypicFeatureAssociation implements ChemicalToEntityAssociationMixin, EntityToDiseaseOrPhenotypicFeatureAssociationMixin, EntityToFeatureOrDiseaseQualifiersMixin +type ChemicalOrDrugOrTreatmentToDiseaseOrPhenotypicFeatureAssociation implements ChemicalToEntityAssociationMixin & EntityToDiseaseOrPhenotypicFeatureAssociationMixin & EntityToFeatureOrDiseaseQualifiersMixin { id: String! iri: IriType @@ -1667,7 +1683,7 @@ type ChemicalToChemicalDerivationAssociation predicate: PredicateType! } -type ChemicalToDiseaseOrPhenotypicFeatureAssociation implements ChemicalToEntityAssociationMixin, EntityToDiseaseOrPhenotypicFeatureAssociationMixin +type ChemicalToDiseaseOrPhenotypicFeatureAssociation implements ChemicalToEntityAssociationMixin & EntityToDiseaseOrPhenotypicFeatureAssociationMixin { id: String! iri: IriType @@ -1957,6 +1973,7 @@ type Cohort implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } type CommonDataElement @@ -2106,7 +2123,7 @@ type ContributorAssociation qualifiers: [OntologyClass] } -type CorrelatedGeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin, GeneToEntityAssociationMixin +type CorrelatedGeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin & GeneToEntityAssociationMixin { id: String! iri: IriType @@ -2318,7 +2335,7 @@ type DiseaseOrPhenotypicFeature inTaxonLabel: LabelType } -type DiseaseOrPhenotypicFeatureExposure implements ExposureEvent, PathologicalEntityMixin +type DiseaseOrPhenotypicFeatureExposure implements ExposureEvent & PathologicalEntityMixin { id: String! type: [String] @@ -2336,10 +2353,12 @@ type DiseaseOrPhenotypicFeatureExposure implements ExposureEvent, PathologicalEn hasQualitativeValue: NamedThing iri: IriType timepoint: TimeType + _placeholder: Boolean } type DiseaseOrPhenotypicFeatureOutcome implements Outcome { + _placeholder: Boolean } interface DiseaseOrPhenotypicFeatureToEntityAssociationMixin @@ -2442,7 +2461,7 @@ interface DiseaseToEntityAssociationMixin object: NamedThing! } -type DiseaseToExposureEventAssociation implements DiseaseToEntityAssociationMixin, EntityToExposureEventAssociationMixin +type DiseaseToExposureEventAssociation implements DiseaseToEntityAssociationMixin & EntityToExposureEventAssociationMixin { id: String! iri: IriType @@ -2485,7 +2504,7 @@ type DiseaseToExposureEventAssociation implements DiseaseToEntityAssociationMixi category: [String] } -type DiseaseToPhenotypicFeatureAssociation implements FrequencyQuantifier, EntityToPhenotypicFeatureAssociationMixin, DiseaseToEntityAssociationMixin +type DiseaseToPhenotypicFeatureAssociation implements FrequencyQuantifier & EntityToPhenotypicFeatureAssociationMixin & DiseaseToEntityAssociationMixin { id: String! iri: IriType @@ -2544,7 +2563,7 @@ type DiseaseToPhenotypicFeatureAssociation implements FrequencyQuantifier, Entit anatomicalContextQualifier: String } -type Drug implements ChemicalOrDrugOrTreatment, OntologyClass +type Drug implements ChemicalOrDrugOrTreatment & OntologyClass { id: String! iri: IriType @@ -2567,6 +2586,7 @@ type Drug implements ChemicalOrDrugOrTreatment, OntologyClass highestFDAApprovalStatus: ApprovalStatusEnum drugRegulatoryStatusWorldWide: ApprovalStatusEnum routesOfDelivery: [DrugDeliveryEnum] + _placeholder: Boolean } type DrugExposure implements ExposureEvent @@ -2686,7 +2706,7 @@ type DrugToGeneInteractionExposure implements GeneGroupingMixin hasGeneOrGeneProduct: [Gene] } -type DruggableGeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin, GeneToEntityAssociationMixin +type DruggableGeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin & GeneToEntityAssociationMixin { id: String! iri: IriType @@ -2895,7 +2915,7 @@ type EntityToPhenotypicFeatureAssociation maxResearchPhase: ResearchPhaseEnum } -type EntityToPhenotypicFeatureAssociationMixin implements FrequencyQuantifier +interface EntityToPhenotypicFeatureAssociationMixin implements FrequencyQuantifier { frequencyQualifier: FrequencyValue subjectAspectQualifier: String @@ -2988,10 +3008,12 @@ type EnvironmentalProcess implements Occurrent fullName: LabelType synonym: [LabelType] category: [String]! + _placeholder: Boolean } type EpidemiologicalOutcome implements Outcome { + _placeholder: Boolean } interface EpigenomicEntity @@ -3147,7 +3169,7 @@ type ExposureEventToOutcomeAssociation implements EntityToOutcomeAssociationMixi temporalContextQualifier: TimeType } -type ExposureEventToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin +type ExposureEventToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin & FrequencyQuantifier { id: String! iri: IriType @@ -3341,7 +3363,7 @@ type Fungus hasAttribute: [Attribute] } -type Gene implements GeneOrGeneProduct, GenomicEntity, ChemicalEntityOrGeneOrGeneProduct, PhysicalEssence, OntologyClass +type Gene implements GeneOrGeneProduct & GenomicEntity & ChemicalEntityOrGeneOrGeneProduct & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -3359,6 +3381,7 @@ type Gene implements GeneOrGeneProduct, GenomicEntity, ChemicalEntityOrGeneOrGen symbol: String xref: [String] hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } type GeneAffectsChemicalAssociation @@ -3420,7 +3443,7 @@ type GeneAffectsChemicalAssociation object: ChemicalEntity! } -type GeneAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin, EntityToDiseaseAssociationMixin +type GeneAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin & EntityToDiseaseAssociationMixin { id: String! iri: IriType @@ -3486,7 +3509,7 @@ interface GeneExpressionMixin phenotypicState: DiseaseOrPhenotypicFeature } -type GeneFamily implements GeneGroupingMixin, ChemicalEntityOrGeneOrGeneProduct +type GeneFamily implements GeneGroupingMixin & ChemicalEntityOrGeneOrGeneProduct { id: String! iri: IriType @@ -3503,6 +3526,7 @@ type GeneFamily implements GeneGroupingMixin, ChemicalEntityOrGeneOrGeneProduct inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasGeneOrGeneProduct: [Gene] + _placeholder: Boolean } interface GeneGroupingMixin @@ -3635,7 +3659,7 @@ type GeneRegulatesGeneAssociation object: GeneOrGeneProduct! } -type GeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin, GeneToEntityAssociationMixin +type GeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin & GeneToEntityAssociationMixin { id: String! iri: IriType @@ -3693,7 +3717,7 @@ type GeneToDiseaseAssociation implements EntityToDiseaseAssociationMixin, GeneTo object: Disease! } -type GeneToDiseaseOrPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin, GeneToEntityAssociationMixin +type GeneToDiseaseOrPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin & FrequencyQuantifier & GeneToEntityAssociationMixin { id: String! iri: IriType @@ -4108,7 +4132,7 @@ type GeneToPathwayAssociation implements GeneToEntityAssociationMixin object: Pathway! } -type GeneToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin, GeneToEntityAssociationMixin +type GeneToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin & FrequencyQuantifier & GeneToEntityAssociationMixin { id: String! iri: IriType @@ -4184,7 +4208,7 @@ type GeneticInheritance inTaxonLabel: LabelType } -type Genome implements GenomicEntity, PhysicalEssence, OntologyClass +type Genome implements GenomicEntity & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -4201,9 +4225,10 @@ type Genome implements GenomicEntity, PhysicalEssence, OntologyClass inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } -type GenomicBackgroundExposure implements ExposureEvent, GeneGroupingMixin, PhysicalEssence, GenomicEntity, ThingWithTaxon, OntologyClass +type GenomicBackgroundExposure implements ExposureEvent & GeneGroupingMixin & PhysicalEssence & GenomicEntity & ThingWithTaxon & OntologyClass { id: String! type: [String] @@ -4225,6 +4250,7 @@ type GenomicBackgroundExposure implements ExposureEvent, GeneGroupingMixin, Phys hasBiologicalSequence: BiologicalSequence inTaxon: [OrganismTaxon] inTaxonLabel: LabelType + _placeholder: Boolean } interface GenomicEntity @@ -4280,7 +4306,7 @@ type GenomicSequenceLocalization predicate: PredicateType! } -type Genotype implements PhysicalEssence, GenomicEntity, OntologyClass +type Genotype implements PhysicalEssence & GenomicEntity & OntologyClass { id: String! iri: IriType @@ -4298,9 +4324,10 @@ type Genotype implements PhysicalEssence, GenomicEntity, OntologyClass inTaxonLabel: LabelType hasZygosity: Zygosity hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } -type GenotypeAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin, EntityToDiseaseAssociationMixin +type GenotypeAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin & EntityToDiseaseAssociationMixin { id: String! iri: IriType @@ -4350,7 +4377,7 @@ type GenotypeAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMi subject: Genotype! } -type GenotypeToDiseaseAssociation implements GenotypeToEntityAssociationMixin, EntityToDiseaseAssociationMixin +type GenotypeToDiseaseAssociation implements GenotypeToEntityAssociationMixin & EntityToDiseaseAssociationMixin { id: String! iri: IriType @@ -4493,7 +4520,7 @@ type GenotypeToGenotypePartAssociation object: Genotype! } -type GenotypeToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin, GenotypeToEntityAssociationMixin +type GenotypeToPhenotypicFeatureAssociation implements EntityToPhenotypicFeatureAssociationMixin & FrequencyQuantifier & GenotypeToEntityAssociationMixin { id: String! iri: IriType @@ -4688,7 +4715,7 @@ type GrossAnatomicalStructure hasAttribute: [Attribute] } -type Haplotype implements GenomicEntity, PhysicalEssence, OntologyClass +type Haplotype implements GenomicEntity & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -4705,6 +4732,7 @@ type Haplotype implements GenomicEntity, PhysicalEssence, OntologyClass inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } type Hospitalization @@ -4725,6 +4753,7 @@ type Hospitalization type HospitalizationOutcome implements Outcome { + _placeholder: Boolean } type Human implements SubjectOfInvestigation @@ -4743,6 +4772,7 @@ type Human implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } type IndividualOrganism implements SubjectOfInvestigation @@ -4761,6 +4791,7 @@ type IndividualOrganism implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } interface InformationContentEntity @@ -5091,6 +5122,7 @@ type Mammal implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } interface MappingCollection @@ -5112,6 +5144,7 @@ type MaterialSample implements SubjectOfInvestigation fullName: LabelType synonym: [LabelType] category: [String]! + _placeholder: Boolean } type MaterialSampleDerivationAssociation @@ -5157,7 +5190,7 @@ type MaterialSampleDerivationAssociation object: NamedThing! } -type MaterialSampleToDiseaseOrPhenotypicFeatureAssociation implements MaterialSampleToEntityAssociationMixin, EntityToDiseaseOrPhenotypicFeatureAssociationMixin +type MaterialSampleToDiseaseOrPhenotypicFeatureAssociation implements MaterialSampleToEntityAssociationMixin & EntityToDiseaseOrPhenotypicFeatureAssociationMixin { id: String! iri: IriType @@ -5232,7 +5265,7 @@ interface ModelToDiseaseAssociationMixin object: NamedThing! } -type MolecularActivity implements Occurrent, OntologyClass +type MolecularActivity implements Occurrent & OntologyClass { id: String! iri: IriType @@ -5251,6 +5284,7 @@ type MolecularActivity implements Occurrent, OntologyClass hasInput: [MolecularEntity] hasOutput: [MolecularEntity] enabledBy: [MacromolecularMachineMixin] + _placeholder: Boolean } type MolecularActivityToChemicalEntityAssociation @@ -5431,6 +5465,7 @@ type MolecularMixture type MortalityOutcome implements Outcome { + _placeholder: Boolean } type NamedThing @@ -5515,7 +5550,7 @@ type NoncodingRNAProduct inTaxonLabel: LabelType } -type NucleicAcidEntity implements GenomicEntity, ThingWithTaxon, PhysicalEssence, OntologyClass +type NucleicAcidEntity implements GenomicEntity & ThingWithTaxon & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -5538,6 +5573,7 @@ type NucleicAcidEntity implements GenomicEntity, ThingWithTaxon, PhysicalEssence hasBiologicalSequence: BiologicalSequence inTaxon: [OrganismTaxon] inTaxonLabel: LabelType + _placeholder: Boolean } type NucleicAcidSequenceMotif @@ -5558,7 +5594,7 @@ type NucleicAcidSequenceMotif inTaxonLabel: LabelType } -type NucleosomeModification implements GeneProductIsoformMixin, GenomicEntity, EpigenomicEntity +type NucleosomeModification implements GeneProductIsoformMixin & GenomicEntity & EpigenomicEntity { id: String! iri: IriType @@ -5599,6 +5635,7 @@ type ObservedExpectedFrequencyAnalysisResult interface Occurrent { + _placeholder: Boolean } type Onset @@ -5668,7 +5705,7 @@ interface OrganismTaxonToEntityAssociation object: NamedThing! } -type OrganismTaxonToEnvironmentAssociation implements OrganismTaxonToEntityAssociation +interface OrganismTaxonToEnvironmentAssociation implements OrganismTaxonToEntityAssociation { id: String! iri: IriType @@ -5711,7 +5748,7 @@ type OrganismTaxonToEnvironmentAssociation implements OrganismTaxonToEntityAssoc predicate: PredicateType! } -type OrganismTaxonToOrganismTaxonAssociation implements OrganismTaxonToEntityAssociation +interface OrganismTaxonToOrganismTaxonAssociation implements OrganismTaxonToEntityAssociation { id: String! iri: IriType @@ -5884,7 +5921,7 @@ type OrganismToOrganismAssociation object: IndividualOrganism! } -type OrganismalEntity implements SubjectOfInvestigation +interface OrganismalEntity implements SubjectOfInvestigation { id: String! iri: IriType @@ -5900,9 +5937,10 @@ type OrganismalEntity implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } -type OrganismalEntityAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin, EntityToDiseaseAssociationMixin +type OrganismalEntityAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin & EntityToDiseaseAssociationMixin { id: String! iri: IriType @@ -5954,6 +5992,7 @@ type OrganismalEntityAsAModelOfDiseaseAssociation implements ModelToDiseaseAssoc interface Outcome { + _placeholder: Boolean } type PairwiseGeneToGeneInteraction @@ -6071,6 +6110,7 @@ type Patent interface PathognomonicityQuantifier { + _placeholder: Boolean } type PathologicalAnatomicalExposure implements ExposureEvent @@ -6095,6 +6135,7 @@ type PathologicalAnatomicalExposure implements ExposureEvent type PathologicalAnatomicalOutcome implements Outcome { + _placeholder: Boolean } type PathologicalAnatomicalStructure implements PathologicalEntityMixin @@ -6113,10 +6154,12 @@ type PathologicalAnatomicalStructure implements PathologicalEntityMixin inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } interface PathologicalEntityMixin { + _placeholder: Boolean } type PathologicalProcess implements PathologicalEntityMixin @@ -6138,6 +6181,7 @@ type PathologicalProcess implements PathologicalEntityMixin hasInput: [NamedThing] hasOutput: [NamedThing] enabledBy: [PhysicalEntity] + _placeholder: Boolean } type PathologicalProcessExposure implements ExposureEvent @@ -6162,6 +6206,7 @@ type PathologicalProcessExposure implements ExposureEvent type PathologicalProcessOutcome implements Outcome { + _placeholder: Boolean } type Pathway implements OntologyClass @@ -6199,6 +6244,7 @@ type Phenomenon implements Occurrent fullName: LabelType synonym: [LabelType] category: [String]! + _placeholder: Boolean } type PhenotypicFeature @@ -6219,7 +6265,7 @@ type PhenotypicFeature inTaxonLabel: LabelType } -type PhenotypicFeatureToDiseaseAssociation implements EntityToDiseaseAssociationMixin, PhenotypicFeatureToEntityAssociationMixin +type PhenotypicFeatureToDiseaseAssociation implements EntityToDiseaseAssociationMixin & PhenotypicFeatureToEntityAssociationMixin & FrequencyQuantifier { id: String! iri: IriType @@ -6274,7 +6320,7 @@ type PhenotypicFeatureToDiseaseAssociation implements EntityToDiseaseAssociation hasPercentage: Float } -type PhenotypicFeatureToEntityAssociationMixin implements FrequencyQuantifier +interface PhenotypicFeatureToEntityAssociationMixin implements FrequencyQuantifier { frequencyQualifier: FrequencyValue predicate: PredicateType! @@ -6292,7 +6338,7 @@ type PhenotypicFeatureToEntityAssociationMixin implements FrequencyQuantifier hasPercentage: Float } -type PhenotypicFeatureToPhenotypicFeatureAssociation implements PhenotypicFeatureToEntityAssociationMixin, EntityToPhenotypicFeatureAssociationMixin +type PhenotypicFeatureToPhenotypicFeatureAssociation implements PhenotypicFeatureToEntityAssociationMixin & FrequencyQuantifier & EntityToPhenotypicFeatureAssociationMixin { id: String! iri: IriType @@ -6402,14 +6448,17 @@ type PhysicalEntity implements PhysicalEssence fullName: LabelType synonym: [LabelType] category: [String]! + _placeholder: Boolean } interface PhysicalEssence { + _placeholder: Boolean } interface PhysicalEssenceOrOccurrent { + _placeholder: Boolean } type PhysiologicalProcess implements OntologyClass @@ -6467,7 +6516,7 @@ type Plant hasAttribute: [Attribute] } -type Polypeptide implements ChemicalEntityOrGeneOrGeneProduct, ChemicalEntityOrProteinOrPolypeptide +type Polypeptide implements ChemicalEntityOrGeneOrGeneProduct & ChemicalEntityOrProteinOrPolypeptide { id: String! iri: IriType @@ -6483,6 +6532,7 @@ type Polypeptide implements ChemicalEntityOrGeneOrGeneProduct, ChemicalEntityOrP category: [String]! inTaxon: [OrganismTaxon] inTaxonLabel: LabelType + _placeholder: Boolean } type PopulationOfIndividualOrganisms implements SubjectOfInvestigation @@ -6501,6 +6551,7 @@ type PopulationOfIndividualOrganisms implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } type PopulationToPopulationAssociation @@ -6629,6 +6680,7 @@ type Procedure implements ActivityAndBehavior fullName: LabelType synonym: [LabelType] category: [String]! + _placeholder: Boolean } type ProcessRegulatesProcessAssociation @@ -6717,7 +6769,7 @@ type Protein implements GeneProductMixin inTaxonLabel: LabelType } -type ProteinDomain implements GeneGroupingMixin, ChemicalEntityOrGeneOrGeneProduct +type ProteinDomain implements GeneGroupingMixin & ChemicalEntityOrGeneOrGeneProduct { id: String! iri: IriType @@ -6734,9 +6786,10 @@ type ProteinDomain implements GeneGroupingMixin, ChemicalEntityOrGeneOrGeneProdu inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasGeneOrGeneProduct: [Gene] + _placeholder: Boolean } -type ProteinFamily implements GeneGroupingMixin, ChemicalEntityOrGeneOrGeneProduct +type ProteinFamily implements GeneGroupingMixin & ChemicalEntityOrGeneOrGeneProduct { id: String! iri: IriType @@ -6753,6 +6806,7 @@ type ProteinFamily implements GeneGroupingMixin, ChemicalEntityOrGeneOrGeneProdu inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasGeneOrGeneProduct: [Gene] + _placeholder: Boolean } type ProteinIsoform implements GeneProductIsoformMixin @@ -6897,7 +6951,7 @@ type ReactionToParticipantAssociation subject: MolecularEntity! } -type ReagentTargetedGene implements GenomicEntity, PhysicalEssence, OntologyClass +type ReagentTargetedGene implements GenomicEntity & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -6914,9 +6968,10 @@ type ReagentTargetedGene implements GenomicEntity, PhysicalEssence, OntologyClas inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } -type RegulatoryRegion implements GenomicEntity, ChemicalEntityOrGeneOrGeneProduct, PhysicalEssence, OntologyClass +type RegulatoryRegion implements GenomicEntity & ChemicalEntityOrGeneOrGeneProduct & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -6933,10 +6988,12 @@ type RegulatoryRegion implements GenomicEntity, ChemicalEntityOrGeneOrGeneProduc inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } interface RelationshipQuantifier { + _placeholder: Boolean } type RelationshipType @@ -7025,6 +7082,7 @@ type RNAProductIsoform implements GeneProductIsoformMixin interface SensitivityQuantifier { + _placeholder: Boolean } type SequenceAssociation @@ -7113,7 +7171,7 @@ type SequenceFeatureRelationship object: NucleicAcidEntity! } -type SequenceVariant implements GenomicEntity, PhysicalEssence, OntologyClass +type SequenceVariant implements GenomicEntity & PhysicalEssence & OntologyClass { iri: IriType type: [String] @@ -7131,6 +7189,7 @@ type SequenceVariant implements GenomicEntity, PhysicalEssence, OntologyClass hasGene: [Gene] hasBiologicalSequence: BiologicalSequence id: String! + _placeholder: Boolean } interface SequenceVariantModulatesTreatmentAssociation @@ -7325,10 +7384,12 @@ type SocioeconomicExposure implements ExposureEvent type SocioeconomicOutcome implements Outcome { + _placeholder: Boolean } interface SpecificityQuantifier { + _placeholder: Boolean } type Study @@ -7407,6 +7468,7 @@ type StudyVariable interface SubjectOfInvestigation { + _placeholder: Boolean } type TaxonToTaxonAssociation @@ -7544,7 +7606,7 @@ type TranscriptToGeneRelationship object: Gene! } -type TranscriptionFactorBindingSite implements GenomicEntity, ChemicalEntityOrGeneOrGeneProduct, PhysicalEssence, OntologyClass +type TranscriptionFactorBindingSite implements GenomicEntity & ChemicalEntityOrGeneOrGeneProduct & PhysicalEssence & OntologyClass { id: String! iri: IriType @@ -7561,9 +7623,10 @@ type TranscriptionFactorBindingSite implements GenomicEntity, ChemicalEntityOrGe inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasBiologicalSequence: BiologicalSequence + _placeholder: Boolean } -type Treatment implements ExposureEvent, ChemicalOrDrugOrTreatment +type Treatment implements ExposureEvent & ChemicalOrDrugOrTreatment { id: String! iri: IriType @@ -7581,9 +7644,10 @@ type Treatment implements ExposureEvent, ChemicalOrDrugOrTreatment hasDevice: [Device] hasProcedure: [Procedure] timepoint: TimeType + _placeholder: Boolean } -type VariantAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin, EntityToDiseaseAssociationMixin +type VariantAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMixin & EntityToDiseaseAssociationMixin { id: String! iri: IriType @@ -7633,7 +7697,7 @@ type VariantAsAModelOfDiseaseAssociation implements ModelToDiseaseAssociationMix subject: SequenceVariant! } -type VariantToDiseaseAssociation implements VariantToEntityAssociationMixin, EntityToDiseaseAssociationMixin +type VariantToDiseaseAssociation implements VariantToEntityAssociationMixin & EntityToDiseaseAssociationMixin { id: String! iri: IriType @@ -7780,7 +7844,7 @@ type VariantToGeneExpressionAssociation implements GeneExpressionMixin phenotypicState: DiseaseOrPhenotypicFeature } -type VariantToPhenotypicFeatureAssociation implements VariantToEntityAssociationMixin, EntityToPhenotypicFeatureAssociationMixin +type VariantToPhenotypicFeatureAssociation implements VariantToEntityAssociationMixin & EntityToPhenotypicFeatureAssociationMixin & FrequencyQuantifier { id: String! iri: IriType @@ -7838,7 +7902,7 @@ type VariantToPhenotypicFeatureAssociation implements VariantToEntityAssociation hasPercentage: Float } -type VariantToPopulationAssociation implements VariantToEntityAssociationMixin, FrequencyQuantifier, FrequencyQualifierMixin +type VariantToPopulationAssociation implements VariantToEntityAssociationMixin & FrequencyQuantifier & FrequencyQualifierMixin { id: String! iri: IriType @@ -7920,6 +7984,7 @@ type Virus implements SubjectOfInvestigation inTaxon: [OrganismTaxon] inTaxonLabel: LabelType hasAttribute: [Attribute] + _placeholder: Boolean } type WebPage @@ -7966,3 +8031,21 @@ type Zygosity hasQualitativeValue: NamedThing iri: IriType } + +scalar BiologicalSequence + +scalar FrequencyValue + +scalar IriType + +scalar LabelType + +scalar NarrativeText + +scalar PredicateType + +scalar SymbolType + +scalar TimeType + +scalar Unit diff --git a/tests/linkml/test_generators/test_graphql.py b/tests/linkml/test_generators/test_graphql.py index 18c8e62516..5eb86fbd5d 100644 --- a/tests/linkml/test_generators/test_graphql.py +++ b/tests/linkml/test_generators/test_graphql.py @@ -1,7 +1,8 @@ import logging +from pathlib import Path import pytest -from graphql import parse +from graphql import build_schema, parse, validate_schema from linkml.generators.graphqlgen import GraphqlGenerator @@ -112,3 +113,338 @@ def test_graphql_validity(kitchen_sink_path): + f"{str(ex)}\n" + "^^^^^^^ End Error Message ^^^^^^^" ) + + +EMPTY_CLASS_SCHEMA = """ +id: https://example.org/empty +name: empty +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/empty/ +default_prefix: ex +default_range: string +imports: [linkml:types] +classes: + Marker: + description: A class that carries no slot. + AbstractMarker: + abstract: true + description: An abstract class that carries no slot. + Holder: + attributes: + marker: {range: Marker} +""" + +CUSTOM_TYPE_SCHEMA = """ +id: https://example.org/types +name: types +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/types/ +default_prefix: ex +default_range: string +imports: [linkml:types] +types: + city code: + typeof: string + unused code: + typeof: string + int: + typeof: string +classes: + Place: + attributes: + code: {range: city code} + ranking: {range: int} +""" + +MIXIN_SCHEMA = """ +id: https://example.org/mixin +name: mixin +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/mixin/ +default_prefix: ex +default_range: string +imports: [linkml:types] +classes: + BareMixin: + mixin: true + description: A mixin that carries no slot. + RichMixin: + mixin: true + attributes: + tag: {} + Consumer: + mixins: [BareMixin, RichMixin] + attributes: + name: {} +""" + +NESTED_MIXIN_SCHEMA = """ +id: https://example.org/nested +name: nested +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/nested/ +default_prefix: ex +default_range: string +imports: [linkml:types] +classes: + Base: + mixin: true + attributes: + a: {} + Nested: + mixin: true + mixins: [Base] + attributes: + b: {} + Consumer: + mixins: [Nested] + attributes: + c: {} +""" + +ANY_SCHEMA = """ +id: https://example.org/any +name: any +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/any/ +default_prefix: ex +default_range: string +imports: [linkml:types] +classes: + any object: + class_uri: linkml:Any + Holder: + attributes: + payload: {range: any object} +""" + +# The class of the name that ANY_SCHEMA declares to be "linkml:Any" is an ordinary one here. +PLAIN_SAME_NAME_SCHEMA = """ +id: https://example.org/plain +name: plain +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/plain/ +default_prefix: ex +default_range: string +imports: [linkml:types] +classes: + any object: + attributes: + label: {} +""" + +# LinkML holds its types, classes and enumerations apart, so one name may stand in several +# of them; GraphQL carries every type in one namespace. +COLLIDING_NAME_SCHEMA = """ +id: https://example.org/collide +name: collide +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/collide/ +default_prefix: ex +default_range: string +imports: [linkml:types] +types: + person: + typeof: string + colour: + typeof: string +enums: + Colour: + permissible_values: + red: +classes: + Person: + attributes: + name: {} + Holder: + attributes: + who: {range: Person} + reference: {range: person} + shade: {range: colour} +""" + +DYNAMIC_ENUM_SCHEMA = """ +id: https://example.org/dynamic +name: dynamic +prefixes: + linkml: https://w3id.org/linkml/ + ex: https://example.org/dynamic/ +default_prefix: ex +default_range: string +imports: [linkml:types] +enums: + VehicleKind: + reachable_from: + source_ontology: obo:vehicle + source_nodes: [VEHICLE:1] +classes: + Car: + attributes: + kind: {range: VehicleKind} +""" + + +def generate(tmp_path: Path, schema_text: str, name: str = "schema.yaml") -> str: + """Write a schema and return the GraphQL document generated from it.""" + schema = tmp_path / name + schema.write_text(schema_text, encoding="utf-8") + return GraphqlGenerator(str(schema)).serialize() + + +def schema_errors(generated: str) -> list[str]: + """Return what the GraphQL type system rejects in a document, bar the missing query root. + + ``build_schema`` raises when the document does not parse. The generator names no root + operation type, which a schema has to provide (GraphQL spec, October 2021, section + 3.3.1); that gap is left aside here so the remaining rules can be checked. + """ + errors = validate_schema(build_schema(generated)) + return [str(error) for error in errors if "Query root type must be provided" not in str(error)] + + +@pytest.mark.parametrize( + "schema_text", + [ + pytest.param(EMPTY_CLASS_SCHEMA, id="class-without-a-slot"), + pytest.param(CUSTOM_TYPE_SCHEMA, id="type-of-the-schema-itself"), + pytest.param(MIXIN_SCHEMA, id="two-mixins"), + pytest.param(NESTED_MIXIN_SCHEMA, id="mixin-of-a-mixin"), + pytest.param(ANY_SCHEMA, id="any-class"), + pytest.param(COLLIDING_NAME_SCHEMA, id="type-sharing-a-name"), + pytest.param(DYNAMIC_ENUM_SCHEMA, id="enum-without-permissible-values"), + ], +) +def test_the_generated_document_is_a_valid_schema(tmp_path, schema_text): + """Every generated document parses and satisfies the GraphQL type system.""" + assert schema_errors(generate(tmp_path, schema_text)) == [] + + +def test_a_class_without_a_slot_still_defines_a_field(tmp_path): + """An object type and an interface each define at least one field. + + GraphQL spec, October 2021, sections 3.6 and 3.7. A LinkML class may carry no slot, + and an empty body does not parse. + """ + generated = generate(tmp_path, EMPTY_CLASS_SCHEMA) + assert "type Marker\n {\n _placeholder: Boolean\n }" in generated + assert "interface AbstractMarker\n {\n _placeholder: Boolean\n }" in generated + # A class with a slot of its own is left alone. + assert "_placeholder" not in generated.split("type Holder")[1].split("}")[0] + + +def test_an_implementation_repeats_the_field_of_a_fieldless_interface(tmp_path): + """An object type defines every field of the interfaces it implements. + + GraphQL spec, October 2021, section 3.6. The placeholder an interface needs is + therefore part of the implementing type as well. + """ + generated = generate(tmp_path, MIXIN_SCHEMA) + assert "interface BareMixin\n {\n _placeholder: Boolean\n }" in generated + consumer = generated.split("type Consumer")[1].split("}")[0] + assert "_placeholder: Boolean" in consumer + + +def test_interfaces_are_separated_by_an_ampersand(tmp_path): + """``implements`` separates interfaces with "&"; a comma does not parse. + + GraphQL spec, October 2021, section 3.6, ImplementsInterfaces. + """ + generated = generate(tmp_path, MIXIN_SCHEMA) + assert "type Consumer implements BareMixin & RichMixin" in generated + + +def test_a_mixin_of_a_mixin_is_an_interface_and_is_implemented_along(tmp_path): + """A mixin stays an interface when it is mixed into another, and is declared by its users. + + An interface may implement an interface, and a type declares the interfaces that the + interfaces it implements declare in turn (GraphQL spec, October 2021, sections 3.6 + and 3.7). + """ + generated = generate(tmp_path, NESTED_MIXIN_SCHEMA) + assert "interface Nested implements Base" in generated + assert "type Consumer implements Nested & Base" in generated + + +def test_a_type_of_the_schema_itself_is_declared_as_a_custom_scalar(tmp_path): + """A document defines every type it names, and a custom scalar carries one GraphQL lacks. + + GraphQL spec, October 2021, section 3.5, Custom Scalars. + """ + generated = generate(tmp_path, CUSTOM_TYPE_SCHEMA) + assert "code: CityCode" in generated + assert "scalar CityCode" in generated + + +def test_a_type_no_slot_names_is_not_declared(tmp_path): + """Only the types a field names are declared, so an unused one stays out of the document.""" + generated = generate(tmp_path, CUSTOM_TYPE_SCHEMA) + assert "scalar CityCode" in generated + assert "scalar UnusedCode" not in generated + + +def test_a_type_named_after_a_built_in_scalar_is_not_redeclared(tmp_path): + """A built-in scalar is defined by every service, so the document does not repeat it. + + GraphQL spec, October 2021, section 3.5. The type ``int`` of the schema collapses onto + the built-in ``Int`` rather than being declared, which is the behaviour of record; the + type it is declared with is lost in the process. + """ + generated = generate(tmp_path, CUSTOM_TYPE_SCHEMA) + assert "ranking: Int" in generated + assert "scalar Int" not in generated + + +def test_a_type_sharing_a_name_with_a_class_or_an_enum_is_not_declared_twice(tmp_path, caplog): + """Every type of a schema shares one namespace, in which a name is unique. + + GraphQL spec, October 2021, section 3.3. A LinkML type whose name lands on that of a + class or of an enumeration is therefore left undeclared rather than repeated, which + would make the document unloadable. + """ + with caplog.at_level(logging.WARNING): + generated = generate(tmp_path, COLLIDING_NAME_SCHEMA) + assert "type Person" in generated + assert "scalar Person" not in generated + assert "enum Colour" in generated + assert "scalar Colour" not in generated + assert "Person" in caplog.text + assert "Colour" in caplog.text + + +def test_an_enum_without_permissible_values_is_declared_as_a_scalar(tmp_path): + """An enum type defines one or more values, so an enumeration short of them is a scalar. + + GraphQL spec, October 2021, section 3.9. The values of an enumeration reached from an + ontology are not known at generation time; leaving it out altogether left the fields + naming it pointing at a type the document never declared. + """ + generated = generate(tmp_path, DYNAMIC_ENUM_SCHEMA) + assert "kind: VehicleKind" in generated + assert "scalar VehicleKind" in generated + assert "enum VehicleKind" not in generated + + +def test_an_any_class_is_declared_under_the_name_its_fields_use(tmp_path): + """A class standing for ``linkml:Any`` is a scalar named as the fields naming it are.""" + generated = generate(tmp_path, ANY_SCHEMA) + assert "scalar AnyObject" in generated + assert "payload: AnyObject" in generated + + +def test_one_generator_does_not_see_the_any_class_of_another(tmp_path): + """The names a run collects stay with it. + + Held on the class, the name of a ``linkml:Any`` class outlived its generator, and the + class of that name in the next schema was closed as if it were a scalar. + """ + generate(tmp_path, ANY_SCHEMA, name="any.yaml") + generated = generate(tmp_path, PLAIN_SAME_NAME_SCHEMA, name="plain.yaml") + parse(generated) + assert "type AnyObject\n {\n label: String\n }" in generated