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}