Skip to main content

fraiseql_cli/schema/
artifact.rs

1//! What one compile writes to `schema.compiled.json`.
2//!
3//! The file is not the serialization of a single type. It is the core
4//! [`CompiledSchema`] plus the **platform-extension sections that sit beside it**:
5//! keys the GraphQL engine knows nothing about, read by subsystems that are compiled
6//! in optionally. `functions` is the first such section, and it cannot be a
7//! `CompiledSchema` field for a structural reason — the crate that defines
8//! [`FunctionDefinition`](fraiseql_functions::FunctionDefinition) already depends on
9//! `fraiseql-core`, so the dependency cannot run back the other way.
10//!
11//! Before #1325 that distinction did not matter, because nothing could author a
12//! `functions` section: `IntermediateSchema` had no field for it, and the only writer
13//! of one in the whole tree was a test that hand-built the JSON. The section existed
14//! on the reading side only — `fraiseql-server`'s `ExtendedCompiledSchema` parsed a
15//! key the compiler could not emit.
16//!
17//! This type is the writing side of that pair, and [`CompiledArtifact::to_json_value`]
18//! is the one routine that assembles the file — so the `_content_hash` covers every
19//! section rather than only the ones that happen to be `CompiledSchema` fields.
20
21use anyhow::{Context, Result};
22use fraiseql_core::schema::CompiledSchema;
23use fraiseql_functions::FunctionsConfig;
24
25/// The compiled schema together with the platform sections written alongside it.
26#[derive(Debug, Clone)]
27pub struct CompiledArtifact {
28    /// GraphQL types, queries, mutations, subscriptions and their generated SQL.
29    pub schema: CompiledSchema,
30
31    /// The compiled `functions` section (#1325), when the project declares one.
32    ///
33    /// `None` — not an empty section — when it declares none, so a project that has
34    /// never heard of functions produces byte-for-byte the artifact it did before.
35    pub functions: Option<FunctionsConfig>,
36}
37
38impl CompiledArtifact {
39    /// Assemble the artifact's JSON: the compiled schema, plus each platform section
40    /// spliced in as a sibling key.
41    ///
42    /// Hashing happens **after** this, on the whole object, so schema integrity
43    /// (`_content_hash`, #899) covers a tampered `functions` section exactly as it
44    /// covers a tampered query.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the compiled schema or one of its sibling sections fails
49    /// to serialize, or if the schema does not serialize to a JSON object.
50    pub fn to_json_value(&self) -> Result<serde_json::Value> {
51        // Serialize through a string exactly as the writer always has, so the value
52        // being hashed is the value that reaches disk.
53        let body = serde_json::to_string_pretty(&self.schema)
54            .context("Failed to serialize compiled schema")?;
55        let mut value: serde_json::Value = serde_json::from_str(&body)?;
56
57        if let Some(functions) = &self.functions {
58            let obj = value.as_object_mut().context("schema must serialise as JSON object")?;
59            obj.insert(
60                "functions".to_string(),
61                serde_json::to_value(functions).context("Failed to serialize functions section")?,
62            );
63        }
64
65        Ok(value)
66    }
67}