@lix-js/sdk 0.6.0-preview.2 → 0.6.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +4 -5
- package/dist/engine-wasm/wasm/lix_engine.js +1 -1
- package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
- package/dist/generated/builtin-schemas.d.ts +87 -162
- package/dist/generated/builtin-schemas.js +139 -236
- package/dist/open-lix.d.ts +1 -1
- package/dist-engine-src/src/binary_cas/types.rs +0 -6
- package/dist-engine-src/src/catalog/context.rs +412 -0
- package/dist-engine-src/src/catalog/mod.rs +10 -0
- package/dist-engine-src/src/catalog/schema.rs +4 -0
- package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
- package/dist-engine-src/src/cel/mod.rs +1 -1
- package/dist-engine-src/src/cel/provider.rs +1 -1
- package/dist-engine-src/src/commit_graph/context.rs +328 -1015
- package/dist-engine-src/src/commit_graph/mod.rs +2 -3
- package/dist-engine-src/src/commit_graph/types.rs +7 -43
- package/dist-engine-src/src/commit_graph/walker.rs +57 -81
- package/dist-engine-src/src/commit_store/codec.rs +887 -0
- package/dist-engine-src/src/commit_store/context.rs +944 -0
- package/dist-engine-src/src/commit_store/materialization.rs +84 -0
- package/dist-engine-src/src/commit_store/mod.rs +16 -0
- package/dist-engine-src/src/commit_store/storage.rs +600 -0
- package/dist-engine-src/src/commit_store/types.rs +215 -0
- package/dist-engine-src/src/common/identity.rs +15 -5
- package/dist-engine-src/src/common/json_pointer.rs +67 -0
- package/dist-engine-src/src/common/metadata.rs +17 -12
- package/dist-engine-src/src/common/mod.rs +5 -5
- package/dist-engine-src/src/domain.rs +324 -0
- package/dist-engine-src/src/engine.rs +29 -43
- package/dist-engine-src/src/entity_identity.rs +238 -118
- package/dist-engine-src/src/functions/context.rs +17 -52
- package/dist-engine-src/src/functions/deterministic.rs +1 -1
- package/dist-engine-src/src/functions/mod.rs +1 -1
- package/dist-engine-src/src/functions/provider.rs +4 -4
- package/dist-engine-src/src/functions/state.rs +39 -66
- package/dist-engine-src/src/functions/types.rs +1 -1
- package/dist-engine-src/src/init.rs +204 -151
- package/dist-engine-src/src/json_store/context.rs +354 -60
- package/dist-engine-src/src/json_store/encoded.rs +6 -6
- package/dist-engine-src/src/json_store/mod.rs +4 -1
- package/dist-engine-src/src/json_store/store.rs +884 -11
- package/dist-engine-src/src/json_store/types.rs +166 -1
- package/dist-engine-src/src/lib.rs +10 -9
- package/dist-engine-src/src/live_state/context.rs +608 -830
- package/dist-engine-src/src/live_state/mod.rs +3 -3
- package/dist-engine-src/src/live_state/overlay.rs +7 -7
- package/dist-engine-src/src/live_state/reader.rs +5 -5
- package/dist-engine-src/src/live_state/types.rs +19 -36
- package/dist-engine-src/src/live_state/visibility.rs +19 -14
- package/dist-engine-src/src/plugin/archive.rs +3 -6
- package/dist-engine-src/src/plugin/install.rs +0 -18
- package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
- package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
- package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
- package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
- package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
- package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
- package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
- package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
- package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
- package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
- package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
- package/dist-engine-src/src/schema/compatibility.rs +787 -0
- package/dist-engine-src/src/schema/definition.json +47 -17
- package/dist-engine-src/src/schema/definition.rs +202 -96
- package/dist-engine-src/src/schema/key.rs +9 -77
- package/dist-engine-src/src/schema/mod.rs +4 -4
- package/dist-engine-src/src/schema/tests.rs +133 -92
- package/dist-engine-src/src/session/context.rs +40 -42
- package/dist-engine-src/src/session/create_version.rs +22 -14
- package/dist-engine-src/src/session/execute.rs +45 -14
- package/dist-engine-src/src/session/merge/apply.rs +4 -4
- package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
- package/dist-engine-src/src/session/merge/stats.rs +1 -1
- package/dist-engine-src/src/session/merge/version.rs +35 -45
- package/dist-engine-src/src/session/mod.rs +4 -2
- package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
- package/dist-engine-src/src/session/switch_version.rs +16 -28
- package/dist-engine-src/src/sql2/change_provider.rs +14 -20
- package/dist-engine-src/src/sql2/classify.rs +61 -26
- package/dist-engine-src/src/sql2/context.rs +22 -18
- package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
- package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
- package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
- package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
- package/dist-engine-src/src/sql2/error.rs +21 -1
- package/dist-engine-src/src/sql2/execute.rs +325 -264
- package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
- package/dist-engine-src/src/sql2/file_provider.rs +533 -108
- package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
- package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
- package/dist-engine-src/src/sql2/history_projection.rs +3 -27
- package/dist-engine-src/src/sql2/history_provider.rs +11 -17
- package/dist-engine-src/src/sql2/history_route.rs +22 -8
- package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
- package/dist-engine-src/src/sql2/mod.rs +6 -3
- package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
- package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
- package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
- package/dist-engine-src/src/sql2/public_bind/dml.rs +166 -0
- package/dist-engine-src/src/sql2/public_bind/mod.rs +25 -0
- package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
- package/dist-engine-src/src/sql2/read_only.rs +10 -12
- package/dist-engine-src/src/sql2/session.rs +7 -10
- package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
- package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
- package/dist-engine-src/src/sql2/udfs/public_call.rs +211 -0
- package/dist-engine-src/src/sql2/version_provider.rs +46 -31
- package/dist-engine-src/src/sql2/version_scope.rs +4 -4
- package/dist-engine-src/src/storage_bench.rs +1782 -325
- package/dist-engine-src/src/test_support.rs +183 -36
- package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
- package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
- package/dist-engine-src/src/tracked_state/context.rs +1155 -271
- package/dist-engine-src/src/tracked_state/diff.rs +249 -57
- package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
- package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
- package/dist-engine-src/src/tracked_state/merge.rs +37 -19
- package/dist-engine-src/src/tracked_state/mod.rs +8 -7
- package/dist-engine-src/src/tracked_state/storage.rs +138 -6
- package/dist-engine-src/src/tracked_state/tree.rs +695 -252
- package/dist-engine-src/src/tracked_state/types.rs +176 -6
- package/dist-engine-src/src/transaction/commit.rs +695 -435
- package/dist-engine-src/src/transaction/context.rs +551 -310
- package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
- package/dist-engine-src/src/transaction/mod.rs +2 -0
- package/dist-engine-src/src/transaction/normalization.rs +311 -447
- package/dist-engine-src/src/transaction/prep.rs +37 -0
- package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
- package/dist-engine-src/src/transaction/staging.rs +701 -406
- package/dist-engine-src/src/transaction/types.rs +231 -122
- package/dist-engine-src/src/transaction/validation.rs +2717 -1698
- package/dist-engine-src/src/untracked_state/codec.rs +40 -96
- package/dist-engine-src/src/untracked_state/context.rs +21 -5
- package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
- package/dist-engine-src/src/untracked_state/mod.rs +3 -5
- package/dist-engine-src/src/untracked_state/storage.rs +105 -57
- package/dist-engine-src/src/untracked_state/types.rs +63 -13
- package/dist-engine-src/src/version/context.rs +1 -13
- package/dist-engine-src/src/version/lifecycle.rs +221 -0
- package/dist-engine-src/src/version/mod.rs +3 -2
- package/dist-engine-src/src/version/refs.rs +12 -103
- package/dist-engine-src/src/version/stage_rows.rs +15 -19
- package/package.json +1 -1
- package/dist-engine-src/src/changelog/codec.rs +0 -321
- package/dist-engine-src/src/changelog/context.rs +0 -92
- package/dist-engine-src/src/changelog/materialization.rs +0 -121
- package/dist-engine-src/src/changelog/mod.rs +0 -13
- package/dist-engine-src/src/changelog/reader.rs +0 -20
- package/dist-engine-src/src/changelog/storage.rs +0 -220
- package/dist-engine-src/src/changelog/types.rs +0 -38
- package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
- package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
- package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
- package/dist-engine-src/src/schema_registry.rs +0 -294
- package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
- package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
- package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"title": "Lix Schema Definition",
|
|
4
|
-
"description": "A Lix schema is a JSON Schema draft 2020-12 document augmented with `x-lix-*` extensions that identify
|
|
4
|
+
"description": "A Lix schema is a JSON Schema draft 2020-12 document augmented with `x-lix-*` extensions that identify and constrain an entity type. Every schema must declare `x-lix-key` (snake_case identifier, preferably prefixed with a plugin or domain namespace such as `library_book` to avoid collisions) and `additionalProperties: false`; add `x-lix-primary-key` (array of JSON Pointers to required string properties) to make the schema writable. Lix will auto-materialize a public virtual table named after `x-lix-key` with an `INSERT/UPDATE/DELETE` surface. See the `examples` field for a minimal working schema.",
|
|
5
5
|
"examples": [
|
|
6
6
|
{
|
|
7
7
|
"x-lix-key": "library_book",
|
|
8
|
-
"x-lix-version": "1",
|
|
9
8
|
"type": "object",
|
|
10
|
-
"x-lix-primary-key": [
|
|
9
|
+
"x-lix-primary-key": [
|
|
10
|
+
"/id"
|
|
11
|
+
],
|
|
11
12
|
"properties": {
|
|
12
|
-
"id": {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
"id": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"x-lix-default": "lix_uuid_v7()"
|
|
16
|
+
},
|
|
17
|
+
"title": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
},
|
|
20
|
+
"author": {
|
|
21
|
+
"type": "string"
|
|
22
|
+
}
|
|
15
23
|
},
|
|
16
|
-
"required": [
|
|
24
|
+
"required": [
|
|
25
|
+
"id",
|
|
26
|
+
"title"
|
|
27
|
+
],
|
|
17
28
|
"additionalProperties": false
|
|
18
29
|
}
|
|
19
30
|
],
|
|
@@ -47,7 +58,7 @@
|
|
|
47
58
|
"type": "array",
|
|
48
59
|
"minItems": 1,
|
|
49
60
|
"uniqueItems": true,
|
|
50
|
-
"description": "Primary-key fields as JSON Pointers (RFC 6901) into
|
|
61
|
+
"description": "Primary-key fields as JSON Pointers (RFC 6901) into required string-valued entity properties, e.g. `[\"/id\"]` for a single-column key or `[\"/tenant_id\", \"/handle\"]` for a composite key. Note the leading slash; `\"id\"` without a slash is not a valid pointer.",
|
|
51
62
|
"items": {
|
|
52
63
|
"type": "string",
|
|
53
64
|
"format": "json-pointer",
|
|
@@ -103,6 +114,34 @@
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
},
|
|
117
|
+
"x-lix-state-foreign-keys": {
|
|
118
|
+
"type": "array",
|
|
119
|
+
"description": "Foreign keys from local fields to arbitrary live state rows. Each entry is exactly three required local JSON Pointers ordered as `[entity_id, schema_key, file_id]`: index 0 points to the local entity_id JSON array, index 1 points to the local schema_key string, and index 2 points to the local file_id string-or-null. Use explicit null for global file_id targets; omitted fields are invalid. The referenced state row is resolved in the same version.",
|
|
120
|
+
"items": {
|
|
121
|
+
"type": "array",
|
|
122
|
+
"minItems": 3,
|
|
123
|
+
"maxItems": 3,
|
|
124
|
+
"uniqueItems": true,
|
|
125
|
+
"prefixItems": [
|
|
126
|
+
{
|
|
127
|
+
"type": "string",
|
|
128
|
+
"format": "json-pointer",
|
|
129
|
+
"description": "[0] Local JSON Pointer for the target entity_id. The value must be a non-empty JSON array of strings."
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"type": "string",
|
|
133
|
+
"format": "json-pointer",
|
|
134
|
+
"description": "[1] Local JSON Pointer for the target schema_key. The value must be a string."
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"type": "string",
|
|
138
|
+
"format": "json-pointer",
|
|
139
|
+
"description": "[2] Local JSON Pointer for the target file_id. The value must be a string or null."
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"items": false
|
|
143
|
+
}
|
|
144
|
+
},
|
|
106
145
|
"x-lix-key": {
|
|
107
146
|
"type": "string",
|
|
108
147
|
"pattern": "^[a-z][a-z0-9_]*$",
|
|
@@ -112,14 +151,6 @@
|
|
|
112
151
|
"csv_plugin_cell"
|
|
113
152
|
]
|
|
114
153
|
},
|
|
115
|
-
"x-lix-version": {
|
|
116
|
-
"type": "string",
|
|
117
|
-
"pattern": "^[1-9]\\d*$",
|
|
118
|
-
"description": "Schema version identifier. Deliberately constrained to a monotonic integer (as a string) without leading zeros to keep translation rules open while avoiding future breaking changes.",
|
|
119
|
-
"examples": [
|
|
120
|
-
"1"
|
|
121
|
-
]
|
|
122
|
-
},
|
|
123
154
|
"properties": {
|
|
124
155
|
"type": "object",
|
|
125
156
|
"additionalProperties": {
|
|
@@ -149,7 +180,6 @@
|
|
|
149
180
|
},
|
|
150
181
|
"required": [
|
|
151
182
|
"x-lix-key",
|
|
152
|
-
"x-lix-version",
|
|
153
183
|
"additionalProperties"
|
|
154
184
|
]
|
|
155
185
|
}
|
|
@@ -4,6 +4,7 @@ use serde_json::Value as JsonValue;
|
|
|
4
4
|
use std::collections::BTreeSet;
|
|
5
5
|
use std::sync::OnceLock;
|
|
6
6
|
|
|
7
|
+
use crate::common::parse_json_pointer;
|
|
7
8
|
use crate::LixError;
|
|
8
9
|
|
|
9
10
|
static LIX_SCHEMA_DEFINITION: OnceLock<JsonValue> = OnceLock::new();
|
|
@@ -11,9 +12,6 @@ static LIX_SCHEMA_VALIDATOR: OnceLock<Result<JSONSchema, LixError>> = OnceLock::
|
|
|
11
12
|
|
|
12
13
|
pub fn lix_schema_definition() -> &'static JsonValue {
|
|
13
14
|
LIX_SCHEMA_DEFINITION.get_or_init(|| {
|
|
14
|
-
// NOTE: x-lix-version is intentionally constrained to a monotonic integer (as a string).
|
|
15
|
-
// This keeps translation rules open while avoiding a future breaking change when versioning
|
|
16
|
-
// semantics become concrete.
|
|
17
15
|
let raw = include_str!("definition.json");
|
|
18
16
|
serde_json::from_str(raw).expect("definition.json must be valid JSON")
|
|
19
17
|
})
|
|
@@ -27,6 +25,9 @@ pub fn validate_lix_schema_definition(schema: &JsonValue) -> Result<(), LixError
|
|
|
27
25
|
if let Some(err) = detect_missing_pointer_slash(schema) {
|
|
28
26
|
return Err(err);
|
|
29
27
|
}
|
|
28
|
+
if let Some(err) = detect_state_foreign_key_tuple_shape(schema) {
|
|
29
|
+
return Err(err);
|
|
30
|
+
}
|
|
30
31
|
|
|
31
32
|
let validator = lix_schema_validator()?;
|
|
32
33
|
if let Err(errors) = validator.validate(schema) {
|
|
@@ -41,13 +42,39 @@ pub fn validate_lix_schema_definition(schema: &JsonValue) -> Result<(), LixError
|
|
|
41
42
|
|
|
42
43
|
assert_primary_key_pointers(schema)?;
|
|
43
44
|
assert_unique_pointers(schema)?;
|
|
44
|
-
|
|
45
|
+
assert_state_foreign_key_pointers(schema)?;
|
|
45
46
|
assert_known_x_lix_top_level_fields(schema)?;
|
|
47
|
+
assert_entity_properties_do_not_use_reserved_lix_prefix(schema)?;
|
|
46
48
|
assert_entity_properties_have_projectable_types(schema)?;
|
|
47
49
|
|
|
48
50
|
Ok(())
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
fn assert_entity_properties_do_not_use_reserved_lix_prefix(
|
|
54
|
+
schema: &JsonValue,
|
|
55
|
+
) -> Result<(), LixError> {
|
|
56
|
+
let Some(schema_key) = schema.get("x-lix-key").and_then(JsonValue::as_str) else {
|
|
57
|
+
return Ok(());
|
|
58
|
+
};
|
|
59
|
+
let Some(properties) = schema.get("properties").and_then(JsonValue::as_object) else {
|
|
60
|
+
return Ok(());
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for property_name in properties.keys() {
|
|
64
|
+
if property_name.starts_with("lix") {
|
|
65
|
+
return Err(LixError::new(
|
|
66
|
+
LixError::CODE_SCHEMA_DEFINITION,
|
|
67
|
+
format!(
|
|
68
|
+
"Invalid Lix schema definition: schema '{schema_key}' property '/{property_name}' uses reserved prefix 'lix'."
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
.with_hint("Property names starting with 'lix' are reserved for Lix system fields."));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Ok(())
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
fn assert_entity_properties_have_projectable_types(schema: &JsonValue) -> Result<(), LixError> {
|
|
52
79
|
let Some(schema_key) = schema.get("x-lix-key").and_then(JsonValue::as_str) else {
|
|
53
80
|
return Ok(());
|
|
@@ -57,9 +84,6 @@ fn assert_entity_properties_have_projectable_types(schema: &JsonValue) -> Result
|
|
|
57
84
|
};
|
|
58
85
|
|
|
59
86
|
for (property_name, property_schema) in properties {
|
|
60
|
-
if property_name.starts_with("lixcol_") {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
87
|
if !schema_property_has_sql_projection_type(property_schema) {
|
|
64
88
|
return Err(LixError::new(
|
|
65
89
|
LixError::CODE_SCHEMA_DEFINITION,
|
|
@@ -110,7 +134,8 @@ fn collect_schema_type_kinds<'a>(schema: &'a JsonValue, out: &mut BTreeSet<&'a s
|
|
|
110
134
|
|
|
111
135
|
/// Detect the common no-leading-slash mistake in JSON-Pointer-valued fields
|
|
112
136
|
/// (`x-lix-primary-key`, `x-lix-unique`, `x-lix-foreign-keys[].properties`,
|
|
113
|
-
/// `x-lix-foreign-keys[].references.properties
|
|
137
|
+
/// `x-lix-foreign-keys[].references.properties`,
|
|
138
|
+
/// `x-lix-state-foreign-keys[]`) and return a targeted
|
|
114
139
|
/// error + hint suggesting the fix.
|
|
115
140
|
///
|
|
116
141
|
/// Surfacing this before the meta-schema validator runs replaces the
|
|
@@ -166,6 +191,15 @@ fn detect_missing_pointer_slash(schema: &JsonValue) -> Option<LixError> {
|
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
|
|
194
|
+
if let Some(fks) = schema
|
|
195
|
+
.get("x-lix-state-foreign-keys")
|
|
196
|
+
.and_then(JsonValue::as_array)
|
|
197
|
+
{
|
|
198
|
+
for fk in fks {
|
|
199
|
+
collect(fk.as_array(), "x-lix-state-foreign-keys", &mut offenders);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
169
203
|
if offenders.is_empty() {
|
|
170
204
|
return None;
|
|
171
205
|
}
|
|
@@ -194,6 +228,26 @@ fn detect_missing_pointer_slash(schema: &JsonValue) -> Option<LixError> {
|
|
|
194
228
|
)
|
|
195
229
|
}
|
|
196
230
|
|
|
231
|
+
fn detect_state_foreign_key_tuple_shape(schema: &JsonValue) -> Option<LixError> {
|
|
232
|
+
let foreign_keys = schema
|
|
233
|
+
.get("x-lix-state-foreign-keys")
|
|
234
|
+
.and_then(JsonValue::as_array)?;
|
|
235
|
+
for (index, foreign_key) in foreign_keys.iter().enumerate() {
|
|
236
|
+
let Some(local_pointers) = foreign_key.as_array() else {
|
|
237
|
+
continue;
|
|
238
|
+
};
|
|
239
|
+
if local_pointers.len() != 3 {
|
|
240
|
+
return Some(LixError::new(
|
|
241
|
+
LixError::CODE_SCHEMA_DEFINITION,
|
|
242
|
+
format!(
|
|
243
|
+
"Invalid Lix schema definition: x-lix-state-foreign-keys[{index}] must contain exactly three JSON Pointers ordered as [entity_id, schema_key, file_id]; [0] entity_id, [1] schema_key, [2] file_id."
|
|
244
|
+
),
|
|
245
|
+
));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
None
|
|
249
|
+
}
|
|
250
|
+
|
|
197
251
|
pub fn validate_lix_schema(schema: &JsonValue, data: &JsonValue) -> Result<(), LixError> {
|
|
198
252
|
validate_lix_schema_definition(schema)?;
|
|
199
253
|
|
|
@@ -257,50 +311,6 @@ fn is_cel_expression(value: &str) -> bool {
|
|
|
257
311
|
Program::compile(value).is_ok()
|
|
258
312
|
}
|
|
259
313
|
|
|
260
|
-
fn parse_json_pointer(pointer: &str) -> Result<Vec<String>, LixError> {
|
|
261
|
-
if pointer.is_empty() {
|
|
262
|
-
return Ok(Vec::new());
|
|
263
|
-
}
|
|
264
|
-
if !pointer.starts_with('/') {
|
|
265
|
-
return Err(LixError {
|
|
266
|
-
code: LixError::CODE_SCHEMA_DEFINITION.to_string(),
|
|
267
|
-
message: "Invalid JSON pointer".to_string(),
|
|
268
|
-
hint: None,
|
|
269
|
-
details: None,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
let mut segments = Vec::new();
|
|
274
|
-
for raw in pointer[1..].split('/') {
|
|
275
|
-
segments.push(unescape_pointer_segment(raw)?);
|
|
276
|
-
}
|
|
277
|
-
Ok(segments)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
fn unescape_pointer_segment(segment: &str) -> Result<String, LixError> {
|
|
281
|
-
let mut out = String::new();
|
|
282
|
-
let mut chars = segment.chars();
|
|
283
|
-
while let Some(ch) = chars.next() {
|
|
284
|
-
if ch == '~' {
|
|
285
|
-
match chars.next() {
|
|
286
|
-
Some('0') => out.push('~'),
|
|
287
|
-
Some('1') => out.push('/'),
|
|
288
|
-
_ => {
|
|
289
|
-
return Err(LixError {
|
|
290
|
-
code: LixError::CODE_SCHEMA_DEFINITION.to_string(),
|
|
291
|
-
message: "Invalid JSON pointer".to_string(),
|
|
292
|
-
hint: None,
|
|
293
|
-
details: None,
|
|
294
|
-
})
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
out.push(ch);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
Ok(out)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
314
|
fn assert_primary_key_pointers(schema: &JsonValue) -> Result<(), LixError> {
|
|
305
315
|
let Some(primary_key) = schema
|
|
306
316
|
.get("x-lix-primary-key")
|
|
@@ -314,7 +324,10 @@ fn assert_primary_key_pointers(schema: &JsonValue) -> Result<(), LixError> {
|
|
|
314
324
|
continue;
|
|
315
325
|
};
|
|
316
326
|
let segments = parse_json_pointer(pointer)?;
|
|
317
|
-
|
|
327
|
+
let Some(property_schema) = (!segments.is_empty())
|
|
328
|
+
.then(|| schema_property(schema, &segments))
|
|
329
|
+
.flatten()
|
|
330
|
+
else {
|
|
318
331
|
return Err(LixError { code: LixError::CODE_SCHEMA_DEFINITION.to_string(), message: format!(
|
|
319
332
|
"Invalid Lix schema definition: x-lix-primary-key references missing property \"{}\".",
|
|
320
333
|
pointer
|
|
@@ -322,6 +335,22 @@ fn assert_primary_key_pointers(schema: &JsonValue) -> Result<(), LixError> {
|
|
|
322
335
|
hint: None,
|
|
323
336
|
details: None,
|
|
324
337
|
});
|
|
338
|
+
};
|
|
339
|
+
if !schema_property_is_string_only(property_schema) {
|
|
340
|
+
return Err(LixError::new(
|
|
341
|
+
LixError::CODE_SCHEMA_DEFINITION,
|
|
342
|
+
format!(
|
|
343
|
+
"Invalid Lix schema definition: x-lix-primary-key property \"{pointer}\" must have type \"string\"."
|
|
344
|
+
),
|
|
345
|
+
));
|
|
346
|
+
}
|
|
347
|
+
if !schema_pointer_is_required(schema, &segments) {
|
|
348
|
+
return Err(LixError::new(
|
|
349
|
+
LixError::CODE_SCHEMA_DEFINITION,
|
|
350
|
+
format!(
|
|
351
|
+
"Invalid Lix schema definition: x-lix-primary-key property \"{pointer}\" must be required."
|
|
352
|
+
),
|
|
353
|
+
));
|
|
325
354
|
}
|
|
326
355
|
}
|
|
327
356
|
|
|
@@ -360,33 +389,67 @@ fn assert_unique_pointers(schema: &JsonValue) -> Result<(), LixError> {
|
|
|
360
389
|
Ok(())
|
|
361
390
|
}
|
|
362
391
|
|
|
363
|
-
fn
|
|
392
|
+
fn assert_state_foreign_key_pointers(schema: &JsonValue) -> Result<(), LixError> {
|
|
364
393
|
let Some(foreign_keys) = schema
|
|
365
|
-
.get("x-lix-foreign-keys")
|
|
394
|
+
.get("x-lix-state-foreign-keys")
|
|
366
395
|
.and_then(|value| value.as_array())
|
|
367
396
|
else {
|
|
368
397
|
return Ok(());
|
|
369
398
|
};
|
|
370
399
|
|
|
371
|
-
for foreign_key in foreign_keys {
|
|
372
|
-
let Some(
|
|
373
|
-
.get("references")
|
|
374
|
-
.and_then(|value| value.get("schemaKey"))
|
|
375
|
-
.and_then(|value| value.as_str())
|
|
376
|
-
else {
|
|
400
|
+
for (index, foreign_key) in foreign_keys.iter().enumerate() {
|
|
401
|
+
let Some(local_pointers) = foreign_key.as_array() else {
|
|
377
402
|
continue;
|
|
378
403
|
};
|
|
379
|
-
|
|
380
|
-
let Some(replacement) = preferred_lix_schema_key_alias(schema_key) else {
|
|
404
|
+
if local_pointers.len() != 3 {
|
|
381
405
|
continue;
|
|
382
|
-
}
|
|
406
|
+
}
|
|
383
407
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
),
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
408
|
+
let roles = [
|
|
409
|
+
("entity_id", "a non-empty JSON array of strings"),
|
|
410
|
+
("schema_key", "a string"),
|
|
411
|
+
("file_id", "a string or null"),
|
|
412
|
+
];
|
|
413
|
+
for (slot, (role, expected)) in roles.iter().enumerate() {
|
|
414
|
+
let Some(pointer) = local_pointers[slot].as_str() else {
|
|
415
|
+
continue;
|
|
416
|
+
};
|
|
417
|
+
let segments = parse_json_pointer(pointer)?;
|
|
418
|
+
let Some(property_schema) = (!segments.is_empty())
|
|
419
|
+
.then(|| schema_property(schema, &segments))
|
|
420
|
+
.flatten()
|
|
421
|
+
else {
|
|
422
|
+
return Err(LixError::new(
|
|
423
|
+
LixError::CODE_SCHEMA_DEFINITION,
|
|
424
|
+
format!(
|
|
425
|
+
"Invalid Lix schema definition: x-lix-state-foreign-keys[{index}][{slot}] ({role}) references missing property \"{pointer}\"."
|
|
426
|
+
),
|
|
427
|
+
));
|
|
428
|
+
};
|
|
429
|
+
if !schema_pointer_is_required(schema, &segments) {
|
|
430
|
+
return Err(LixError::new(
|
|
431
|
+
LixError::CODE_SCHEMA_DEFINITION,
|
|
432
|
+
format!(
|
|
433
|
+
"Invalid Lix schema definition: x-lix-state-foreign-keys[{index}][{slot}] ({role}) property \"{pointer}\" must be required. Tuple order is [entity_id, schema_key, file_id]."
|
|
434
|
+
),
|
|
435
|
+
));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let valid = match *role {
|
|
439
|
+
"entity_id" => schema_property_is_string_array(property_schema),
|
|
440
|
+
"schema_key" => schema_property_is_string_only(property_schema),
|
|
441
|
+
"file_id" => schema_property_is_string_or_null(property_schema),
|
|
442
|
+
_ => unreachable!("state foreign key roles are exhaustive"),
|
|
443
|
+
};
|
|
444
|
+
if !valid {
|
|
445
|
+
return Err(LixError::new(
|
|
446
|
+
LixError::CODE_SCHEMA_DEFINITION,
|
|
447
|
+
format!(
|
|
448
|
+
"Invalid Lix schema definition: x-lix-state-foreign-keys[{index}][{slot}] ({role}) property \"{pointer}\" must be {expected}. Tuple order is [entity_id, schema_key, file_id]."
|
|
449
|
+
),
|
|
450
|
+
));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
390
453
|
}
|
|
391
454
|
|
|
392
455
|
Ok(())
|
|
@@ -405,10 +468,10 @@ fn assert_known_x_lix_top_level_fields(schema: &JsonValue) -> Result<(), LixErro
|
|
|
405
468
|
let known = matches!(
|
|
406
469
|
key.as_str(),
|
|
407
470
|
"x-lix-key"
|
|
408
|
-
| "x-lix-version"
|
|
409
471
|
| "x-lix-primary-key"
|
|
410
472
|
| "x-lix-unique"
|
|
411
473
|
| "x-lix-foreign-keys"
|
|
474
|
+
| "x-lix-state-foreign-keys"
|
|
412
475
|
);
|
|
413
476
|
|
|
414
477
|
if !known {
|
|
@@ -427,40 +490,84 @@ fn assert_known_x_lix_top_level_fields(schema: &JsonValue) -> Result<(), LixErro
|
|
|
427
490
|
Ok(())
|
|
428
491
|
}
|
|
429
492
|
|
|
430
|
-
fn
|
|
431
|
-
|
|
432
|
-
"state" => Some("lix_state"),
|
|
433
|
-
"state_by_version" => Some("lix_state_by_version"),
|
|
434
|
-
"state_history" => Some("lix_state_history"),
|
|
435
|
-
"state_history_by_version" => Some("lix_state_history_by_version"),
|
|
436
|
-
"label" => Some("lix_label"),
|
|
437
|
-
"entity_label" => Some("lix_entity_label"),
|
|
438
|
-
"conversation" => Some("lix_conversation"),
|
|
439
|
-
"entity_conversation" => Some("lix_entity_conversation"),
|
|
440
|
-
_ => None,
|
|
441
|
-
}
|
|
493
|
+
fn schema_has_property(schema: &JsonValue, segments: &[String]) -> bool {
|
|
494
|
+
schema_property(schema, segments).is_some()
|
|
442
495
|
}
|
|
443
496
|
|
|
444
|
-
fn
|
|
497
|
+
fn schema_pointer_is_required(schema: &JsonValue, segments: &[String]) -> bool {
|
|
498
|
+
if segments.is_empty() {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
445
502
|
let mut node = schema;
|
|
446
503
|
for segment in segments {
|
|
447
|
-
let
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
504
|
+
let required = node
|
|
505
|
+
.get("required")
|
|
506
|
+
.and_then(JsonValue::as_array)
|
|
507
|
+
.map(|required| {
|
|
508
|
+
required
|
|
509
|
+
.iter()
|
|
510
|
+
.any(|required_property| required_property.as_str() == Some(segment))
|
|
511
|
+
})
|
|
512
|
+
.unwrap_or(false);
|
|
513
|
+
if !required {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let Some(next) = node
|
|
518
|
+
.get("properties")
|
|
519
|
+
.and_then(JsonValue::as_object)
|
|
520
|
+
.and_then(|properties| properties.get(segment))
|
|
521
|
+
else {
|
|
522
|
+
return false;
|
|
458
523
|
};
|
|
459
524
|
node = next;
|
|
460
525
|
}
|
|
526
|
+
|
|
461
527
|
true
|
|
462
528
|
}
|
|
463
529
|
|
|
530
|
+
fn schema_property<'a>(schema: &'a JsonValue, segments: &[String]) -> Option<&'a JsonValue> {
|
|
531
|
+
let mut node = schema;
|
|
532
|
+
for segment in segments {
|
|
533
|
+
let properties = node.get("properties")?.as_object()?;
|
|
534
|
+
let next = properties.get(segment)?;
|
|
535
|
+
node = next;
|
|
536
|
+
}
|
|
537
|
+
Some(node)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
fn schema_property_is_string_only(schema: &JsonValue) -> bool {
|
|
541
|
+
let mut kinds = BTreeSet::new();
|
|
542
|
+
collect_schema_type_kinds(schema, &mut kinds);
|
|
543
|
+
kinds.len() == 1 && kinds.contains("string")
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fn schema_property_is_string_or_null(schema: &JsonValue) -> bool {
|
|
547
|
+
let mut kinds = BTreeSet::new();
|
|
548
|
+
collect_schema_type_kinds(schema, &mut kinds);
|
|
549
|
+
kinds.remove("null");
|
|
550
|
+
kinds.len() == 1 && kinds.contains("string")
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fn schema_property_is_string_array(schema: &JsonValue) -> bool {
|
|
554
|
+
let mut kinds = BTreeSet::new();
|
|
555
|
+
collect_schema_type_kinds(schema, &mut kinds);
|
|
556
|
+
if kinds.len() != 1 || !kinds.contains("array") {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
let Some(items) = schema.get("items") else {
|
|
560
|
+
return false;
|
|
561
|
+
};
|
|
562
|
+
if !schema_property_is_string_only(items) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
schema
|
|
566
|
+
.get("minItems")
|
|
567
|
+
.and_then(JsonValue::as_u64)
|
|
568
|
+
.is_some_and(|min_items| min_items >= 1)
|
|
569
|
+
}
|
|
570
|
+
|
|
464
571
|
pub(crate) fn format_lix_schema_validation_errors<'a>(
|
|
465
572
|
errors: impl Iterator<Item = jsonschema::ValidationError<'a>>,
|
|
466
573
|
) -> String {
|
|
@@ -490,7 +597,6 @@ mod pointer_slash_detection_tests {
|
|
|
490
597
|
let mut obj = json!({
|
|
491
598
|
"type": "object",
|
|
492
599
|
"x-lix-key": "book",
|
|
493
|
-
"x-lix-version": "1",
|
|
494
600
|
"properties": {
|
|
495
601
|
"id": { "type": "string" },
|
|
496
602
|
"author_id": { "type": "string" },
|