# Copyright The DeviceChain Authors # SPDX-License-Identifier: Apache-2.0 # --------------------------------------------------------------------------- # DeviceChain GraphQL schema: device-management — tenant plane. # # Endpoint https:///api/device-management/graphql # Authorize tenant access token # All areas https://docs.devicechain.io/schema/index.json # # Generated from backend/services/device-management/graphql/schema.graphql. # Introspection is disabled by default on a DeviceChain instance, so this file # is how to read the API without deploying one. # --------------------------------------------------------------------------- # # Device Management GraphQL Schema # # Descriptions: a """string""" above an element is its PUBLIC description — served by # introspection, read by every GraphQL tool, published on the docs site — and may not # cite a decision record. A # comment is a maintainer note. backend/tools/docsgql/sdldesc # enforces both. # Base elements in gorm RDB model. """Fields every stored record carries: a server-assigned id and its timestamps.""" interface Model { """ Server-assigned numeric identifier, as a string. Records are normally addressed by token; the ById lookups take this value. """ id: ID! """When the record was created, as an RFC 3339 timestamp.""" createdAt: String """When the record was last written, as an RFC 3339 timestamp.""" updatedAt: String """Always null: deleted records are not returned by this API.""" deletedAt: String } """A record addressed by a token, an identifier unique within the tenant among records of its kind.""" interface TokenReference { """ Identifier of the record, unique within the tenant among records of its kind. Letters, digits, hyphens and underscores, starting with a letter or digit; at most 128 characters. """ token: String! } """A record with a human-readable name and description.""" interface NamedEntity { """Human-readable name. Null when none is set.""" name: String """Free-text description. Null when none is set.""" description: String } """ Presentation hints a user interface can use when showing the record. The server stores them as text and does not interpret them. """ interface BrandedEntity { """URL of an image representing the record.""" imageUrl: String """Name of an icon representing the record. Not checked against any icon set.""" icon: String """Background colour to display the record with, such as a CSS colour. Not validated.""" backgroundColor: String """Foreground (text) colour to display the record with, such as a CSS colour. Not validated.""" foregroundColor: String """Border colour to display the record with, such as a CSS colour. Not validated.""" borderColor: String } """A record that carries caller-defined metadata.""" interface MetadataEntity { """Caller-defined metadata as a JSON document, or null when none is set.""" metadata: String } """ Where a page of search results sits in the full result set. Positions are 1-based and inclusive. """ type SearchResultsPagination { """Position of the first result on this page within the full result set (1-based).""" pageStart: Int """ Position of the last result on this page within the full result set (1-based, inclusive). Lower than pageStart when the page is past the end of the results. """ pageEnd: Int """Number of records matching the criteria across all pages.""" totalRecords: Int } # The input analogue of the pageNumber/pageSize pair the search-criteria inputs carry # inline; resolved through rdb.Pagination, so the same clamp applies. """Which page of results to return.""" input PaginationInput { """Page to return, starting at 1. Values below 1 are read as 1.""" pageNumber: Int! """Results per page. Below 1 means the default of 100; above 1000 is capped at 1000.""" pageSize: Int! } # Represents a type or class of devices """ A kind of device: what it is (manufacturer and model), how it is displayed, and the device profile that defines what its devices measure and accept. Every device has exactly one device type, and many types may share one profile. """ type DeviceType implements Model & TokenReference & NamedEntity & BrandedEntity & MetadataEntity { """Server-assigned numeric identifier, as a string. Address a device type by its token instead where you can.""" id: ID! """When the device type was created, as an RFC 3339 timestamp.""" createdAt: String """When the device type was last written, as an RFC 3339 timestamp.""" updatedAt: String """Always null: deleted device types are not returned.""" deletedAt: String """Identifier of the device type, unique within the tenant.""" token: String! """Human-readable name, up to 128 characters. Null when none is set.""" name: String """Free-text description, up to 1024 characters. Null when none is set.""" description: String """URL of an image representing the device type, up to 512 characters.""" imageUrl: String """Name of an icon representing the device type, up to 128 characters. Not checked against any icon set.""" icon: String """Background colour to display the device type with, up to 32 characters. Not validated.""" backgroundColor: String """Foreground (text) colour to display the device type with, up to 32 characters. Not validated.""" foregroundColor: String """Border colour to display the device type with, up to 32 characters. Not validated.""" borderColor: String """ The device profile this type adopts, or null when it adopts none. Devices of a type with no profile have no declared metrics or commands. """ profile: DeviceProfile """ Manufacturer of the devices of this type: free text, up to 128 characters. facetValues lists the values already in use. """ manufacturer: String """ Model of the devices of this type: free text, up to 128 characters. facetValues lists the values already in use. """ model: String """Caller-defined metadata as a JSON document, or null when none is set.""" metadata: String } # A discovery facet whose in-use values suggest completions in the # authoring UI. """A free-text classification field whose values in use facetValues can list.""" enum DeviceFacet { """The manufacturer field of device types.""" MANUFACTURER """The model field of device types.""" MODEL """The category field of device profiles.""" CATEGORY } # Represents a device profile — the reusable, tenant-scoped capability # contract a device type adopts. Distinct from the type's taxonomy/appearance; many # types may share one profile. It is the home for the metric and command # definitions and for the profile's detection rules, # which are the one alarm-authoring path since the standalone alarm-definition # model was torn down. """ The capability contract a device type adopts: the metrics its devices report, the commands they accept, the detection rules evaluated against them and whether they report their own position. What you edit is a draft; publishDeviceProfile freezes the draft into a numbered version, and devices use only the active published version. Many device types may share one profile. """ type DeviceProfile implements Model & TokenReference & NamedEntity & MetadataEntity { """Server-assigned numeric identifier, as a string. Address a profile by its token instead where you can.""" id: ID! """When the profile was created, as an RFC 3339 timestamp.""" createdAt: String """When the profile was last written, as an RFC 3339 timestamp.""" updatedAt: String """Always null: deleted profiles are not returned.""" deletedAt: String """ Identifier of the profile, unique within the tenant. It can be changed with renameDeviceProfile only while the profile has never been published and no device type currently adopts it. """ token: String! """Human-readable name, up to 128 characters. Null when none is set.""" name: String """Free-text description, up to 1024 characters. Null when none is set.""" description: String """ Functional class of the devices, such as thermostat or meter: free text, up to 64 characters. facetValues lists the values already in use. """ category: String """Caller-defined metadata as a JSON document, or null when none is set.""" metadata: String # The published version a device currently resolves through this profile # (versioning); null until the profile is first published — an # unpublished profile's definitions are inert (a device resolves nothing). """ The published version devices currently use, or null if the profile has never been published. Until then devices of this profile have no declared metrics or commands. """ activeVersion: Int # A profile is a shared contract, so editing its definitions affects every adopting # type; the authoring UI surfaces this before a publish. """Number of device types that currently adopt this profile.""" deviceTypeCount: Int! """ The metric definitions in the draft. Devices use the active published version, so a change here takes effect only when the profile is published. """ metricDefinitions: [MetricDefinition!]! """ The command definitions in the draft. Devices use the active published version, so a change here takes effect only when the profile is published. """ commandDefinitions: [CommandDefinition!]! """ The detection rules in the draft. Devices use the active published version, so a change here takes effect only when the profile is published. """ detectionRules: [DetectionRule!]! # Note this is a single nullable object and not a list — a device reports its position # once — so null is the whole display rule: no declaration means no map, no track, no # last-fix panel. """ The draft position declaration: present when devices of this profile report their own position, null when they do not. Like the other draft content, it reaches devices only when the profile is published. """ location: DeviceLocationDeclaration """ The profile's DRAFT device-visible configuration declaration: the shared attribute keys a device of this profile may see. Empty when nothing is declared, in which case a device sees an empty document. Like every draft field, it reaches devices only when the profile is published; the declaration frozen into each published version never changes. """ configurationDeclaration: [ConfigurationKey!]! } """One shared attribute key declared as device-visible configuration on a device profile.""" type ConfigurationKey { """The shared attribute key.""" key: String! """How the key's value is typed: STRING, LONG, DOUBLE, BOOLEAN or JSON.""" valueType: String! """An optional human-readable note.""" description: String } """ One key of a configuration declaration. The key must follow the attribute-key grammar (letters, digits, hyphens and underscores, starting with a letter or digit), be unique within the declaration, and carry a valid value type. """ input ConfigurationKeyInput { """The shared attribute key.""" key: String! """How the key's value is typed: STRING, LONG, DOUBLE, BOOLEAN or JSON.""" valueType: String! """An optional human-readable note.""" description: String } # Declares that devices built on a profile report THEIR OWN position, and what an # operator should expect of it. Its presence is the declaration; both fields inside # are optional refinements, so an object with neither set still means "this device # reports position" and is a different statement from a null declaration. # # It describes and displays — it never permits. A device that reports a position # before anyone declares it keeps every fix it sent; the platform stores the data and # surfaces the gap, rather than discarding data on the strength of an unedited # profile. # # The coordinate contract is fixed platform-wide and cannot be overridden per device # or per profile: WGS84 / EPSG:4326 decimal degrees, elevation in metres above the # ellipsoid, accuracy in metres, speed in metres per second, heading in degrees # clockwise from true north in [0, 360). """ A statement that devices report their own position, with what to expect of it. The object being present is the statement; both fields are optional, so an empty object still means the devices report position. It is descriptive only: location data is accepted and stored whether or not a declaration exists. """ type DeviceLocationDeclaration { """ Horizontal accuracy to expect from these devices, in metres, or null when not stated. Descriptive only: a less accurate fix is still stored unchanged. """ expectedAccuracyMeters: Float # Nothing alarms on it here — an absence rule on the profile is the mechanism for that. """ How often a position fix is expected, in seconds, or null when not stated. Descriptive only: nothing is raised when fixes arrive less often. """ expectedUpdateIntervalSeconds: Int } """ Sets a profile's position declaration. Supplying it states that the devices report their own position; an empty object makes that statement with no expectations. """ input DeviceLocationDeclarationInput { """ Horizontal accuracy to expect, in metres. Must be a positive finite number when sent; omit it or send null to state no expectation. """ expectedAccuracyMeters: Float """ How often a position fix is expected, in seconds. Must be positive when sent; omit it or send null to state no expectation. """ expectedUpdateIntervalSeconds: Int } # An immutable published snapshot of a device profile's whole capability set # (versioning): its metric, command, and detection-rule definitions # frozen together. # The snapshot blob is deliberately omitted here — the list stays light; a device # resolves the active version server-side, and rollback re-points it by version. """ An immutable, published snapshot of a profile's metric, command and detection-rule definitions and its position declaration. Versions are never deleted while the profile exists; rollbackDeviceProfile makes an earlier one active again. """ type DeviceProfileVersion { """Version number: 1 for the first publish of the profile, increasing by one with each publish.""" version: Int! """Optional label given at publish time, such as a release name. Not interpreted.""" label: String """Optional notes given at publish time.""" description: String """When the version was published, as an RFC 3339 timestamp.""" publishedAt: String! """Who published it: their username, or their email when there is no username.""" publishedBy: String } # Represents a typed, unit-bearing metric declared on a device profile. """ A measurement the devices of a profile report: its key, numeric type, unit and allowed values. Inbound measurements whose key matches are validated against it; a measurement whose key is not declared is not validated. """ type MetricDefinition implements Model & TokenReference & NamedEntity & MetadataEntity { """Server-assigned numeric identifier, as a string. Address a metric definition by its token instead where you can.""" id: ID! """When the metric definition was created, as an RFC 3339 timestamp.""" createdAt: String """When the metric definition was last written, as an RFC 3339 timestamp.""" updatedAt: String """Always null: deleted metric definitions are not returned.""" deletedAt: String """Identifier of the metric definition, unique within the tenant.""" token: String! """Human-readable name, up to 128 characters. Null when none is set.""" name: String """Free-text description, up to 1024 characters. Null when none is set.""" description: String """The profile this metric is declared on.""" deviceProfile: DeviceProfile! """ The key a measurement uses to report this metric, unique within the profile and at most 128 characters. """ metricKey: String! """The value type: DOUBLE, INT or BOOLEAN. A BOOLEAN value is stored as 0 or 1.""" dataType: String! """ Unit of measure, such as a UCUM code (Cel, kW). It is carried with each measurement of this metric; values are not converted. """ unit: String """ Smallest value accepted for a DOUBLE or INT metric, or null for no lower bound. A measurement below it is rejected. """ minValue: Float """ Largest value accepted for a DOUBLE or INT metric, or null for no upper bound. A measurement above it is rejected. """ maxValue: Float """ The allowed values as a JSON array of strings, or null when any value is allowed. A measurement must equal one of them exactly. A value that is not a non-empty array of strings restricts nothing. """ enum: String """Optional semantic tag for the metric, such as a Web of Things type. Stored and returned; not interpreted.""" descriptor: String """Caller-defined metadata as a JSON document, or null when none is set.""" metadata: String } """Fields for a new metric definition.""" input MetricDefinitionCreateRequest { """Identifier for the new metric definition. A token already in use in the tenant is refused with CONFLICT.""" token: String! """Token of the profile to declare the metric on. An unknown token fails the request.""" deviceProfileToken: String! """ The key measurements will use for this metric, at most 128 characters. A key the profile already declares is refused with CONFLICT. """ metricKey: String! """Human-readable name, up to 128 characters.""" name: String """Free-text description, up to 1024 characters.""" description: String """ The value type: DOUBLE, INT or BOOLEAN. STRING is refused, because a metric must be numeric; report string values as attributes instead. """ dataType: String! """Unit of measure, such as a UCUM code (Cel, kW).""" unit: String """Smallest value accepted for a DOUBLE or INT metric.""" minValue: Float """Largest value accepted for a DOUBLE or INT metric.""" maxValue: Float """Allowed values as a JSON array of strings. Must be valid JSON.""" enum: String """Optional semantic tag, such as a Web of Things type.""" descriptor: String """Caller-defined metadata. Must be valid JSON; an empty string means none.""" metadata: String } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. # # dataType is validated ONLY when the caller names it: an absent field has nothing to # validate, and checking the stored value instead would refuse a rename over a type the # caller never sent. """ A partial update to a metric definition. Omit a field to leave the stored value alone, send a value to set it, or send an explicit null to clear it; the fields that cannot be cleared say so. The definition is named by the mutation's token argument, so there is no token here. Any refusal leaves the definition unchanged. """ input MetricDefinitionUpdateRequest { """ Token of another profile to move the definition to. An explicit null or a blank value is refused, and an unknown token fails the whole update. """ deviceProfileToken: String """ New key for the metric, unique within the profile the definition ends up in. An explicit null or a blank value is refused. """ metricKey: String """New name, or null to clear it.""" name: String """New description, or null to clear it.""" description: String """New value type: DOUBLE, INT or BOOLEAN. An explicit null is refused.""" dataType: String """New unit, or null to clear it.""" unit: String """New lower bound, or null to remove it.""" minValue: Float """New upper bound, or null to remove it.""" maxValue: Float """New allowed values as a JSON array of strings, or null to allow any value.""" enum: String """New semantic tag, or null to clear it.""" descriptor: String """New metadata (valid JSON, replacing the stored document), or null to clear it.""" metadata: String } """Criteria for searching metric definitions. Results are newest first.""" input MetricDefinitionSearchCriteria { """Page to return, starting at 1. Values below 1 are read as 1.""" pageNumber: Int! """Results per page. Below 1 means the default of 100; above 1000 is capped at 1000.""" pageSize: Int! """Return only definitions on the profile with this token. An unknown token matches nothing.""" deviceProfile: String """Return only definitions with exactly this metric key.""" metricKey: String } """One page of metric definitions matching a search.""" type MetricDefinitionSearchResults { """The metric definitions on this page.""" results: [MetricDefinition!]! """Where this page sits in the full result set.""" pagination: SearchResultsPagination! } # The enqueue gate, evaluated here because this service owns the # command vocabulary. A rejection is a verdict, not a GraphQL error, so a caller can tell # an invalid command (relayable to its client) from an unavailable check (fail closed). """ Whether a command may be sent to a device. A refusal is reported here rather than as a GraphQL error; an error means the check itself could not be made. """ type CommandEnqueueVerdict { """True when the command may be sent.""" allowed: Boolean! """ Why the command was refused, as a stable code; null when allowed. One of DEVICE_NOT_FOUND (no device has the token), COMMAND_NOT_IN_VOCABULARY (the device's published profile declares commands and this key is not one of them) or PAYLOAD_SCHEMA_VIOLATION (the payload does not satisfy the command's parameters). Branch on this, not on reason. """ code: String """ Why the command was refused, in words a person can act on; null when allowed. The wording may change; use code to decide what to do. """ reason: String } """One device a batch command check refused, and why.""" type BatchEnqueueRefusal { """The refused device's token, exactly as it appeared in the request.""" deviceToken: String! """ Why the command was refused, as a stable code: DEVICE_NOT_FOUND, COMMAND_NOT_IN_VOCABULARY or PAYLOAD_SCHEMA_VIOLATION, with the same meanings as in validateCommandEnqueue. Branch on this, not on reason. """ code: String! """Why the command was refused, in words a person can act on. The wording may change.""" reason: String! } """One page of the devices an entity group contains, for use as the target of a command.""" type DeviceGroupTargetPage { """ True when the group cannot be a command target. code and reason then explain why, and the empty deviceTokens list does not mean the group has no members. """ rejected: Boolean! """ Why the group was refused, as a stable code; null when not rejected. One of GROUP_NOT_FOUND, GROUP_NOT_A_DEVICE_GROUP (it contains something other than devices), GROUP_NOT_PUBLISHED (a dynamic group that was never published), GROUP_VERSION_NOT_FOUND (the requested version does not exist) or GROUP_NOT_VERSIONED (a version was requested for a static group). """ code: String """Why the group was refused, in words a person can act on; null when not rejected.""" reason: String """The device tokens on this page, in ascending order of the devices' ids.""" deviceTokens: [String!]! # It is a String rather than an Int because it carries a row id, and GraphQL's Int is # a signed 32-bit value — an id beyond that would fail to marshal on the one field # whose entire job is to not lose members. """ Opaque cursor to pass as afterId for the next page. Null when this page holds fewer tokens than the limit; a full page always carries a cursor, so a walk can end with an empty page. """ nextCursor: String # Record it: it is what lets an audit answer "what did this group MEAN when the # command fired?" after someone edits the selector. """The dynamic group's published version this page was resolved from; null for a static group.""" resolvedVersion: Int } # This is a snapshot copy frozen at publish — deliberately NOT a CommandDefinition, # because it has no id or token to address: the draft row it was captured from may since # have been edited or deleted. """ One command a device accepts, as frozen in its profile's active published version. To edit commands, use the profile's command definitions and publish the profile. """ type PublishedCommand { """The key used to send this command.""" commandKey: String! """Human-readable name, or null when none was set.""" name: String """Free-text description, or null when none was set.""" description: String """The command's parameters as a JSON array of parameter descriptors, or null when it takes no structured arguments.""" parameterSchema: String } """The commands a device currently accepts, from its profile's active published version.""" type DeviceCommandVocabulary { """ True when the device accepts only the listed commands. False when its published profile declares no commands, or it has no published profile: any command key is then accepted, and commands is empty. """ constrained: Boolean! """The accepted commands; empty when not constrained.""" commands: [PublishedCommand!]! } # That distinction is the whole reason this type exists. A client that read the draft # would show and hide a position panel on an unpublished edit, which is the same # publish-boundary confusion deviceCommandVocabulary was added to prevent for commands. """ Whether a device reports its own position, according to the active published version of its profile (not the draft). Location data is accepted whether or not it is declared, so an undeclared device may still have stored positions. """ type DeviceLocationCapability { """ True when the published version declares that the device reports position. False when the device's type has no profile, the profile was never published, or the published version declares nothing. """ declared: Boolean! """ The declaration, or null when declared is false. An empty object is a real answer: the device reports position and no expectations are stated. """ declaration: DeviceLocationDeclaration } # Represents a typed command declared on a device profile. parameterSchema # is the ordered []ParameterSpec argument contract as a JSON string, which drives the # console command form and constrains an issued command's payload at enqueue (the # validateCommandEnqueue gate). """ A command the devices of a profile accept, with the parameters its payload must satisfy. Once the profile is published, a command sent to such a device must use one of the published keys, and its payload is checked against the parameters. """ type CommandDefinition implements Model & TokenReference & NamedEntity & MetadataEntity { """Server-assigned numeric identifier, as a string. Address a command definition by its token instead where you can.""" id: ID! """When the command definition was created, as an RFC 3339 timestamp.""" createdAt: String """When the command definition was last written, as an RFC 3339 timestamp.""" updatedAt: String """Always null: deleted command definitions are not returned.""" deletedAt: String """Identifier of the command definition, unique within the tenant.""" token: String! """Human-readable name, up to 128 characters. Null when none is set.""" name: String """Free-text description, up to 1024 characters. Null when none is set.""" description: String """The profile this command is declared on.""" deviceProfile: DeviceProfile! """ The key used to send this command, unique within the profile. Letters, digits, hyphens and underscores, starting with a letter or digit; at most 128 characters. """ commandKey: String! """ The command's parameters as a JSON array of parameter descriptors, or null when the command takes no structured arguments, in which case payloads are not checked. """ parameterSchema: String """Caller-defined metadata as a JSON document, or null when none is set.""" metadata: String } """Fields for a new command definition.""" input CommandDefinitionCreateRequest { """Identifier for the new command definition. A token already in use in the tenant is refused with CONFLICT.""" token: String! """Token of the profile to declare the command on. An unknown token fails the request.""" deviceProfileToken: String! """ The key used to send the command. Letters, digits, hyphens and underscores, starting with a letter or digit; at most 128 characters. A key the profile already declares is refused with CONFLICT. """ commandKey: String! """Human-readable name, up to 128 characters.""" name: String """Free-text description, up to 1024 characters.""" description: String """ The command's parameters: a JSON array of descriptors, each with a name and optional description, kind (SCALAR, the default, or OBJECT with nested parameters), dataType (DOUBLE, INT, BOOLEAN or STRING), unit, required, default, minValue, maxValue and enum. Unknown keys and inconsistent constraints are refused. Omit it for a command without structured arguments. """ parameterSchema: String """Caller-defined metadata. Must be valid JSON; an empty string means none.""" metadata: String } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. """ A partial update to a command definition. Omit a field to leave the stored value alone, send a value to set it, or send an explicit null to clear it; the fields that cannot be cleared say so. The definition is named by the mutation's token argument, so there is no token here. Any refusal leaves the definition unchanged. """ input CommandDefinitionUpdateRequest { """ Token of another profile to move the definition to. An explicit null or a blank value is refused, and an unknown token fails the whole update. The command key must be unique in the profile the definition ends up in. """ deviceProfileToken: String """ New command key, in the same format as on create. An explicit null or a blank value is refused, and a key already declared on the profile is refused with CONFLICT. """ commandKey: String """New name, or null to clear it.""" name: String """New description, or null to clear it.""" description: String """ New parameters (validated as on create), or null to make the command one without structured arguments. """ parameterSchema: String """New metadata (valid JSON, replacing the stored document), or null to clear it.""" metadata: String } """Criteria for searching command definitions. Results are newest first.""" input CommandDefinitionSearchCriteria { """Page to return, starting at 1. Values below 1 are read as 1.""" pageNumber: Int! """Results per page. Below 1 means the default of 100; above 1000 is capped at 1000.""" pageSize: Int! """Return only definitions on the profile with this token. An unknown token matches nothing.""" deviceProfile: String """Return only definitions with exactly this command key.""" commandKey: String } """One page of command definitions matching a search.""" type CommandDefinitionSearchResults { """The command definitions on this page.""" results: [CommandDefinition!]! """Where this page sits in the full result set.""" pagination: SearchResultsPagination! } # A DETECT rule declared on a device profile. The rule # body — definition — is an opaque JSON document (the event-processing rules.Rule schema); # device-management versions it with the profile but does not parse the detection taxonomy # (that stays single-homed in event-processing, which compiles and cost-gates it at # publish). DetectionRule is the one alarm-authoring path; its edge-triggered rules # raise/resolve alarms through the contributor-set integrator, the sole alarm-raise path since the 6d # cutover. (An alarm is still acknowledged and cleared by an operator through the mutations below.) type DetectionRule implements Model & TokenReference & NamedEntity & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String deviceProfile: DeviceProfile! # The authored rule as a JSON document (the rules.Rule schema). definition: String! # The optional visual-canvas document (CanvasDefinition JSON) this rule was # authored with, so the console re-opens it on the canvas — null for a form-authored rule. # Opaque authoring metadata; not parsed here and not frozen into the publish snapshot. authoringGraph: String enabled: Boolean! metadata: String # The optional group scope: the published dynamic entity-group version whose # members the rule fires for. Both null for an unscoped (profile-wide) rule; set together # for a scoped rule (the engine fires it only for events whose resolved entity is a member # of entityGroupToken@entityGroupVersion). entityGroupToken: String entityGroupVersion: Int } # Data required to create a detection rule. definition is the rules.Rule JSON document as a # string; device-management checks it is well-formed JSON on write, and event-processing # performs the authoritative type/cost validation when the profile is published. input DetectionRuleCreateRequest { token: String! deviceProfileToken: String! name: String description: String definition: String! # The optional canvas CanvasDefinition JSON sidecar. Omit for a # form-authored rule; when present it must be a JSON object. Stored verbatim, not parsed. authoringGraph: String enabled: Boolean! metadata: String # The optional group scope: the published dynamic entity-group version whose # members the rule fires for. Omit both for an unscoped rule; set both together for a scoped # rule. Validated at save (must be a published dynamic group@version). entityGroupToken: String entityGroupVersion: Int } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. input DetectionRuleUpdateRequest { # Re-parents the rule. Null is refused; an unknown token refuses the whole update. deviceProfileToken: String name: String description: String # Required and unclearable: a rule with no definition detects nothing. definition: String # The canvas sidecar. Null DROPS it — what a form edit of a canvas-authored rule # needs, since the graph is then stale. Omitting it KEEPS it. authoringGraph: String # Required and unclearable. Null is refused rather than read as false, which would # park a live rule and report success. enabled: Boolean metadata: String # The optional group scope. The two are folded independently and then validated as a # PAIR against what the rule ends up with, so naming one without the other is refused # rather than silently discarded. Null on either un-scopes the rule. entityGroupToken: String entityGroupVersion: Int } # Criteria used when searching for detection rules. input DetectionRuleSearchCriteria { pageNumber: Int! pageSize: Int! deviceProfile: String } # Search results returned from detection rule query. type DetectionRuleSearchResults { results: [DetectionRule!]! pagination: SearchResultsPagination! } # An authored geofence: a region a device's reported position can be tested against by a # DETECT rule, which names it by token. Geofencing is evaluated in Go by the detection # engine, never in SQL — geometry is an opaque GeoJSON document here, with no spatial # column, no spatial index and no PostGIS. # # The reason is replay determinism, not performance. A containment test issued at replay # time would answer against the fences that exist NOW rather than the fences that existed # when the position was reported. Instead, the tenant's active fence-set version is # stamped onto each resolved LOCATION event at resolve time (see currentFenceSetVersion), # so the fence set an event is evaluated against is frozen into the event itself. A fence # edit that moves a boundary mints a new version; it is not a rule migration, and rules # need not be republished. Renaming a fence mints nothing — the frozen shapes are the same. type GeoFence implements Model & TokenReference & NamedEntity & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String # The geometry as a self-describing JSON document: a kind discriminator plus a GeoJSON # geometry object, e.g. # {"kind":"POLYGON_2D","geometry":{"type":"Polygon","coordinates":[[[lon,lat],...]]}}. # Coordinates follow the platform-wide contract: WGS84 / EPSG:4326 decimal degrees. geometry: String! # The geometry kind, derived from the document above rather than stored separately. # POLYGON_2D today; POLYGON_2_5D (polygon + altitude band) and VOXEL_3D are reserved # names that are rejected on write until the engine can evaluate them. The kind belongs # to the FENCE, not to the rule language — a rule asks whether a device is inside a # fence, and that question does not change when the fence gains a third dimension. kind: String! metadata: String } # Data required to create a geofence. geometry is the JSON document described # above; it is validated on write (kind, GeoJSON shape, coordinate ranges, and a vertex # bound). Two limits are enforced at authoring time because nothing downstream can see # them: a fence may carry at most 512 positions, and a tenant at most 100 fences. The # publish-time rule cost gate estimates cost from the expression alone, so it cannot price # a containment call whose real cost is the fence's vertex count times the fences in scope. input GeoFenceCreateRequest { token: String! name: String description: String geometry: String! metadata: String } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. # # A fence's token is IMMUTABLE — rules name fences by token, and a rename would leave # them naming nothing. Under this input a rename is unrepresentable rather than refused. # A caller who wants a different token creates the new fence FIRST and deletes the old # one after; doing it the other way round can forfeit grandfathered position headroom and # leave the fence unrecreatable. input GeoFenceUpdateRequest { name: String description: String # Required and unclearable: a fence with no shape is not a fence. Omitting it leaves # the shape alone, which is what makes a rename or a metadata edit exempt from the # tenant's position ceiling — that ceiling refuses GROWTH, not size. geometry: String metadata: String } # Criteria used when searching for geofences. input GeoFenceSearchCriteria { pageNumber: Int! pageSize: Int! } # Search results returned from geofence query. type GeoFenceSearchResults { results: [GeoFence!]! pagination: SearchResultsPagination! } # The FROZEN fence set of one fence-set version: the fences that were live at the instant # an event stamped with that version was resolved. It is not an entity — it has no id and # cannot be edited or deleted — it is the immutable content of a version. # # This is what makes replaying history CORRECT rather than vacuous. Evaluating last week's # events against today's fences would answer a different question; each of those events # names the version whose snapshot holds the geometry it was actually resolved against. type GeoFenceSetSnapshot { # The version this set is the frozen state of. 0 means the tenant had never created a # fence when the event was resolved — an empty fence set as knowledge, not as absence. version: Int! # The fences as of this version, ordered by token, one bounded page at a time. # # It is PAGINATED because a fence set at the documented authoring ceiling does not fit # in one cross-service response: the service-token client that reads this door caps a # response at 1 MiB, and a hundred fences of five hundred vertices is more than that. # Unpaginated, the read failed for exactly the tenants who had used the product as # documented, and the failure surfaced only as containment eval errors. Read pages # until pagination.pageEnd reaches pagination.totalRecords. # # Paging a SNAPSHOT is safe in a way paging a live list is not: a version's fence set # is frozen at mint and nothing ever rewrites it, so two pages of one version cannot # come from two different fence sets however long the read takes. fences(pagination: PaginationInput!): GeoFenceSnapshotEntrySearchResults! } # One page of a frozen fence set's fences. type GeoFenceSnapshotEntrySearchResults { results: [GeoFenceSnapshotEntry!]! pagination: SearchResultsPagination! } # One fence inside a frozen snapshot. Name and description are deliberately absent: the # snapshot freezes only what changes what a rule computes, so a typo fix in a fence's name # does not make history read differently. type GeoFenceSnapshotEntry { # The stable per-tenant token a rule names this fence by. token: String! # The geometry envelope as it stood at this version — NOT the fence's current geometry. geometry: String! } # One enabled detection rule of a published profile version, as the detection engine # receives it. type PublishedProfileRule { # The rule's authoring token, unique within the profile. token: String! # The rule definition document. definition: String! # The published entity group the rule is scoped to; empty for a profile-wide rule. entityGroupToken: String! # The version of that group; 0 for a profile-wide rule. entityGroupVersion: Int! } # A published profile's active version, as the detection engine should hold it. type ActiveProfileRules { profileToken: String! # "{profileToken}@{version}" — the token resolved events carry and rules are keyed on. versionToken: String! # When this version became active (published, or rolled back to). activeSince: String! rules: [PublishedProfileRule!]! } type ActiveProfileRulesPage { entries: [ActiveProfileRules!]! # The cursor to pass as afterId for the next page; null when the walk is complete. # Treat it as opaque. nextCursor: String } # One device as the detection engine's roster holds it. type DeviceRosterEntry { deviceToken: String! # The token of the profile the device's type adopts; empty when it adopts none. profileToken: String! # When the device's membership of that profile began: its creation, or the last # re-type of the device or re-point of its type. expectedSince: String! } type DeviceRosterPage { entries: [DeviceRosterEntry!]! # The cursor to pass as afterId for the next page; null when the walk is complete. nextCursor: String } # One numeric device attribute a dynamic detection threshold can read. type DeviceThresholdAttribute { deviceToken: String! # SHARED or SERVER. scope: String! key: String! value: Float! # When the attribute was last written. updatedAt: String! } type DeviceThresholdAttributePage { entries: [DeviceThresholdAttribute!]! # The cursor to pass as afterId for the next page; null when the walk is complete. nextCursor: String } # The MANIFEST of one fence-set version: which fences it holds, and the content address of # each one's geometry. It carries no geometry, which is what makes its size a function of the # fence COUNT alone — so unlike geoFenceSetSnapshot it needs no pagination, at any fence count. # # This is how a holder of a version number learns what to resolve: an engine that missed the # announcement, one starting cold, or a preview replaying last week. The geometry itself comes # from geoFenceGeometry, keyed by the addresses named here. type GeoFenceSetManifest { # The version this manifest describes. 0 means the tenant had never created a fence when # the event was resolved — an empty fence set as knowledge, not as absence. version: Int! # When the change that minted this version committed. Carried for diagnosis — how old is # the set an engine is holding? — and never read by containment. mintedAt: String # The fences of this version, ordered by token. fences: [GeoFenceManifestEntry!]! } # One fence inside a manifest: the token a rule names it by, and the content address under # which its geometry is stored. type GeoFenceManifestEntry { token: String! hash: String! } # One archived geometry document and the content address it is filed under. The address is # the SHA-256 of exactly these bytes, so a caller can — and should — re-derive it to verify # what it received rather than trusting the association. type GeoFenceGeometry { hash: String! geometry: String! } # One alarm a bulk acknowledge did not act on, and why. type AlarmAckRefusal { # The alarm this refusal is about, exactly as the caller spelled it. token: String! # The machine-readable classification. Branch on this, never on `reason`. # # ALARM_NOT_FOUND is the only value, and that is a fact about acknowledgment rather than an # omission: acknowledgment is orthogonal to ACTIVE/CLEARED (either may be acknowledged) and # is idempotent, so no alarm STATE refuses it. A token naming nothing is the whole failure # space. Treat the field as an open vocabulary anyway. code: String! # Why it was refused, in client-safe terms. reason: String! } # What one bulk acknowledge did. # # Every token the caller supplied appears in EXACTLY ONE of the two lists, so a selection can be # reconciled completely rather than inferred from a count. type BulkAcknowledgeAlarmsResult { # The alarms that are now acknowledged — including any that ALREADY were, matching # acknowledgeAlarm, which likewise returns an already-acknowledged alarm rather than # treating the repeat as a failure. acknowledged: [Alarm!]! # The tokens that named no alarm. Empty for a clean request. refusals: [AlarmAckRefusal!]! } # A raised alarm — the operational alarm object. Raised when a DETECT rule's # condition fires (via the contributor-set integrator), keyed by (originator, # alarmKey): there is at most one live alarm per key, so re-crossings dedup into the # same row and severity escalates in place. The four-state model is state (ACTIVE | # CLEARED) crossed with acknowledged. The originator is addressed uniformly by # (originatorType, originatorId); today it is always a device. type Alarm implements Model & TokenReference & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! originatorType: String! originatorId: ID! # The originator's token, resolved on demand (device originators only today); # null when the originator is not a device or no longer exists. Mirrors # AlarmEvent.originatorToken so a live list and its reconciling query address the # originator the same way. originatorToken: String alarmKey: String! metricKey: String! state: String! acknowledged: Boolean! severity: String! raisedTime: String clearedTime: String acknowledgedTime: String acknowledgedBy: String lastValue: Float metadata: String } # Criteria used when searching for alarms. All facets are optional and AND together; # originator restricts to a single entity by its (originatorType, originator token). input AlarmSearchCriteria { pageNumber: Int! pageSize: Int! originatorType: String originator: String state: String severity: String acknowledged: Boolean alarmKey: String } # Search results returned from an alarm query. type AlarmSearchResults { results: [Alarm!]! pagination: SearchResultsPagination! } # Represents a single current-state attribute for an entity. type EntityAttribute implements Model { id: ID! createdAt: String updatedAt: String deletedAt: String entityType: String! entity: Entity! scope: String! attrKey: String! valueType: String! value: String lastUpdated: String } # Data required to set (upsert) an entity attribute. input EntityAttributeSetRequest { entityType: String! entity: String! scope: String! attrKey: String! valueType: String! value: String } # Criteria used when searching for entity attributes. input EntityAttributeSearchCriteria { pageNumber: Int! pageSize: Int! entityType: String entity: String scope: String attrKeys: [String!] } # A row of the audit journal. Captured by construction on every entity # mutation in this service, and never mutated via the API. It is written to by # exactly one other path: deleting a tenant empties the fields that name a person, # keeping the row as the evidence of the deletion. Times are RFC3339 strings. type AuditEvent { id: ID! # RFC3339 time the audited statement committed. occurredTime: String! # "mutation" (an entity create/update/delete) or "auth" (login/refresh). category: String! # JWT subject that performed it, "system" for a system-context operation, or # "" when neither is present. actor: String! # "create" | "update" | "delete" for a mutation. operation: String! # Schema-qualified mutated table (empty for an auth event). tableName: String # Primary key of the affected row when the statement targets a single row; # empty for a bulk/condition mutation (rowsAffected then conveys the scope). entityPk: String # Human-facing label of the affected row (e.g. its token), captured when the # model provides one; null otherwise. It is emptied for a tenant that has been # deleted: the journal is retained as the evidence of the deletion, and it does # not keep the identifiers the deletion was of. entityLabel: String # Number of rows the audited statement affected. rowsAffected: Int! } # Criteria for an audit-journal query. All filters are optional except # pagination; a bounded (startTime, endTime) range is recommended. input AuditEventSearchCriteria { pageNumber: Int! pageSize: Int! startTime: String endTime: String category: String operation: String actor: String tableName: String entityPk: String } # A page of audit-journal rows, newest first. type AuditEventSearchResults { results: [AuditEvent!]! pagination: SearchResultsPagination! } # Search results returned from entity attribute query. type EntityAttributeSearchResults { results: [EntityAttribute!]! pagination: SearchResultsPagination! } """Fields for a new device type.""" input DeviceTypeCreateRequest { """Identifier for the new device type. A token already in use in the tenant is refused with CONFLICT.""" token: String! """Human-readable name, up to 128 characters.""" name: String """Free-text description, up to 1024 characters.""" description: String """URL of an image representing the device type, up to 512 characters.""" imageUrl: String """Name of an icon representing the device type, up to 128 characters.""" icon: String """Background colour, up to 32 characters.""" backgroundColor: String """Foreground (text) colour, up to 32 characters.""" foregroundColor: String """Border colour, up to 32 characters.""" borderColor: String """ Token of the device profile the type adopts. Omit it, or send an empty value, for a type with no profile. An unknown token fails the request. """ profileToken: String """Manufacturer of the devices of this type, up to 128 characters.""" manufacturer: String """Model of the devices of this type, up to 128 characters.""" model: String """Caller-defined metadata. Must be valid JSON; an empty string means none.""" metadata: String } # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. """ A partial update to a device type. Omit a field to leave the stored value alone, send a value to set it, or send an explicit null to clear it; sending only name renames the type and changes nothing else. The type is named by the mutation's token argument, so there is no token here and a type's token cannot be changed. """ input DeviceTypeUpdateRequest { """New name, or null to clear it.""" name: String """New description, or null to clear it.""" description: String """New image URL, or null to clear it.""" imageUrl: String """New icon name, or null to clear it.""" icon: String """New background colour, or null to clear it.""" backgroundColor: String """New foreground colour, or null to clear it.""" foregroundColor: String """New border colour, or null to clear it.""" borderColor: String """ Token of the profile the type should adopt; null or an empty value detaches the current profile. An unknown token fails the whole update. Changing the profile changes what every device of the type measures and accepts. """ profileToken: String """New manufacturer, or null to clear it.""" manufacturer: String """New model, or null to clear it.""" model: String """New metadata (valid JSON, replacing the stored document), or null to clear it.""" metadata: String } """Criteria for searching device types. Results are newest first.""" input DeviceTypeSearchCriteria { """Page to return, starting at 1. Values below 1 are read as 1.""" pageNumber: Int! """Results per page. Below 1 means the default of 100; above 1000 is capped at 1000.""" pageSize: Int! } """One page of device types matching a search.""" type DeviceTypeSearchResults { """The device types on this page.""" results: [DeviceType!]! """Where this page sits in the full result set.""" pagination: SearchResultsPagination! } """Fields for a new device profile. Its definitions are added afterwards, and take effect when the profile is published.""" input DeviceProfileCreateRequest { """Identifier for the new profile. A token already in use in the tenant is refused with CONFLICT.""" token: String! """Human-readable name, up to 128 characters.""" name: String """Free-text description, up to 1024 characters.""" description: String """Functional class of the devices, such as thermostat or meter, up to 64 characters.""" category: String """Caller-defined metadata. Must be valid JSON; an empty string means none.""" metadata: String """ Whether the devices report their own position, and what to expect of it. Omit it when they do not. """ location: DeviceLocationDeclarationInput } # It carries no token. The mutation's own `token` argument names the profile, and # a profile is renamed through renameDeviceProfile — where the new token can mean # only one thing. """ A partial update to a device profile's own fields. Omit a field to leave the stored value alone, send a value to set it, or send an explicit null to clear it. The profile is named by the mutation's token argument; use renameDeviceProfile to change its token. Changes here are part of the draft. """ input DeviceProfileUpdateRequest { """New name, or null to clear it.""" name: String """New description, or null to clear it.""" description: String """New category, or null to clear it.""" category: String """New metadata (valid JSON, replacing the stored document), or null to clear it.""" metadata: String """ New position declaration, replacing the stored one; null states that the devices do not report their own position. An empty object states that they do, with no expectations. """ location: DeviceLocationDeclarationInput } """Criteria for searching device profiles. Results are newest first.""" input DeviceProfileSearchCriteria { """Page to return, starting at 1. Values below 1 are read as 1.""" pageNumber: Int! """Results per page. Below 1 means the default of 100; above 1000 is capped at 1000.""" pageSize: Int! } """One page of device profiles matching a search.""" type DeviceProfileSearchResults { """The device profiles on this page.""" results: [DeviceProfile!]! """Where this page sits in the full result set.""" pagination: SearchResultsPagination! } # Represents a device instance """ A device: one connected thing the platform receives events from and sends commands to. What it measures and accepts comes from its device type's profile. """ type Device implements Model & TokenReference & NamedEntity & MetadataEntity & Entity { """Server-assigned numeric identifier, as a string. Address a device by its token instead where you can.""" id: ID! """When the device was created, as an RFC 3339 timestamp.""" createdAt: String """When the device was last written, as an RFC 3339 timestamp.""" updatedAt: String """Always null: deleted devices are not returned.""" deletedAt: String """ Identifier of the device, unique within the tenant. It cannot be changed after the device is created. """ token: String! # opaque, not a credential, unique per tenant when set. """ Optional business identifier you assign, such as a serial number or VIN, up to 256 characters. Unique within the tenant when set. It is only a lookup key: devices are not addressed or authenticated by it. """ externalId: String """Human-readable name, up to 128 characters. Null when none is set.""" name: String """Free-text description, up to 1024 characters. Null when none is set.""" description: String """The device's type.""" deviceType: DeviceType! """Caller-defined metadata as a JSON document, or null when none is set.""" metadata: String } """Fields for a new device.""" input DeviceCreateRequest { """Identifier for the new device. A token already in use in the tenant is refused with CONFLICT.""" token: String! """ Optional business identifier, up to 256 characters. A value another device in the tenant already has is refused with CONFLICT. """ externalId: String """Human-readable name, up to 128 characters.""" name: String """Free-text description, up to 1024 characters.""" description: String """Token of the device's type. An unknown token fails the request.""" deviceTypeToken: String! """Caller-defined metadata. Must be valid JSON; an empty string means none.""" metadata: String } # There is no `token` member — the mutation's own `token` argument names the row. It used # to be the payload token that named the row, with the argument ignored, so a request # whose two tokens disagreed silently updated the device in the payload and returned it as # though it were the one asked for. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. """ A partial update to a device. Omit a field to leave the stored value alone, send a value to set it, or send an explicit null to clear it; the fields that cannot be cleared say so. The device is named by the mutation's token argument, so there is no token here. An empty object changes nothing. """ input DeviceUpdateRequest { """New business identifier, or null to clear it. A value another device already has is refused with CONFLICT.""" externalId: String """New name, or null to clear it.""" name: String """New description, or null to clear it.""" description: String """ Token of a different device type to move the device to. An explicit null or an empty value is refused, because every device must have a type; an unknown token fails the whole update. """ deviceTypeToken: String """New metadata (valid JSON, replacing the stored document), or null to clear it.""" metadata: String } """ Creates many devices of one type from templates. Device n of the batch (n running from startIndex to startIndex + count - 1) is rendered by replacing {n} with n, or {n:0Wd} with n zero-padded to width W. Each template may be at most 256 characters, with a pad width of at most 128. The whole batch is created or none of it is. """ input DeviceBulkCreateRequest { """Token of the device type for every device. An unknown token fails the request.""" deviceTypeToken: String! """Number of devices to create, from 1 to 1000.""" count: Int! """Index of the first device. Defaults to 1; values below 1 are refused.""" startIndex: Int """ Template for the device tokens. It must contain {n} or {n:0Wd} so each token is distinct, must not contain {random}, and every rendered token must be a valid token not already in use. """ tokenTemplate: String! """Template for the device names. Optional; {random} is not allowed. A rendered name may be at most 128 characters.""" nameTemplate: String """ Template for the external ids. Optional. {random} renders a fresh random value per device. When creating more than one device the template must contain {n}, {n:0Wd} or {random}, since external ids are unique within the tenant. A rendered value may be at most 256 characters. """ externalIdTemplate: String """Metadata given to every device. Must be valid JSON; an empty string means none.""" metadata: String } """Criteria for searching devices. Results are newest first.""" input DeviceSearchCriteria { """Page to return, starting at 1. Values below 1 are read as 1.""" pageNumber: Int! """Results per page. Below 1 means the default of 100; above 1000 is capped at 1000.""" pageSize: Int! """Return only devices of the device type with this token. An unknown token matches nothing.""" deviceType: String } """One page of devices matching a search.""" type DeviceSearchResults { """The devices on this page.""" results: [Device!]! """Where this page sits in the full result set.""" pagination: SearchResultsPagination! } # Represents a group of related devices. type EntityGroup implements Model & TokenReference & NamedEntity & BrandedEntity & MetadataEntity & Entity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String # The entity family this group collects: device | asset | area | customer. memberType: String! # How members are determined: static (explicit "member" edges) or dynamic # (a CEL selector over member-entity facet attributes). membershipMode: String! # The CEL membership predicate, present only for dynamic groups. selector: String # The resolved members of this group — a static group's "member" edges or a # dynamic group's eval-on-read selector matches, transparently (a consumer does # not branch on mode). Always bounded + paginated. members(pagination: PaginationInput!): EntityGroupMemberSearchResults! # The published version whose frozen selector a rule-scoped resolve reads. # Null until a dynamic group is first published; always null for a # static group (never versioned). The live `selector` above is the mutable draft. activeVersion: Int # The group's published versions, newest first. Empty for a group # that has never been published (and always empty for a static group). versions: [EntityGroupVersion!]! } # An immutable published snapshot of a DYNAMIC entity group's membership # selector. A rule scopes to a specific {group}@{version}, so its target set # cannot drift under a later selector edit. Mirrors DeviceProfileVersion. type EntityGroupVersion { # Monotonic per-group version number. version: Int! # The frozen lowerable-CEL membership selector captured at publish. selector: String! # The entity family the group collects, frozen with the version. memberType: String! # Optional user-supplied label (may embed a semver string; not parsed). label: String description: String # When the version was published (the row's creation time). publishedAt: String! # The identity that published it (username, falling back to email). publishedBy: String } # A resolved member of an entity group — the lightweight identity (row id + token) # of a family entity, enough for a consumer to hydrate the full record. The family # is the group's memberType, so it is not repeated per member. type EntityGroupMember { id: ID! token: String! } # A paginated page of an entity group's resolved members. type EntityGroupMemberSearchResults { results: [EntityGroupMember!]! pagination: SearchResultsPagination! } # The result of evaluating a CANDIDATE dynamic-group selector without saving it. # A selector that fails the publish gate (non-lowerable, over-budget, # a parse/type error) returns valid=false with the console-surfaceable error in # `error` and a null `members`, so the authoring UI can show the problem inline as # the user types rather than treating it as a hard failure. When valid, `members` # carries the eval-on-read matches (the live "matches N" preview). type SelectorPreview { valid: Boolean! error: String members: EntityGroupMemberSearchResults } # Data required to create an entity group. membershipMode defaults to # "static"; selector is only permitted (and required) for "dynamic". # Editing one afterwards is EntityGroupUpdateRequest. input EntityGroupCreateRequest { token: String! memberType: String! membershipMode: String selector: String name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. # # memberType and membershipMode are absent for the same reason as token: all three are # IDENTITY. Changing the member family would leave the group collecting a family its # members do not belong to, and converting a static group to dynamic (or back) would # orphan its members. A field that can only ever be a no-op or an error does not belong # in an update input. input EntityGroupUpdateRequest { # The CEL membership predicate, required (and only permitted) for a dynamic group. A # provided selector is re-compiled and cost-gated before it replaces the stored one. # Null is refused: a dynamic group with no selector matches nothing and cannot be # repaired into a valid group. selector: String name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # Criteria used when searching for entity groups. memberType and membershipMode # are optional filters. input EntityGroupSearchCriteria { pageNumber: Int! pageSize: Int! memberType: String membershipMode: String } # Search results returned from entity groups query. type EntityGroupSearchResults { results: [EntityGroup!]! pagination: SearchResultsPagination! } # A per-tenant declaration that an EntityAttribute key, for a member family, is a # classification facet. It declares which keys are facets for # the console browse/filter axes + typeahead; it does NOT store facet values (those # stay as EntityAttribute rows on the member entities). Addressed by its natural # key (memberType, key). Named FacetKey to avoid colliding with the # discovery-facet concept (DeviceFacet / facetValues). type FacetKey implements Model { id: ID! createdAt: String updatedAt: String deletedAt: String # The entity family this facet classifies: device | asset | area | customer. memberType: String! # The EntityAttribute key this facet declares (e.g. "climate", "country"). key: String! # The value type the facet's attribute values carry: STRING | LONG | DOUBLE | # BOOLEAN | JSON. valueType: String! # Where the facet's values come from: "attribute" (v1) or "system" (a # predefined, non-deletable, modeled-column facet — declared seam, deferred). source: String! # An optional value vocabulary that powers the console typeahead; null when the # facet's values are free-form. values: [String!] # An optional human display label for the axis; null falls back to key. label: String } # Data required to declare (upsert) a facet key. The natural key # (memberType, key) selects the row; the remaining fields are (re)written. v1 only # writes attribute-source facets, so there is no source field here. input FacetKeySetRequest { memberType: String! key: String! valueType: String! values: [String!] label: String } # Criteria used when searching for facet keys. memberType is an optional filter # (the console axis picker asks per family). input FacetKeySearchCriteria { pageNumber: Int! pageSize: Int! memberType: String } # Search results returned from the facet keys query. type FacetKeySearchResults { results: [FacetKey!]! pagination: SearchResultsPagination! } # Represents rotatable authentication material for a device. type DeviceCredential implements Model & TokenReference & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! device: Device! credentialType: String! credentialId: String! # Write-only: accepted on create/update but never returned on read (always # null), so a stored secret (e.g. an MQTT_BASIC password) cannot be read back # out through the API. credentialValue: String enabled: Boolean! expiresAt: String metadata: String } # Data required to create a device credential. input DeviceCredentialCreateRequest { token: String! deviceToken: String! credentialType: String! credentialId: String! """ The password for an MQTT_BASIC credential: at least 16 bytes, counted as sent, and at most 4096. A shorter one is refused with extensions.code INVALID_VALUE. Write-only; it is stored only as a keyed digest. A password set before the 16-byte minimum keeps working until it is next changed. """ credentialValue: String enabled: Boolean! expiresAt: String metadata: String } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. input DeviceCredentialUpdateRequest { # Re-points the credential at another device. Null is refused; an unknown token # refuses the whole update. deviceToken: String # Required and unclearable. credentialType is validated against the known vocabulary # when sent; credentialId is what a device presents at connect time. credentialType: String credentialId: String # The secret material, nullable — a credential type that carries no secret is a real # state. OMITTING IT LEAVES THE SECRET IN PLACE, which is what makes editing anything # else on a credential safe; the input this replaces blanked it on every such edit. """ The new password for an MQTT_BASIC credential: at least 16 bytes, counted as sent, and at most 4096. A shorter one is refused with extensions.code INVALID_VALUE. Required when this request changes the credential's type to MQTT_BASIC. Ignored for a type that has no password. Write-only; it is stored only as a keyed digest. A password set before the 16-byte minimum keeps working until it is next changed. """ credentialValue: String # Required and unclearable: null is refused rather than read as false, which would # silently disable the credential and take the device offline at its next reconnect. enabled: Boolean # An RFC3339 timestamp. Null — or an empty string, which is what a cleared form field # sends — means "never expires". expiresAt: String metadata: String } # Criteria used when searching for device credentials. input DeviceCredentialSearchCriteria { pageNumber: Int! pageSize: Int! device: String credentialType: String credentialId: String enabled: Boolean } # Search results returned from device credential query. type DeviceCredentialSearchResults { results: [DeviceCredential!]! pagination: SearchResultsPagination! } # One append-only record of a physical unit swap: the failed hardware was retired # and a new unit bound to the SAME logical device identity, so the device's events, # alarms, relationships, group membership and command vocabulary carry forward # unbroken. Written only by replaceDevice; there is no update or delete for it, and # it carries no token, so it cannot be re-pointed. type DeviceReplacement { id: ID! createdAt: String # The logical identity that survived the swap - unchanged by the operation. device: Device! # When the replacement was applied, from the server's clock. Never caller-supplied. occurredTime: String! # The authenticated subject that performed the replacement, from the caller's # token rather than the request body. actor: String! # The operator's free-text note ("water ingress", "RMA 41822"). reason: String # The incoming physical unit's own identifier - a manufacturer serial or IMEI. # Recorded here and deliberately NOT written onto the device's externalId, which # is the stable business key of the logical thing and must survive a swap. unitIdentifier: String # Entity tokens of the credentials this replacement disabled. Tokens, never # credential ids: for an ACCESS_TOKEN the credential id is the device's bearer. retiredCredentialTokens: [String!]! # Entity token of the credential minted for the incoming unit. newCredentialToken: String! newCredentialType: String! } # Criteria used when searching the device-replacement journal. input DeviceReplacementSearchCriteria { pageNumber: Int! pageSize: Int! # Narrow to one device by its token. device: String } # Search results returned from a device-replacement query, newest first. type DeviceReplacementSearchResults { results: [DeviceReplacement!]! pagination: SearchResultsPagination! } """ One immutable revision of a device's configuration document: the values of the shared attribute keys its profile's active published version declares as device-visible. Revisions are append-only; a new one is minted only when the document changes. """ type DeviceConfigurationRevision { """ The revision number: 1 for the device's first document and one higher for each later one. A number is never reused for the same device. """ revision: Int! """ "sha256:" followed by the lowercase hex SHA-256 of the document bytes. A device converges on a revision by reporting this revision number and this digest. A device hashes the bytes it receives and never re-serializes them to check the digest. """ digest: String! """ The document as canonical JSON text: an object of declared key to typed value with keys sorted. These are exactly the bytes the digest covers. """ document: String! """ When the revision was minted. """ createdAt: String """ The authenticated subject whose attribute change minted the revision; empty when none was recorded. """ actor: String! } """ The configuration state a device last reported. Written only from the device's own reports; no mutation sets it. """ type DeviceConfigurationReport { """ The revision the device reported, or null when it has not reported one. """ revision: Int """ The digest the device reported for that revision. """ digest: String """ RECEIVED, APPLIED or REJECTED. """ status: String """ The device's reasons for a REJECTED report; empty otherwise. """ errors: [String!]! """ When the device made the report. """ reportedAt: String """ When the device last asked for its configuration. """ lastSyncAt: String } """ A device's configuration: the latest revision it should hold, what it last reported, and whether the two agree. """ type DeviceConfiguration { """ The latest minted revision, or null when none has been minted for this device. """ desired: DeviceConfigurationRevision """ The device's last report, or null when it has never reported. """ reported: DeviceConfigurationReport """ True when a revision exists and the device has not reported APPLIED for that same revision with that same digest. Equal values under a different revision or digest do not count as applied. """ pending: Boolean! """ True when the document the device's current profile declaration and shared attributes would build differs from the latest revision - for example after a profile publish declared new keys. A newer revision is minted when a declared key is next written or the device next synchronizes. """ stale: Boolean! """ The device's shared attribute keys that its declaration does not name. They are never part of the document. """ undeclared: [String!]! """ Declared keys whose stored value cannot be carried as the declared value type (written with another type, or unparseable). They are left out of the document. """ invalid: [String!]! } """ A page of a device's configuration revisions, newest first. """ type DeviceConfigurationRevisionSearchResults { """ The revisions on this page, highest revision number first. """ results: [DeviceConfigurationRevision!]! """ Where this page sits in the device's whole revision history. """ pagination: SearchResultsPagination! } # What a caller sends to swap the physical unit behind a device identity. # # It carries NO device identity fields, and that is the contract rather than an # omission: there is no token, externalId, device type or name here, so a # replacement cannot move the identity, the business key or the profile binding. # "Rotate credentials, not identity" is unrepresentable to violate, not merely # refused. input DeviceReplaceRequest { # The surviving logical identity. deviceToken: String! # Credential type minted for the incoming unit. Defaults to ACCESS_TOKEN. credentialType: String # The identifier the new unit presents at connect time. Optional only for # ACCESS_TOKEN, where the id is itself the bearer and a fresh random one is # minted; required for every other type, since the server cannot invent an # MQTT username. credentialId: String # Secret material for types that carry one separately (an MQTT password). # Write-only, as on any credential. """ The password for an MQTT_BASIC credential: at least 16 bytes, counted as sent, and at most 4096. A shorter one is refused with extensions.code INVALID_VALUE. Write-only; it is stored only as a keyed digest. A password set before the 16-byte minimum keeps working until it is next changed. """ credentialValue: String # Entity token for the new credential row. A UUID is minted when absent. credentialToken: String # Optional expiry for the new credential (RFC3339). expiresAt: String reason: String unitIdentifier: String } # What replaceDevice returns. type DeviceReplaceResult { # The surviving identity, unchanged. device: Device! # The append-only record this call wrote. replacement: DeviceReplacement! # The credential minted for the incoming unit. This is the one moment the # material the new unit must be programmed with is readable, which is why the # mutation is gated on device:write like every other credential door. newCredential: DeviceCredential! # Entity tokens of the credentials this call disabled. Tokens only: the # credential rows themselves stay behind the gated credential queries. retiredCredentialTokens: [String!]! } # A device provisioning profile: the shared key+secret a fleet presents # to self-register, the strategy governing whether unknown devices may be created, # and the device type stamped onto auto-created devices. The provision secret is # write-only and is never exposed on this type. type ProvisioningProfile implements Model & TokenReference & NamedEntity & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String provisionKey: String! strategy: String! deviceType: DeviceType! credentialType: String! enabled: Boolean! expiresAt: String metadata: String } # Data required to create a provisioning profile. input ProvisioningProfileCreateRequest { token: String! name: String description: String # Both are refused when empty or whitespace-only: a blank secret is no proof of # anything. A padded value is stored exactly as sent. provisionKey: String! provisionSecret: String! strategy: String! deviceTypeToken: String! credentialType: String enabled: Boolean! expiresAt: String metadata: String } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. input ProvisioningProfileUpdateRequest { name: String description: String # The credentials a fleet presents to self-register. Both are required and # unclearable: a blank either side authenticates nothing. OMITTING provisionSecret # LEAVES IT IN PLACE — the input this replaces blanked it on every edit that failed to # restate it, after which the whole fleet failed to register. provisionKey: String provisionSecret: String # Required and unclearable; validated against the strategy vocabulary when sent. strategy: String # Stamped onto auto-created devices. Null is refused; an unknown token refuses the # whole update. deviceTypeToken: String # credentialType is deliberately absent. Provisioning can mint exactly one credential # type today (ACCESS_TOKEN), so every stored value is that one and an update naming # the field could only restate it or be refused — a field with no useful request does # not belong in an update input. It returns additively when a second type is mintable. enabled: Boolean # An RFC3339 timestamp. Null or an empty string means "never expires". expiresAt: String metadata: String } # Criteria used when searching for provisioning profiles. input ProvisioningProfileSearchCriteria { pageNumber: Int! pageSize: Int! deviceType: String strategy: String enabled: Boolean } # Search results returned from provisioning profile query. type ProvisioningProfileSearchResults { results: [ProvisioningProfile!]! pagination: SearchResultsPagination! } # A device's possession claim: a one-time, optionally expiring secret an # eventual owner presents to assign the device to a customer. The claim secret is # write-only and is never exposed on this type. type DeviceClaim implements Model { id: ID! createdAt: String updatedAt: String deletedAt: String device: Device! status: String! expiresAt: String claimedTime: String claimedByCustomer: Customer } # Data required to open (or reopen) a device claim. input DeviceClaimInitiateRequest { deviceToken: String! claimSecret: String! expiresAt: String } # Data required to redeem a device claim: the device, the claim secret (proof of # possession), the customer to assign the device to, and the relationship type for # the resulting device→customer ownership edge. input DeviceClaimRequest { deviceToken: String! claimSecret: String! customerToken: String! relationshipType: String! } # Represents a type or class of assets type AssetType implements Model & TokenReference & NamedEntity & BrandedEntity & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String # The DRAFT property contract for assets of this type: a JSON array of typed # field descriptors, in declaration order, or null when the type declares none. # It is the same descriptor shape a command definition uses for its parameters. # Editing it changes nothing an asset is validated against until the type is # published. propertySchema: String # The published version assets of this type are validated against, or null when # the type has never been published. An asset of an unpublished type may carry # no properties at all. activeVersion: Int } # An immutable published snapshot of an asset type's property contract. Assets are # validated against the type's ACTIVE version, never against its draft, so # publishing is what makes a contract binding. History is append-only; a rollback # re-points the active version without deleting anything. type AssetTypeVersion { version: Int! label: String description: String publishedAt: String! publishedBy: String # The frozen property contract this version publishes, in the same JSON form as # AssetType.propertySchema. Unlike a device profile version's snapshot this IS # exposed: an author needs to see what a version froze in order to decide whether # to roll back to it. propertySchema: String! } # Data required to create an asset type. input AssetTypeCreateRequest { token: String! name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String # The draft property contract (see AssetType.propertySchema). Rejected if it is # not a JSON array of descriptors, if it carries a key no descriptor declares # (so a typo'd "maximum" is refused rather than dropped), or if it declares a # contract nothing could satisfy — a duplicate name, an unknown data type, a # default outside its own enum. propertySchema: String } # A PARTIAL update to an asset type: omit a field to leave it alone, send null to # clear it, send a value to set it. There is no `token` member — the mutation's own # `token` argument names the row, so a token move is unrepresentable. It used to be # the payload token that named the row, with the argument ignored, so a request # whose two tokens disagreed silently updated the one in the payload. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. input AssetTypeUpdateRequest { name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String # Replaces the draft property contract wholesale; null withdraws it. Omit to # leave the stored draft alone. It is validated the same way as on create, and a # rejection writes nothing at all. propertySchema: String } # Criteria used when searching for asset types. input AssetTypeSearchCriteria { pageNumber: Int! pageSize: Int! } # Search results returned from asset types query. type AssetTypeSearchResults { results: [AssetType!]! pagination: SearchResultsPagination! } # Represents an asset instance type Asset implements Model & TokenReference & NamedEntity & MetadataEntity & Entity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String assetType: AssetType! metadata: String # The asset's property document: a JSON object whose keys are the properties its # type's published contract declares. Null when the asset carries none. properties: String } # Data required to create an asset. input AssetCreateRequest { token: String! name: String description: String assetTypeToken: String! metadata: String # The asset's property document, validated against the asset type's ACTIVE # PUBLISHED contract before anything is written. Refused when the type has no # published contract, when a key is not declared, when a value does not fit its # declared type/bounds/allowed values, or when a required property is missing — # and the refusal is total, so no asset is created. properties: String } # A PARTIAL update to an asset: omit a field to leave it alone, send null to clear # it, send a value to set it. There is no `token` member — see AssetTypeUpdateRequest. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. input AssetUpdateRequest { name: String description: String # Re-points the asset at a different asset type. Omit to keep the current one. # Unlike DeviceTypeUpdateRequest.profileToken, NULL IS REFUSED here: every asset # must reference a type, so "no type" is not a state it can be in. An unknown # token is refused too, and the refusal is total — nothing is written. assetTypeToken: String metadata: String # Replaces the asset's property document wholesale; null clears it. Omit to keep # what is stored — but note that omitting it does NOT skip the check: a retype # re-validates the properties the asset already carries against the contract of # the type it is moving to, so an asset can never be left describing itself in a # vocabulary its type does not declare. properties: String } # Criteria used when searching for assets. input AssetSearchCriteria { pageNumber: Int! pageSize: Int! assetTypeToken: String } # Search results returned from assets query. type AssetSearchResults { results: [Asset!]! pagination: SearchResultsPagination! } # Represents a type or class of customers type CustomerType implements Model & TokenReference & NamedEntity & BrandedEntity & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # Data required to create a customer type. input CustomerTypeCreateRequest { token: String! name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # A PARTIAL update to a customer type: omit a field to leave it alone, send null to # clear it, send a value to set it. There is no `token` member — the mutation's own # `token` argument names the row, so a token move is unrepresentable. It used to be # the payload token that named the row, with the argument ignored, so a request # whose two tokens disagreed silently updated the one in the payload. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. input CustomerTypeUpdateRequest { name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # Criteria used when searching for customer types. input CustomerTypeSearchCriteria { pageNumber: Int! pageSize: Int! } # Search results returned from customer type query. type CustomerTypeSearchResults { results: [CustomerType!]! pagination: SearchResultsPagination! } # Represents a customer instance type Customer implements Model & TokenReference & NamedEntity & MetadataEntity & Entity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String customerType: CustomerType! metadata: String } # Data required to create a customer. input CustomerCreateRequest { token: String! name: String description: String customerTypeToken: String! metadata: String } # A PARTIAL update to a customer: omit a field to leave it alone, send null to clear # it, send a value to set it. There is no `token` member — see CustomerTypeUpdateRequest. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. input CustomerUpdateRequest { name: String description: String # Re-points the customer at a different customer type. Omit to keep the current one. # Unlike DeviceTypeUpdateRequest.profileToken, NULL IS REFUSED here: every customer # must reference a type, so "no type" is not a state it can be in. An unknown # token is refused too, and the refusal is total — nothing is written. customerTypeToken: String metadata: String } # Criteria used when searching for customers. input CustomerSearchCriteria { pageNumber: Int! pageSize: Int! customerTypeToken: String } # Search results returned from customer query. type CustomerSearchResults { results: [Customer!]! pagination: SearchResultsPagination! } # Represents a type or class of areas type AreaType implements Model & TokenReference & NamedEntity & BrandedEntity & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # Data required to create an area type. input AreaTypeCreateRequest { token: String! name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # A PARTIAL update to an area type: omit a field to leave it alone, send null to # clear it, send a value to set it. There is no `token` member — the mutation's own # `token` argument names the row, so a token move is unrepresentable. It used to be # the payload token that named the row, with the argument ignored, so a request # whose two tokens disagreed silently updated the one in the payload. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. input AreaTypeUpdateRequest { name: String description: String imageUrl: String icon: String backgroundColor: String foregroundColor: String borderColor: String metadata: String } # Criteria used when searching for area types. input AreaTypeSearchCriteria { pageNumber: Int! pageSize: Int! } # Search results returned from area types query. type AreaTypeSearchResults { results: [AreaType!]! pagination: SearchResultsPagination! } # Represents an area instance type Area implements Model & TokenReference & NamedEntity & MetadataEntity & Entity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String areaType: AreaType! metadata: String } # Data required to create an area. input AreaCreateRequest { token: String! name: String description: String areaTypeToken: String! metadata: String } # A PARTIAL update to an area: omit a field to leave it alone, send null to clear # it, send a value to set it. There is no `token` member — see AreaTypeUpdateRequest. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the # field arrive already-populated for a caller who never sent it, which collapses # "omitted" back into "sent", and turns this input back into the full replace it # replaces. input AreaUpdateRequest { name: String description: String # Re-points the area at a different area type. Omit to keep the current one. # Unlike DeviceTypeUpdateRequest.profileToken, NULL IS REFUSED here: every area # must reference a type, so "no type" is not a state it can be in. An unknown # token is refused too, and the refusal is total — nothing is written. areaTypeToken: String metadata: String } # Criteria used when searching for areas. input AreaSearchCriteria { pageNumber: Int! pageSize: Int! areaTypeToken: String } # Search results returned from areas query. type AreaSearchResults { results: [Area!]! pagination: SearchResultsPagination! } # Uniform entity interface implemented by all entity object types. interface Entity { id: ID! token: String! } # A uniform (type, token) reference to an entity. input EntityRefInput { type: String! token: String! } # A uniform (type, token) reference to an entity, returned from a query. type EntityRef { type: String! token: String! } # Represents a type of relation between entities. type EntityRelationshipType implements Model & TokenReference & NamedEntity & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! name: String description: String metadata: String tracked: Boolean! } # Data required to create an entity relationship type. input EntityRelationshipTypeCreateRequest { token: String! name: String description: String metadata: String tracked: Boolean! } # A PARTIAL update. Every field is optional in three senses, not two: omit it to leave # the stored value untouched, send null to clear it, send a value to set it. # # There is deliberately no `token` field — the record is identified by the mutation's # `token` argument, and an update cannot move it. # # 🔴 Do NOT give any field in this input an SDL default value. A default makes the field # arrive already-populated for a caller who never sent it, which collapses "omitted" back # into "sent", and turns this input back into the full replace it replaces. input EntityRelationshipTypeUpdateRequest { name: String description: String metadata: String # Required and unclearable: null is refused rather than read as false, which would # stop denormalizing this type's relationships onto events and report success. tracked: Boolean } # Criteria used when searching for entity relationship types. input EntityRelationshipTypeSearchCriteria { pageNumber: Int! pageSize: Int! } # Search results returned from entity relationship type query. type EntityRelationshipTypeSearchResults { results: [EntityRelationshipType!]! pagination: SearchResultsPagination! } # Represents a relationship between two entities. type EntityRelationship implements Model & TokenReference & MetadataEntity { id: ID! createdAt: String updatedAt: String deletedAt: String token: String! sourceType: String! source: Entity! targetType: String! target: Entity! relationshipType: EntityRelationshipType! metadata: String } # Data required to create an entity relationship. input EntityRelationshipCreateRequest { token: String! sourceType: String! source: String! targetType: String! target: String! relationshipType: String! metadata: String } # Criteria used when searching for entity relationships. input EntityRelationshipSearchCriteria { pageNumber: Int! pageSize: Int! sourceType: String source: String targetType: String target: String relationshipType: String tracked: Boolean } # Search results returned from entity relationship query. type EntityRelationshipSearchResults { results: [EntityRelationship!]! pagination: SearchResultsPagination! } # Contains queries executed against model. type Query { """ Returns the device types with the given ids. Ids that match no device type are omitted, an id that is not a number fails the request, and more than 1000 ids are refused with LIMIT_EXCEEDED. Requires device:read. """ deviceTypesById( """Ids of the device types to return.""" ids: [ID!]! ): [DeviceType!]! """ Returns the device types with the given tokens. Tokens that match no device type are omitted, and more than 1000 tokens are refused with LIMIT_EXCEEDED. Requires device:read. """ deviceTypesByToken( """Tokens of the device types to return.""" tokens: [String!]! ): [DeviceType!]! """Lists the tenant's device types, newest first, one page at a time. Requires device:read.""" deviceTypes( """The page to return.""" criteria: DeviceTypeSearchCriteria! ): DeviceTypeSearchResults! """ Returns the device profiles with the given ids. Ids that match no profile are omitted, an id that is not a number fails the request, and more than 1000 ids are refused with LIMIT_EXCEEDED. Requires device:read. """ deviceProfilesById( """Ids of the profiles to return.""" ids: [ID!]! ): [DeviceProfile!]! """ Returns the device profiles with the given tokens. Tokens that match no profile are omitted, and more than 1000 tokens are refused with LIMIT_EXCEEDED. Requires device:read. """ deviceProfilesByToken( """Tokens of the profiles to return.""" tokens: [String!]! ): [DeviceProfile!]! """Lists the tenant's device profiles, newest first, one page at a time. Requires device:read.""" deviceProfiles( """The page to return.""" criteria: DeviceProfileSearchCriteria! ): DeviceProfileSearchResults! """ Lists the published versions of a device profile, newest first, at most 1000 per call; use limit and offset to page. Empty when the profile has never been published or offset is past the last version. Fails if no profile has the token. Requires device:read. """ deviceProfileVersions( """Token of the profile.""" token: String! """Maximum number of versions to return, at least 1. Omitted, or above 1000, it is 1000; below 1 is refused.""" limit: Int """Number of newest versions to skip before the first one returned. Omitted, it is 0; negative is refused.""" offset: Int ): [DeviceProfileVersion!]! """ Lists the distinct values currently in use for a free-text classification field across the tenant's device types or profiles, sorted, without empty values. An authoring form can offer them as suggestions. Requires device:read. """ facetValues( """The field whose values to list.""" facet: DeviceFacet! ): [String!]! # List the tenant's declared facet keys, optionally for one member # family. Powers the console browse/filter axis picker + typeahead. facetKeys(criteria: FacetKeySearchCriteria!): FacetKeySearchResults! """ Returns the metric definitions with the given ids. Ids that match nothing are omitted, an id that is not a number fails the request, and more than 1000 ids are refused with LIMIT_EXCEEDED. Requires device:read. """ metricDefinitionsById( """Ids of the metric definitions to return.""" ids: [ID!]! ): [MetricDefinition!]! """ Returns the metric definitions with the given tokens. Tokens that match nothing are omitted, and more than 1000 tokens are refused with LIMIT_EXCEEDED. Requires device:read. """ metricDefinitionsByToken( """Tokens of the metric definitions to return.""" tokens: [String!]! ): [MetricDefinition!]! """ Searches the tenant's metric definitions, newest first, one page at a time. These are the draft definitions, not a published version. Requires device:read. """ metricDefinitions( """Filters and the page to return.""" criteria: MetricDefinitionSearchCriteria! ): MetricDefinitionSearchResults! """ Returns the command definitions with the given ids. Ids that match nothing are omitted, an id that is not a number fails the request, and more than 1000 ids are refused with LIMIT_EXCEEDED. Requires device:read. """ commandDefinitionsById( """Ids of the command definitions to return.""" ids: [ID!]! ): [CommandDefinition!]! """ Returns the command definitions with the given tokens. Tokens that match nothing are omitted, and more than 1000 tokens are refused with LIMIT_EXCEEDED. Requires device:read. """ commandDefinitionsByToken( """Tokens of the command definitions to return.""" tokens: [String!]! ): [CommandDefinition!]! """ Searches the tenant's command definitions, newest first, one page at a time. These are the draft definitions, not a published version. Requires device:read. """ commandDefinitions( """Filters and the page to return.""" criteria: CommandDefinitionSearchCriteria! ): CommandDefinitionSearchResults! # The enqueue gate; a profile declaring no vocabulary allows # free-form payloads. """ Checks whether a command may be sent to a device, using the active published version of the device's profile. If that version declares commands, the key must be one of them and the payload must satisfy its parameters; if it declares none, or the device has no published profile, any command is allowed. Refusals are returned as a verdict, not as an error. Requires device:read. """ validateCommandEnqueue( """Token of the target device.""" deviceToken: String! """Key of the command to send.""" commandKey: String! """ The command's arguments as a JSON object. Omit it or send null to check a command sent with no arguments, which passes only if no parameter is required. """ payload: String ): CommandEnqueueVerdict! # One command key and one payload apply to every device, so the verdict depends only on # each device's TYPE: the devices resolve in one lookup and each distinct type's # published vocabulary is resolved once, rather than once per device. """ Checks one command against many devices at once, with the same rules as validateCommandEnqueue. Only refusals are returned: a device that may receive the command produces no entry, so an empty list means every listed device may receive it. Duplicate tokens are collapsed, and refusals come back in the order each device was first named. More than 2000 tokens fails the request. Requires device:read. """ validateCommandEnqueueBatch( """Tokens of the target devices, at most 2000.""" deviceTokens: [String!]! """Key of the command to send.""" commandKey: String! """The command's arguments as a JSON object, applied to every device. Omit it to check a command with no arguments.""" payload: String ): [BatchEnqueueRefusal!]! # It is a KEYSET walk rather than page-number paging, and that is a correctness # difference: with offset paging, a device leaving the group between two pages shifts # every later row down a slot, so a device that was a member throughout is never # visited. What a walk can still miss is a device that JOINS below the cursor after it # has passed. """ Returns one page of the devices an entity group contains, for use as the target of a command. Pass each page's nextCursor as afterId to get the next one. A device that stays in the group throughout a walk is visited exactly once. A dynamic group is resolved from a published version, never from its editable draft. A group that cannot be a target is answered with rejected set and a code, not with an error. Requires device:read. """ resolveDeviceGroupTargets( """Token of the entity group.""" groupToken: String! """ Published version of a dynamic group to resolve. Omit it for the active version. Must be omitted for a static group. """ version: Int """The nextCursor of the previous page. Omit it to start from the beginning. A malformed cursor fails the request.""" afterId: String """Maximum number of devices on the page, from 1 to 1000.""" limit: Int! ): DeviceGroupTargetPage! """ Lists the commands a device currently accepts, from the active published version of its profile. It reads the same data validateCommandEnqueue checks against. Returns null when no device has the token. Requires device:read. """ deviceCommandVocabulary( """Token of the device.""" deviceToken: String! ): DeviceCommandVocabulary """ Tells whether a device reports its own position, from the active published version of its profile rather than the draft. Returns null when no device has the token; a device that declares nothing answers with declared false. Requires device:read. """ deviceLocationDeclaration( """Token of the device.""" deviceToken: String! ): DeviceLocationCapability # Find detection rules by unique id. detectionRulesById(ids: [ID!]!): [DetectionRule!]! # Find detection rules by unique token. detectionRulesByToken(tokens: [String!]!): [DetectionRule!]! # List detection rules that meet criteria. detectionRules(criteria: DetectionRuleSearchCriteria!): DetectionRuleSearchResults! # Find geofences by unique id. geoFencesById(ids: [ID!]!): [GeoFence!]! # Find geofences by unique token. geoFencesByToken(tokens: [String!]!): [GeoFence!]! # List geofences that meet criteria. geoFences(criteria: GeoFenceSearchCriteria!): GeoFenceSearchResults! # The tenant's active fence-set version — the number stamped onto resolved location # events, naming the frozen snapshot of the fences live at that moment. A change to the # fence SET mints the next one — a fence created, a boundary edited, a fence deleted. # Renaming a fence, or editing its description or metadata, does not: none of them appear # in the frozen snapshot, so a new version would name shapes identical to the current one. # 0 means the tenant has never had a fence, which is distinct from a minted version whose # fence set happens to be empty. currentFenceSetVersion: Int! # The FROZEN fence set of one fence-set version — the fences that were live when an # event stamped with that version was resolved. This is how a consumer resolves a # version it no longer holds in memory: an authoring preview replaying last week, a # late redelivery, a cold start. An unknown non-zero version is an error, because a # stamp naming a version that is not on record means the history was truncated and # answering "empty" would read as "no fences" rather than "cannot know". geoFenceSetSnapshot(version: Int!): GeoFenceSetSnapshot! # The frozen fence set of the tenant's CURRENT version — the version and its fences # read together, so the two cannot disagree across a concurrent fence edit. # # That guarantee holds WITHIN one response. A caller reading a multi-page fence set # must take page 1 from here and every later page from geoFenceSetSnapshot at the # version this one reported: asking here twice is two questions about a moving target, # and a fence edit between them yields two pages of two different sets stitched # together under one version number. currentGeoFenceSet: GeoFenceSetSnapshot! # The manifest of one fence-set version: which fences it held and the content address # of each one's geometry. Unlike geoFenceSetSnapshot this reads no geometry at all, so # it is a fixed cost per fence and needs no pagination. # # An unknown POSITIVE version is an error, for the same reason geoFenceSetSnapshot gives: # a stamp naming a version that is not on record means the history was truncated, and # answering "empty" would read as "no fences" rather than "cannot know". A negative # version is refused outright rather than sharing version 0's known-empty answer. geoFenceSetManifest(version: Int!): GeoFenceSetManifest! # ---- Detection-engine reconcile reads ------------------------------------------------ # What the detection engine's copy of this tenant's rules, devices and thresholds is # checked against. event-processing reads them over a service token when it takes the # DETECT partition and every five minutes after, and corrects whatever differs — which # is how a rule, device or attribute change whose notification was lost still reaches # detection. Each is a keyset page: pass the previous page's nextCursor as afterId, and # stop when it is null. A limit outside 1..the door's maximum is refused. # # One page of the tenant's PUBLISHED profiles, each with its active version, the # instant that version became active and the version's enabled detection rules. A # profile whose frozen snapshot cannot be read fails the whole page rather than # answering with no rules. activeProfileRules(afterId: String, limit: Int!): ActiveProfileRulesPage! # One page of the device roster: every device, the profile its type adopts (empty when # none) and when that membership began. deviceRosterPage(afterId: String, limit: Int!): DeviceRosterPage! # One page of the numeric SHARED and SERVER device attributes a dynamic detection # threshold can read. A page may hold fewer entries than its limit and still have a # nextCursor; only a null cursor ends the walk. deviceThresholdAttributePage(afterId: String, limit: Int!): DeviceThresholdAttributePage! # The manifest of the tenant's CURRENT version — the version and its fences read from # one row, so the two cannot disagree across a concurrent fence edit. Reading the # version and the manifest as two queries would be two questions about a moving target. currentGeoFenceSetManifest: GeoFenceSetManifest! # The archived geometry documents stored under the given content addresses. # # An address this tenant does not hold is ABSENT from the result rather than an error: # the question is "which of these do you have?", and the set of answers is the answer. # Incompleteness is the caller's to interpret — a caller resolving a manifest must turn # a missing body into a fence that reports an ERROR, never one that is silently absent, # because an absent fence reads downstream as "this fence does not exist" and makes # containment answer "outside" for a device that is inside. # # Asking for more addresses than the per-request limit is an error rather than a partial # answer: a caller that asked for forty and silently received twenty-four could not tell # that from a tenant holding only twenty-four of them. geoFenceGeometry(hashes: [String!]!): [GeoFenceGeometry!]! # Find raised alarms by unique id. alarmsById(ids: [ID!]!): [Alarm!]! # Find raised alarms by unique token. alarmsByToken(tokens: [String!]!): [Alarm!]! # List raised alarms that meet criteria. alarms(criteria: AlarmSearchCriteria!): AlarmSearchResults! # List entity attributes that meet criteria. entityAttributes(criteria: EntityAttributeSearchCriteria!): EntityAttributeSearchResults! # Query the audit journal; tenant-scoped, newest first. auditEvents(criteria: AuditEventSearchCriteria!): AuditEventSearchResults! """ Returns the devices with the given ids. Ids that match no device are omitted, an id that is not a number fails the request, and more than 1000 ids are refused with LIMIT_EXCEEDED. Requires device:read. """ devicesById( """Ids of the devices to return.""" ids: [ID!]! ): [Device!]! """ Returns the devices with the given tokens. Tokens that match no device are omitted, and more than 1000 tokens are refused with LIMIT_EXCEEDED. Requires device:read. """ devicesByToken( """Tokens of the devices to return.""" tokens: [String!]! ): [Device!]! """ Returns the devices whose business identifier is one of the given values. Values that match no device are omitted. Requires device:read. """ devicesByExternalId( """Business identifiers to look up, matched exactly.""" externalIds: [String!]! ): [Device!]! """Searches the tenant's devices, newest first, one page at a time. Requires device:read.""" devices( """Filters and the page to return.""" criteria: DeviceSearchCriteria! ): DeviceSearchResults! # Find entity groups by unique id. entityGroupsById(ids: [ID!]!): [EntityGroup!]! # Find entity groups by unique token. entityGroupsByToken(tokens: [String!]!): [EntityGroup!]! # List entity groups that meet criteria (optionally by member family / mode). entityGroups(criteria: EntityGroupSearchCriteria!): EntityGroupSearchResults! # Evaluate a CANDIDATE dynamic-group selector for a member family WITHOUT saving # it — the console's live "matches N" authoring preview. Takes the # identical publish gate + lowering + bounded query as a saved dynamic group, so # the preview is exactly what the group would resolve. A non-lowerable / over-budget # / malformed selector returns valid=false with an inline error, never a hard fault. previewSelector(memberType: String!, selector: String!, pagination: PaginationInput!): SelectorPreview! # List a dynamic entity group's published versions, newest first. # At most 1000 per call (newest first); limit (at least 1) narrows that and offset (not negative) pages further. entityGroupVersions(token: String!, limit: Int, offset: Int): [EntityGroupVersion!]! # Find device credentials by unique id. deviceCredentialsById(ids: [ID!]!): [DeviceCredential!]! # Find device credentials by unique token. deviceCredentialsByToken(tokens: [String!]!): [DeviceCredential!]! # List device credentials that meet criteria. deviceCredentials(criteria: DeviceCredentialSearchCriteria!): DeviceCredentialSearchResults! # List device-replacement records, newest first, optionally narrowed to one # device. There is no by-id or by-token door: a replacement is never addressed # on its own, only read as part of a device's history. deviceReplacements(criteria: DeviceReplacementSearchCriteria!): DeviceReplacementSearchResults! """ A device's configuration: its latest revision, its last report and whether they agree. Null when no device has this token. Reading mints nothing. """ deviceConfiguration( """ The token of the device to read. """ deviceToken: String! ): DeviceConfiguration """ A page of a device's configuration revisions, newest first. Empty when no device has this token. """ deviceConfigurationRevisions( """ The token of the device whose revisions to list. """ deviceToken: String! """ Which page of revisions to return, bounded by the platform page-size ceiling. """ pagination: PaginationInput! ): DeviceConfigurationRevisionSearchResults! # The asset directly above this one in the hierarchy, or null when it is a # root. A root is a normal state, not an error. assetParent(token: String!): Asset # The path from an asset to its root, NEAREST ANCESTOR FIRST - the order a # breadcrumb is built from. assetAncestors(token: String!): [Asset!]! # A page of the assets directly below a parent. Omit parentToken to page the # ROOTS of the tree; a tree browser asks the same question at every level. assetChildren(parentToken: String, pagination: PaginationInput!): AssetSearchResults! # Find provisioning profiles by unique id. provisioningProfilesById(ids: [ID!]!): [ProvisioningProfile!]! # Find provisioning profiles by unique token. provisioningProfilesByToken(tokens: [String!]!): [ProvisioningProfile!]! # List provisioning profiles that meet criteria. provisioningProfiles(criteria: ProvisioningProfileSearchCriteria!): ProvisioningProfileSearchResults! # Get a device's current possession claim, or null if it has none. deviceClaim(deviceToken: String!): DeviceClaim # Find entity relationship types by unique id. entityRelationshipTypesById(ids: [String!]!): [EntityRelationshipType!]! # Find entity relationship types by unique token. entityRelationshipTypesByToken(tokens: [String!]!): [EntityRelationshipType!]! # List entity relationship types that meet criteria. entityRelationshipTypes(criteria: EntityRelationshipTypeSearchCriteria!): EntityRelationshipTypeSearchResults! # Find entity relationships by unique id. entityRelationshipsById(ids: [String!]!): [EntityRelationship!]! # Find entity relationships by unique token. entityRelationshipsByToken(tokens: [String!]!): [EntityRelationship!]! # List entity relationships that meet criteria. entityRelationships(criteria: EntityRelationshipSearchCriteria!): EntityRelationshipSearchResults! # Return the subset of the given (type, id) references that resolve to an # existing entity in the caller's tenant. The # event-management reconciliation sweep uses this to find orphaned anchors whose # referenced entity was deleted. existingEntityRefs(refs: [EntityRefInput!]!): [EntityRef!]! # Find asset types by unique id. assetTypesById(ids: [ID!]!): [AssetType!]! # Find asset types by unique token. assetTypesByToken(tokens: [String!]!): [AssetType!]! # List asset types that meet criteria. assetTypes(criteria: AssetTypeSearchCriteria!): AssetTypeSearchResults! # Find assets by unique id. assetsById(ids: [ID!]!): [Asset!]! # Find assets by unique token. assetsByToken(tokens: [String!]!): [Asset!]! # List assets that meet criteria. assets(criteria: AssetSearchCriteria!): AssetSearchResults! # An asset type's published property-contract versions, newest first. # At most 1000 per call (newest first); limit (at least 1) narrows that and offset (not negative) pages further. assetTypeVersions(token: String!, limit: Int, offset: Int): [AssetTypeVersion!]! # The version an asset of this type is currently validated against, or null when # the type has never been published. activeAssetTypeVersion(token: String!): AssetTypeVersion # Find customer types by unique id. customerTypesById(ids: [ID!]!): [CustomerType!]! # Find customer types by unique token. customerTypesByToken(tokens: [String!]!): [CustomerType!]! # List customer types that meet criteria. customerTypes(criteria: CustomerTypeSearchCriteria!): CustomerTypeSearchResults! # Find customers by unique id. customersById(ids: [ID!]!): [Customer!]! # Find customers by unique token. customersByToken(tokens: [String!]!): [Customer!]! # List customers that meet criteria. customers(criteria: CustomerSearchCriteria!): CustomerSearchResults! # Find area types by unique id. areaTypesById(ids: [ID!]!): [AreaType!]! # Find area types by unique token. areaTypesByToken(tokens: [String!]!): [AreaType!]! # List area types that meet criteria. areaTypes(criteria: AreaTypeSearchCriteria!): AreaTypeSearchResults! # Find areas by unique id. areasById(ids: [ID!]!): [Area!]! # Find areas by unique token. areasByToken(tokens: [String!]!): [Area!]! # List areas that meet criteria. areas(criteria: AreaSearchCriteria!): AreaSearchResults! } # Contains mutations executed against model. type Mutation { """Creates a device type and returns it. Requires device:write.""" createDeviceType( """The new device type's fields. Although the schema lets it be omitted, a call without it fails.""" request: DeviceTypeCreateRequest ): DeviceType! """ Partially updates a device type and returns it. Fails if no device type has the token. Requires device:write. """ updateDeviceType( """Token of the device type to update.""" token: String! """The fields to change.""" request: DeviceTypeUpdateRequest! ): DeviceType! """ Deletes a device type. Returns true when it was deleted and false when no device type has the token. Refused with REFERENCE_VIOLATION while any device has this type. The profile it adopted is not affected. Requires device:write. """ deleteDeviceType( """Token of the device type to delete.""" token: String! ): Boolean! """Creates a device profile, with no definitions and no published version, and returns it. Requires device:write.""" createDeviceProfile( """The new profile's fields. Although the schema lets it be omitted, a call without it fails.""" request: DeviceProfileCreateRequest ): DeviceProfile! """ Partially updates a device profile's draft and returns it. Fails if no profile has the token. Requires device:write. """ updateDeviceProfile( """Token of the profile to update.""" token: String! """The fields to change.""" request: DeviceProfileUpdateRequest! ): DeviceProfile! # REFUSED outright once the profile has been published or adopted by a device type — # from that point the token is the key published rules are filed under and rostered # devices are tracked by, so a rename would silently stop detections and orphan # dead-man entries. """ Changes a device profile's token and returns the profile. Allowed only while the profile has never been published and no device type currently adopts it; once it has been published, its token is permanent. Renaming to the current token succeeds without change. Fails if no profile has the token. Requires device:write. """ renameDeviceProfile( """Current token of the profile.""" token: String! """ The new token, in the usual token format. A blank value is refused, and a token another profile in the tenant holds is refused with CONFLICT. """ newToken: String! ): DeviceProfile! """ Replaces a profile's draft device-visible configuration declaration with the given keys; an empty list withdraws it. At most 256 keys; each key must follow the attribute-key grammar, be unique, and carry a valid value type. The whole call is refused if any key is invalid. Published versions are immutable: this changes only what the next publish freezes. Returns the updated profile. Requires device:write. """ setDeviceProfileConfigurationDeclaration( """Token of the profile.""" token: String! """The complete new declaration, replacing the current one.""" keys: [ConfigurationKeyInput!]! ): DeviceProfile! """ Deletes a device profile together with its metric, command and detection-rule definitions and all its published versions. Returns true when it was deleted and false when no profile has the token. Refused with REFERENCE_VIOLATION while any device type adopts it. Requires device:write. """ deleteDeviceProfile( """Token of the profile to delete.""" token: String! ): Boolean! # versioning; the detection-rule check is the sync gate against # event-processing. """ Freezes the profile's current draft (its metric, command and detection-rule definitions, its position declaration and its configuration declaration) into a new version and makes that version the one devices use. The draft's enabled detection rules are checked first; a rejected rule, or a check that cannot be completed, fails the publish and nothing is stored. Fails if no profile has the token. Returns the new version. Requires device:write. """ publishDeviceProfile( """Token of the profile to publish.""" token: String! """Optional label for the version, up to 128 characters, such as a release name.""" label: String """Optional notes for the version, up to 1024 characters.""" description: String ): DeviceProfileVersion! """ Makes an existing published version the one devices use again. The draft and the version history are untouched, so a later version can be made active the same way. Fails if the profile or the version does not exist. Returns the updated profile. Requires device:write. """ rollbackDeviceProfile( """Token of the profile.""" token: String! """Number of the published version to make active.""" version: Int! ): DeviceProfile! """ Adds a metric definition to a profile's draft and returns it. It reaches devices when the profile is next published. Requires device:write. """ createMetricDefinition( """The new metric definition's fields. Although the schema lets it be omitted, a call without it fails.""" request: MetricDefinitionCreateRequest ): MetricDefinition! """ Partially updates a draft metric definition and returns it. Fails if no metric definition has the token. Requires device:write. """ updateMetricDefinition( """Token of the metric definition to update.""" token: String! """The fields to change.""" request: MetricDefinitionUpdateRequest! ): MetricDefinition! """ Removes a metric definition from a profile's draft. Published versions keep their copy. Returns true when it was deleted and false when nothing has the token. Requires device:write. """ deleteMetricDefinition( """Token of the metric definition to delete.""" token: String! ): Boolean! """ Adds a command definition to a profile's draft and returns it. It reaches devices when the profile is next published. Requires device:write. """ createCommandDefinition( """The new command definition's fields. Although the schema lets it be omitted, a call without it fails.""" request: CommandDefinitionCreateRequest ): CommandDefinition! """ Partially updates a draft command definition and returns it. Fails if no command definition has the token. Requires device:write. """ updateCommandDefinition( """Token of the command definition to update.""" token: String! """The fields to change.""" request: CommandDefinitionUpdateRequest! ): CommandDefinition! """ Removes a command definition from a profile's draft. Published versions keep their copy. Returns true when it was deleted and false when nothing has the token. Requires device:write. """ deleteCommandDefinition( """Token of the command definition to delete.""" token: String! ): Boolean! # Create a new detection rule. createDetectionRule(request: DetectionRuleCreateRequest!): DetectionRule! # Update an existing detection rule. updateDetectionRule(token: String!, request: DetectionRuleUpdateRequest!): DetectionRule! # Delete a single detection rule. deleteDetectionRule(token: String!): Boolean! # Create a new geofence. Takes device:write AND location:read: a fence is a question about # where a device is, and whoever can shape the question can extract the answer — a caller able # to mint fences can narrow one until a containment rule's raise/resolve edge reveals a # device's actual coordinates. Reading fences stays on device:read, since a fence's geometry # is a region someone drew, not a device's position. createGeoFence(request: GeoFenceCreateRequest!): GeoFence! # Update an existing geofence. Same authorities as creation, for the same reason: reshaping a # fence is the same question-shaping primitive as minting one. The request's `token` must # equal the `token` argument: a fence's token is immutable, because rules name fences by # token and this service cannot rewrite the rules that would be left naming nothing. updateGeoFence(token: String!, request: GeoFenceUpdateRequest!): GeoFence! # Delete a single geofence. device:write alone — removing a fence constructs no question and # answers none, and delete-then-recreate is no way around the gate on creation. deleteGeoFence(token: String!): Boolean! # Acknowledge a raised alarm (operator action). The acknowledging identity is the # authenticated subject, recorded server-side — not a client-supplied value. acknowledgeAlarm(token: String!): Alarm! # Acknowledge many raised alarms at once — the fan-out counterpart of acknowledgeAlarm, for # an operator working through an alarm storm. Same authority, same transition, and the same # server-side acknowledging identity. # # 🔑 IT IS PARTIAL. A token naming no alarm is REFUSED and named in `refusals`; every other # alarm is still acknowledged. A stale selection is the normal case here — alarms are being # erased and devices deleted while an operator works — so refusing the whole request over one # vanished row would leave no way to make progress but to bisect the list by hand. # # 🔴 AN EMPTY LIST ACKNOWLEDGES NOTHING. It is not "all alarms" and never has been. # # At most 2000 tokens per request. Over that is a GraphQL ERROR rather than a refusal list: # it is a fault in how the caller chunked its work, not a verdict about any alarm. Duplicate # tokens are collapsed and both result lists come back in the order each token was first # named, so the answer is a deterministic function of the request. acknowledgeAlarms(tokens: [String!]!): BulkAcknowledgeAlarmsResult! # Clear a raised alarm (manual operator override). clearAlarm(token: String!): Alarm! # Set (upsert) an entity attribute. setEntityAttribute(request: EntityAttributeSetRequest!): EntityAttribute! # Delete an entity attribute by its natural key. deleteEntityAttribute(entityType: String!, entity: String!, scope: String!, attrKey: String!): Boolean! """Creates a device and returns it. Requires device:write.""" createDevice( """The new device's fields. Although the schema lets it be omitted, a call without it fails.""" request: DeviceCreateRequest ): Device! """ Creates up to 1000 devices of one type from templates, in one transaction: every device is created, or none is (an invalid template, a rendered token that repeats or is already in use, or an unknown device type fails them all). Returns the created devices in index order. Requires device:write. """ createDevices( """The type, count and templates of the devices to create.""" request: DeviceBulkCreateRequest! ): [Device!]! """ Partially updates a device and returns it. Fails if no device has the token. Requires device:write. """ updateDevice( """Token of the device to update.""" token: String! """The fields to change.""" request: DeviceUpdateRequest! ): Device! """ Deletes a device together with its credentials, replacement history, relationships, attributes, alarms and group memberships. Returns true when it was deleted and false when no device has the token. Requires device:write. """ deleteDevice( """Token of the device to delete.""" token: String! ): Boolean! # Create a new entity group. A static group's members are managed over # "member" relationship edges; a dynamic group carries a CEL selector and resolves # its members eval-on-read (validated + cost-gated at create). createEntityGroup(request: EntityGroupCreateRequest): EntityGroup! # Update an existing entity group's presentation fields (member family + mode # are immutable). updateEntityGroup(token: String!, request: EntityGroupUpdateRequest!): EntityGroup! # Delete an entity group, its relationship edges, and its frozen versions. # Refused (ErrEntityInUse) while a detection rule scopes to the group. deleteEntityGroup(token: String!): Boolean! # Publish a DYNAMIC entity group's current draft selector as a new immutable # version and make it the active version a rule-scoped resolve reads. # label/description are optional. Rejected for a static group. publishEntityGroup(token: String!, label: String, description: String): EntityGroupVersion! # Re-point a dynamic group's active version at an existing version — a # non-destructive rollback; the draft is untouched. Returns the updated group. rollbackEntityGroup(token: String!, version: Int!): EntityGroup! # Declare (upsert) a facet key by its natural key (memberType, key). setFacetKey(request: FacetKeySetRequest): FacetKey! # Delete a facet key by its natural key. Returns whether a row was removed; a # system facet is non-deletable and its removal is refused. deleteFacetKey(memberType: String!, key: String!): Boolean! # Create a new device credential. createDeviceCredential(request: DeviceCredentialCreateRequest): DeviceCredential! # Update an existing device credential. updateDeviceCredential(token: String!, request: DeviceCredentialUpdateRequest!): DeviceCredential! # Delete a single device credential. deleteDeviceCredential(token: String!): Boolean! # Bind a NEW physical unit to an EXISTING logical device identity: retire every # credential the outgoing unit could authenticate with, mint one for the # incoming unit, and write an append-only record of the swap - all in one # transaction. The device's history, alarms, relationships, group membership and # command vocabulary carry forward because none of them are touched. replaceDevice(request: DeviceReplaceRequest!): DeviceReplaceResult! # Place an asset under a parent, replacing whatever parent it had. Refused when # it would make an asset its own ancestor, give it a second parent, or push the # tree past its depth limit. The asset's own children travel with it. setAssetParent(childToken: String!, parentToken: String!): EntityRelationship! # Detach an asset from its parent, making it a root. Its children travel with # it. Reports whether an edge was removed, so "already a root" is # distinguishable from "done". clearAssetParent(childToken: String!): Boolean! # Create a new provisioning profile. createProvisioningProfile(request: ProvisioningProfileCreateRequest): ProvisioningProfile! # Update an existing provisioning profile. updateProvisioningProfile(token: String!, request: ProvisioningProfileUpdateRequest!): ProvisioningProfile! # Delete a single provisioning profile. deleteProvisioningProfile(token: String!): Boolean! # Open (or reopen) a possession claim on a device with a secret an eventual # owner will present. initiateDeviceClaim(request: DeviceClaimInitiateRequest!): DeviceClaim! # Redeem a device's claim by presenting its secret, assigning the device to a # customer via a relationship of the given type. Returns the created edge. claimDevice(request: DeviceClaimRequest!): EntityRelationship! # Cancel a device's open claim without assigning it. Returns whether an open # claim was closed. cancelDeviceClaim(deviceToken: String!): Boolean! # Create a new entity relationship type. createEntityRelationshipType(request: EntityRelationshipTypeCreateRequest!): EntityRelationshipType! # Update an existing entity relationship type. updateEntityRelationshipType(token: String!, request: EntityRelationshipTypeUpdateRequest!): EntityRelationshipType! # Delete a relationship type. Refused while any edge still uses it. deleteEntityRelationshipType(token: String!): Boolean! # Create a new entity relationship. createEntityRelationship(request: EntityRelationshipCreateRequest!): EntityRelationship! # Remove a single relationship edge — the explicit "unassign" operation. removeEntityRelationship(token: String!): Boolean! # Create multiple entity relationships in one transaction (bulk "add members" / # "assign"). Auto-provisions the reserved "member" relationship type on first use. createEntityRelationships(requests: [EntityRelationshipCreateRequest!]!): [EntityRelationship!]! # Remove multiple relationship edges by token (bulk "remove members" / "unassign"). removeEntityRelationships(tokens: [String!]!): Boolean! # Create a new asset type. createAssetType(request: AssetTypeCreateRequest): AssetType! # Update an existing asset type. updateAssetType(token: String!, request: AssetTypeUpdateRequest!): AssetType! # Delete an asset type. Refused while any asset references it; its published # version history goes with it. deleteAssetType(token: String!): Boolean! # Freeze an asset type's draft property contract into a new immutable version and # make it the one its assets are validated against. Refused when the type has no # draft contract to freeze. publishAssetType(token: String!, label: String, description: String): AssetTypeVersion! # Re-point an asset type's active version at an earlier published one. The draft # is untouched and nothing is deleted, so it can be rolled forward again. rollbackAssetType(token: String!, version: Int!): AssetType! # Create a new asset. createAsset(request: AssetCreateRequest): Asset! # Update an existing asset. updateAsset(token: String!, request: AssetUpdateRequest!): Asset! # Delete an asset and its relationship edges. deleteAsset(token: String!): Boolean! # Create a new customer type. createCustomerType(request: CustomerTypeCreateRequest): CustomerType! # Update an existing customer type. updateCustomerType(token: String!, request: CustomerTypeUpdateRequest!): CustomerType! # Delete a customer type. Refused while any customer references it. deleteCustomerType(token: String!): Boolean! # Create a new customer. createCustomer(request: CustomerCreateRequest): Customer! # Update an existing customer. updateCustomer(token: String!, request: CustomerUpdateRequest!): Customer! # Delete a customer and its relationship edges. deleteCustomer(token: String!): Boolean! # Create a new area type. createAreaType(request: AreaTypeCreateRequest): AreaType! # Update an existing area type. updateAreaType(token: String!, request: AreaTypeUpdateRequest!): AreaType! # Delete an area type. Refused while any area references it. deleteAreaType(token: String!): Boolean! # Create a new area. createArea(request: AreaCreateRequest): Area! # Update an existing area. updateArea(token: String!, request: AreaUpdateRequest!): Area! # Delete an area and its relationship edges. deleteArea(token: String!): Boolean! } # A single alarm state-change, pushed live to a subscriber. It # is the transition envelope re-emitted by the alarm integrator and the operator API — not # the stored Alarm row — so it carries the transition kind (eventType) and, for a # severity move, the prior severity, alongside a snapshot of the alarm after the # change. Address the alarm by token to re-query it; the originator is the uniform # (type, id) reference, with originatorToken resolved on demand for a device. type AlarmEvent { # One of RAISED, ESCALATED, DEESCALATED, CLEARED, ACKNOWLEDGED. eventType: String! # Token of the alarm this event is about (stable across its lifecycle). alarmToken: String! originatorType: String! originatorId: ID! # The originator's token, resolved on demand (device originators only today); # null when the originator is not a device or no longer exists. originatorToken: String alarmKey: String! metricKey: String! # Alarm state after the transition: ACTIVE | CLEARED. state: String! # Severity after the transition. severity: String! # The severity held before an in-place change; set only for ESCALATED/DEESCALATED. previousSeverity: String acknowledged: Boolean! acknowledgedBy: String lastValue: Float # When the current alarm cycle began (pairs with occurredTime for "active 42m"). raisedTime: String # The event time that drove the transition (measurement time, or operation time # for an operator ack/clear). occurredTime: String! } # Live alarm stream. Pushes each alarm transition to the subscriber as it # happens — scoped to the subscriber's tenant — from subscribe time onward with no # historical backfill (use the alarm queries for current state). Optional filters # narrow the feed to one originator, state, severity, or alarm key. # # Delivery is best-effort (at-most-once): the stored Alarm row is the source of # truth, and an event may be dropped (a transient publish failure, or a slow # consumer). A client that merges this stream into a live list must reconcile — re-run # the alarm queries on reconnect and periodically — so a dropped event can't leave a # stale row (e.g. a missed CLEARED showing a ghost active alarm). type Subscription { # Streams alarm state-change events. When originator is supplied, originatorType # is required and the token is resolved to its id once at subscribe time. alarmStream( originatorType: String originator: String state: String severity: String alarmKey: String ): AlarmEvent! } schema { query: Query mutation: Mutation subscription: Subscription }