Compare commits
6 Commits
87a845e85a
...
1.0.125
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a4c53dc7d | |||
| 69bd726b25 | |||
| c2267b68d8 | |||
| f58d1a32a3 | |||
| d9f4a90225 | |||
| 509194b55f |
@ -234,7 +234,7 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig
|
|||||||
* **Dynamic Filtering**: Binds parameters natively through `cue.filters` objects. The queryer enforces a strict, structured, MongoDB-style operator syntax to map incoming JSON request constraints directly to their originating structural table columns. Filters support both flat path notation (e.g., `"contacts/is_primary": {...}`) and deeply nested recursive JSON structures (e.g., `{"contacts": {"is_primary": {...}}}`). The queryer recursively traverses and flattens these structures at AST compilation time.
|
* **Dynamic Filtering**: Binds parameters natively through `cue.filters` objects. The queryer enforces a strict, structured, MongoDB-style operator syntax to map incoming JSON request constraints directly to their originating structural table columns. Filters support both flat path notation (e.g., `"contacts/is_primary": {...}`) and deeply nested recursive JSON structures (e.g., `{"contacts": {"is_primary": {...}}}`). The queryer recursively traverses and flattens these structures at AST compilation time.
|
||||||
* **Equality / Inequality**: `{"$eq": value}`, `{"$ne": value}` automatically map to `=` and `!=`.
|
* **Equality / Inequality**: `{"$eq": value}`, `{"$ne": value}` automatically map to `=` and `!=`.
|
||||||
* **Comparison**: `{"$gt": ...}`, `{"$gte": ...}`, `{"$lt": ...}`, `{"$lte": ...}` directly compile to Postgres comparison operators (`> `, `>=`, `<`, `<=`).
|
* **Comparison**: `{"$gt": ...}`, `{"$gte": ...}`, `{"$lt": ...}`, `{"$lte": ...}` directly compile to Postgres comparison operators (`> `, `>=`, `<`, `<=`).
|
||||||
* **Array Inclusion**: `{"$in": [values]}`, `{"$nin": [values]}` use native `jsonb_array_elements_text()` bindings to enforce `IN` and `NOT IN` logic without runtime SQL injection risks.
|
* **Array Inclusion**: `{"$of": [values]}`, `{"$nof": [values]}` use native `jsonb_array_elements_text()` bindings to enforce `IN` and `NOT IN` logic without runtime SQL injection risks.
|
||||||
* **Text Matching (ILIKE)**: Evaluates `$eq` or `$ne` against string fields containing the `%` character natively into Postgres `ILIKE` and `NOT ILIKE` partial substring matches.
|
* **Text Matching (ILIKE)**: Evaluates `$eq` or `$ne` against string fields containing the `%` character natively into Postgres `ILIKE` and `NOT ILIKE` partial substring matches.
|
||||||
* **Type Casting**: Safely resolves dynamic combinations by casting values instantly into the physical database types mapped in the schema (e.g. parsing `uuid` bindings to `::uuid`, formatting DateTimes to `::timestamptz`, and numbers to `::numeric`).
|
* **Type Casting**: Safely resolves dynamic combinations by casting values instantly into the physical database types mapped in the schema (e.g. parsing `uuid` bindings to `::uuid`, formatting DateTimes to `::timestamptz`, and numbers to `::numeric`).
|
||||||
* **Polymorphic SQL Generation (`family`)**: Compiles `family` properties by analyzing the **Physical Database Variations**, *not* the schema descendants.
|
* **Polymorphic SQL Generation (`family`)**: Compiles `family` properties by analyzing the **Physical Database Variations**, *not* the schema descendants.
|
||||||
|
|||||||
@ -94,19 +94,30 @@
|
|||||||
{
|
{
|
||||||
"id": "type3",
|
"id": "type3",
|
||||||
"type": "type",
|
"type": "type",
|
||||||
"name": "filter",
|
"name": "search",
|
||||||
"module": "core",
|
"module": "core",
|
||||||
"source": "filter",
|
"source": "search",
|
||||||
"hierarchy": [
|
"hierarchy": [
|
||||||
"filter"
|
"search"
|
||||||
],
|
],
|
||||||
"variations": [
|
"variations": [
|
||||||
"filter",
|
"search"
|
||||||
"string.condition",
|
|
||||||
"integer.condition",
|
|
||||||
"date.condition"
|
|
||||||
],
|
],
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"search": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"type": "filter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"condition": {
|
"condition": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -161,7 +172,7 @@
|
|||||||
"schemas": {
|
"schemas": {
|
||||||
"person": {},
|
"person": {},
|
||||||
"person.filter": {
|
"person.filter": {
|
||||||
"type": "object",
|
"type": "filter",
|
||||||
"compiledPropertyNames": [
|
"compiledPropertyNames": [
|
||||||
"age",
|
"age",
|
||||||
"billing_address",
|
"billing_address",
|
||||||
@ -197,7 +208,7 @@
|
|||||||
},
|
},
|
||||||
"address": {},
|
"address": {},
|
||||||
"address.filter": {
|
"address.filter": {
|
||||||
"type": "object",
|
"type": "filter",
|
||||||
"compiledPropertyNames": [
|
"compiledPropertyNames": [
|
||||||
"city"
|
"city"
|
||||||
],
|
],
|
||||||
@ -210,10 +221,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"filter": {},
|
||||||
"condition": {},
|
"condition": {},
|
||||||
"string.condition": {},
|
"string.condition": {},
|
||||||
"integer.condition": {},
|
"integer.condition": {},
|
||||||
"date.condition": {}
|
"date.condition": {},
|
||||||
|
"search": {},
|
||||||
|
"search.filter": {
|
||||||
|
"type": "filter",
|
||||||
|
"compiledPropertyNames": [
|
||||||
|
"filter",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"filter": {
|
||||||
|
"type": [
|
||||||
|
"filter.filter",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": [
|
||||||
|
"string.condition",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1195,14 +1195,14 @@
|
|||||||
"description": "Simple entity select with multiple filters",
|
"description": "Simple entity select with multiple filters",
|
||||||
"action": "query",
|
"action": "query",
|
||||||
"schema_id": "entity",
|
"schema_id": "entity",
|
||||||
"filters": {
|
"filter": {
|
||||||
"id": {
|
"id": {
|
||||||
"$eq": "123e4567-e89b-12d3-a456-426614174000",
|
"$eq": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
"$ne": "123e4567-e89b-12d3-a456-426614174001",
|
"$ne": "123e4567-e89b-12d3-a456-426614174001",
|
||||||
"$in": [
|
"$of": [
|
||||||
"123e4567-e89b-12d3-a456-426614174000"
|
"123e4567-e89b-12d3-a456-426614174000"
|
||||||
],
|
],
|
||||||
"$nin": [
|
"$nof": [
|
||||||
"123e4567-e89b-12d3-a456-426614174001"
|
"123e4567-e89b-12d3-a456-426614174001"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1241,9 +1241,9 @@
|
|||||||
" AND entity_1.created_at <= ($7#>>'{}')::timestamptz",
|
" AND entity_1.created_at <= ($7#>>'{}')::timestamptz",
|
||||||
" AND entity_1.created_at != ($8#>>'{}')::timestamptz",
|
" AND entity_1.created_at != ($8#>>'{}')::timestamptz",
|
||||||
" AND entity_1.id = ($9#>>'{}')::uuid",
|
" AND entity_1.id = ($9#>>'{}')::uuid",
|
||||||
" AND entity_1.id IN (SELECT value::uuid FROM jsonb_array_elements_text(($10#>>'{}')::jsonb))",
|
" AND entity_1.id != ($10#>>'{}')::uuid",
|
||||||
" AND entity_1.id != ($11#>>'{}')::uuid",
|
" AND entity_1.id NOT IN (SELECT value::uuid FROM jsonb_array_elements_text(($11#>>'{}')::jsonb))",
|
||||||
" AND entity_1.id NOT IN (SELECT value::uuid FROM jsonb_array_elements_text(($12#>>'{}')::jsonb))",
|
" AND entity_1.id IN (SELECT value::uuid FROM jsonb_array_elements_text(($12#>>'{}')::jsonb))",
|
||||||
")))"
|
")))"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -1443,19 +1443,19 @@
|
|||||||
"description": "Person select on full schema with filters",
|
"description": "Person select on full schema with filters",
|
||||||
"action": "query",
|
"action": "query",
|
||||||
"schema_id": "full.person",
|
"schema_id": "full.person",
|
||||||
"filters": {
|
"filter": {
|
||||||
"age": {
|
"age": {
|
||||||
"$eq": 30,
|
"$eq": 30,
|
||||||
"$gt": 20,
|
"$gt": 20,
|
||||||
"$gte": 20,
|
"$gte": 20,
|
||||||
"$in": [
|
"$of": [
|
||||||
30,
|
30,
|
||||||
40
|
40
|
||||||
],
|
],
|
||||||
"$lt": 50,
|
"$lt": 50,
|
||||||
"$lte": 50,
|
"$lte": 50,
|
||||||
"$ne": 25,
|
"$ne": 25,
|
||||||
"$nin": [
|
"$nof": [
|
||||||
1,
|
1,
|
||||||
2
|
2
|
||||||
]
|
]
|
||||||
@ -1481,24 +1481,24 @@
|
|||||||
"$eq": "Jane%",
|
"$eq": "Jane%",
|
||||||
"$gt": "A",
|
"$gt": "A",
|
||||||
"$gte": "A",
|
"$gte": "A",
|
||||||
"$in": [
|
"$of": [
|
||||||
"Jane",
|
"Jane",
|
||||||
"John"
|
"John"
|
||||||
],
|
],
|
||||||
"$lt": "Z",
|
"$lt": "Z",
|
||||||
"$lte": "Z",
|
"$lte": "Z",
|
||||||
"$ne": "Doe",
|
"$ne": "Doe",
|
||||||
"$nin": [
|
"$nof": [
|
||||||
"Bob"
|
"Bob"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"$eq": "00000000-0000-0000-0000-000000000001",
|
"$eq": "00000000-0000-0000-0000-000000000001",
|
||||||
"$in": [
|
"$of": [
|
||||||
"00000000-0000-0000-0000-000000000001"
|
"00000000-0000-0000-0000-000000000001"
|
||||||
],
|
],
|
||||||
"$ne": "00000000-0000-0000-0000-000000000002",
|
"$ne": "00000000-0000-0000-0000-000000000002",
|
||||||
"$nin": [
|
"$nof": [
|
||||||
"00000000-0000-0000-0000-000000000002"
|
"00000000-0000-0000-0000-000000000002"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -1677,11 +1677,11 @@
|
|||||||
" AND person_1.age = ($1#>>'{}')::numeric",
|
" AND person_1.age = ($1#>>'{}')::numeric",
|
||||||
" AND person_1.age > ($2#>>'{}')::numeric",
|
" AND person_1.age > ($2#>>'{}')::numeric",
|
||||||
" AND person_1.age >= ($3#>>'{}')::numeric",
|
" AND person_1.age >= ($3#>>'{}')::numeric",
|
||||||
" AND person_1.age IN (SELECT value::numeric FROM jsonb_array_elements_text(($4#>>'{}')::jsonb))",
|
" AND person_1.age < ($4#>>'{}')::numeric",
|
||||||
" AND person_1.age < ($5#>>'{}')::numeric",
|
" AND person_1.age <= ($5#>>'{}')::numeric",
|
||||||
" AND person_1.age <= ($6#>>'{}')::numeric",
|
" AND person_1.age != ($6#>>'{}')::numeric",
|
||||||
" AND person_1.age != ($7#>>'{}')::numeric",
|
" AND person_1.age NOT IN (SELECT value::numeric FROM jsonb_array_elements_text(($7#>>'{}')::jsonb))",
|
||||||
" AND person_1.age NOT IN (SELECT value::numeric FROM jsonb_array_elements_text(($8#>>'{}')::jsonb))",
|
" AND person_1.age IN (SELECT value::numeric FROM jsonb_array_elements_text(($8#>>'{}')::jsonb))",
|
||||||
" AND entity_3.archived = ($9#>>'{}')::boolean",
|
" AND entity_3.archived = ($9#>>'{}')::boolean",
|
||||||
" AND entity_3.archived != ($10#>>'{}')::boolean",
|
" AND entity_3.archived != ($10#>>'{}')::boolean",
|
||||||
" AND entity_3.created_at = ($12#>>'{}')::timestamptz",
|
" AND entity_3.created_at = ($12#>>'{}')::timestamptz",
|
||||||
@ -1693,15 +1693,15 @@
|
|||||||
" AND person_1.first_name ILIKE $18#>>'{}'",
|
" AND person_1.first_name ILIKE $18#>>'{}'",
|
||||||
" AND person_1.first_name > ($19#>>'{}')",
|
" AND person_1.first_name > ($19#>>'{}')",
|
||||||
" AND person_1.first_name >= ($20#>>'{}')",
|
" AND person_1.first_name >= ($20#>>'{}')",
|
||||||
" AND person_1.first_name IN (SELECT value FROM jsonb_array_elements_text(($21#>>'{}')::jsonb))",
|
" AND person_1.first_name < ($21#>>'{}')",
|
||||||
" AND person_1.first_name < ($22#>>'{}')",
|
" AND person_1.first_name <= ($22#>>'{}')",
|
||||||
" AND person_1.first_name <= ($23#>>'{}')",
|
" AND person_1.first_name NOT ILIKE $23#>>'{}'",
|
||||||
" AND person_1.first_name NOT ILIKE $24#>>'{}'",
|
" AND person_1.first_name NOT IN (SELECT value FROM jsonb_array_elements_text(($24#>>'{}')::jsonb))",
|
||||||
" AND person_1.first_name NOT IN (SELECT value FROM jsonb_array_elements_text(($25#>>'{}')::jsonb))",
|
" AND person_1.first_name IN (SELECT value FROM jsonb_array_elements_text(($25#>>'{}')::jsonb))",
|
||||||
" AND entity_3.id = ($26#>>'{}')::uuid",
|
" AND entity_3.id = ($26#>>'{}')::uuid",
|
||||||
" AND entity_3.id IN (SELECT value::uuid FROM jsonb_array_elements_text(($27#>>'{}')::jsonb))",
|
" AND entity_3.id != ($27#>>'{}')::uuid",
|
||||||
" AND entity_3.id != ($28#>>'{}')::uuid",
|
" AND entity_3.id NOT IN (SELECT value::uuid FROM jsonb_array_elements_text(($28#>>'{}')::jsonb))",
|
||||||
" AND entity_3.id NOT IN (SELECT value::uuid FROM jsonb_array_elements_text(($29#>>'{}')::jsonb))",
|
" AND entity_3.id IN (SELECT value::uuid FROM jsonb_array_elements_text(($29#>>'{}')::jsonb))",
|
||||||
" AND person_1.last_name ILIKE $30#>>'{}'",
|
" AND person_1.last_name ILIKE $30#>>'{}'",
|
||||||
" AND person_1.last_name NOT ILIKE $31#>>'{}')))"
|
" AND person_1.last_name NOT ILIKE $31#>>'{}')))"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
use crate::database::Database;
|
||||||
use crate::database::object::{SchemaObject, SchemaTypeOrArray};
|
use crate::database::object::{SchemaObject, SchemaTypeOrArray};
|
||||||
use crate::database::schema::Schema;
|
use crate::database::schema::Schema;
|
||||||
use crate::database::Database;
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -20,16 +20,26 @@ impl Schema {
|
|||||||
let mut child_obj = SchemaObject::default();
|
let mut child_obj = SchemaObject::default();
|
||||||
child_obj.type_ = Some(SchemaTypeOrArray::Multiple(filter_type));
|
child_obj.type_ = Some(SchemaTypeOrArray::Multiple(filter_type));
|
||||||
|
|
||||||
filter_props.insert(key.clone(), Arc::new(Schema { obj: child_obj, always_fail: false }));
|
filter_props.insert(
|
||||||
|
key.clone(),
|
||||||
|
Arc::new(Schema {
|
||||||
|
obj: child_obj,
|
||||||
|
always_fail: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filter_props.is_empty() {
|
if !filter_props.is_empty() {
|
||||||
let mut wrapper_obj = SchemaObject::default();
|
let mut wrapper_obj = SchemaObject::default();
|
||||||
wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("object".to_string()));
|
// Conceptually link this directly into the STI lineage of the base `filter` object
|
||||||
|
wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("filter".to_string()));
|
||||||
wrapper_obj.properties = Some(filter_props);
|
wrapper_obj.properties = Some(filter_props);
|
||||||
|
|
||||||
return Some(Schema { obj: wrapper_obj, always_fail: false });
|
return Some(Schema {
|
||||||
|
obj: wrapper_obj,
|
||||||
|
always_fail: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -1,6 +1,6 @@
|
|||||||
pub mod collection;
|
pub mod collection;
|
||||||
pub mod edges;
|
pub mod edges;
|
||||||
pub mod filters;
|
pub mod filter;
|
||||||
pub mod polymorphism;
|
pub mod polymorphism;
|
||||||
|
|
||||||
use crate::database::schema::Schema;
|
use crate::database::schema::Schema;
|
||||||
|
|||||||
@ -29,12 +29,15 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use r#type::Type;
|
use r#type::Type;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub enums: HashMap<String, Enum>,
|
pub enums: HashMap<String, Enum>,
|
||||||
pub types: HashMap<String, Type>,
|
pub types: HashMap<String, Type>,
|
||||||
pub puncs: HashMap<String, Punc>,
|
pub puncs: HashMap<String, Punc>,
|
||||||
pub relations: HashMap<String, Relation>,
|
pub relations: HashMap<String, Relation>,
|
||||||
|
#[serde(skip)]
|
||||||
pub schemas: HashMap<String, Arc<Schema>>,
|
pub schemas: HashMap<String, Arc<Schema>>,
|
||||||
|
#[serde(skip)]
|
||||||
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,23 +241,30 @@ impl Database {
|
|||||||
|
|
||||||
// Phase 2: Synthesize Composed Filter References
|
// Phase 2: Synthesize Composed Filter References
|
||||||
let mut filter_schemas = Vec::new();
|
let mut filter_schemas = Vec::new();
|
||||||
for type_def in self.types.values() {
|
for (type_name, type_def) in &self.types {
|
||||||
for (id, schema_arc) in &type_def.schemas {
|
for (id, schema_arc) in &type_def.schemas {
|
||||||
// Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas!
|
// Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas!
|
||||||
let base_name = id.split('.').last().unwrap_or(id);
|
let base_name = id.split('.').last().unwrap_or(id);
|
||||||
let is_table_backed = base_name == type_def.name;
|
let is_table_backed = base_name == type_def.name;
|
||||||
if is_table_backed && !id.contains('/') {
|
if is_table_backed && !id.contains('/') {
|
||||||
if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) {
|
if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) {
|
||||||
filter_schemas.push((format!("{}.filter", id), Arc::new(filter_schema)));
|
filter_schemas.push((
|
||||||
|
type_name.clone(),
|
||||||
|
format!("{}.filter", id),
|
||||||
|
Arc::new(filter_schema),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut filter_ids = Vec::new();
|
let mut filter_ids = Vec::new();
|
||||||
for (id, filter_arc) in filter_schemas {
|
for (type_name, id, filter_arc) in filter_schemas {
|
||||||
filter_ids.push(id.clone());
|
filter_ids.push(id.clone());
|
||||||
self.schemas.insert(id, filter_arc);
|
self.schemas.insert(id.clone(), filter_arc.clone());
|
||||||
|
if let Some(t) = self.types.get_mut(&type_name) {
|
||||||
|
t.schemas.insert(id, filter_arc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now actively compile the newly injected filters to lock all nested compose references natively
|
// Now actively compile the newly injected filters to lock all nested compose references natively
|
||||||
@ -269,50 +279,88 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_schemas(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
fn collect_schemas(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
||||||
let mut to_insert = Vec::new();
|
let mut type_insert = Vec::new();
|
||||||
|
let mut punc_insert = Vec::new();
|
||||||
|
let mut enum_insert = Vec::new();
|
||||||
|
let mut global_insert = Vec::new();
|
||||||
|
|
||||||
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
|
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
|
||||||
// Validate every node recursively via string filters natively!
|
// Validate every node recursively via string filters natively!
|
||||||
for type_def in self.types.values() {
|
for (type_name, type_def) in &self.types {
|
||||||
for (id, schema_arc) in &type_def.schemas {
|
for (id, schema_arc) in &type_def.schemas {
|
||||||
to_insert.push((id.clone(), Arc::clone(schema_arc)));
|
global_insert.push((id.clone(), Arc::clone(schema_arc)));
|
||||||
|
let mut local_insert = Vec::new();
|
||||||
crate::database::schema::Schema::collect_schemas(
|
crate::database::schema::Schema::collect_schemas(
|
||||||
schema_arc,
|
schema_arc,
|
||||||
id,
|
id,
|
||||||
id.clone(),
|
id.clone(),
|
||||||
&mut to_insert,
|
&mut local_insert,
|
||||||
errors,
|
errors,
|
||||||
);
|
);
|
||||||
|
for entry in &local_insert {
|
||||||
|
type_insert.push((type_name.clone(), entry.0.clone(), Arc::clone(&entry.1)));
|
||||||
|
global_insert.push((entry.0.clone(), Arc::clone(&entry.1)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for punc_def in self.puncs.values() {
|
|
||||||
for (id, schema_arc) in &punc_def.schemas {
|
|
||||||
to_insert.push((id.clone(), Arc::clone(schema_arc)));
|
|
||||||
crate::database::schema::Schema::collect_schemas(
|
|
||||||
schema_arc,
|
|
||||||
id,
|
|
||||||
id.clone(),
|
|
||||||
&mut to_insert,
|
|
||||||
errors,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for enum_def in self.enums.values() {
|
|
||||||
for (id, schema_arc) in &enum_def.schemas {
|
|
||||||
to_insert.push((id.clone(), Arc::clone(schema_arc)));
|
|
||||||
crate::database::schema::Schema::collect_schemas(
|
|
||||||
schema_arc,
|
|
||||||
id,
|
|
||||||
id.clone(),
|
|
||||||
&mut to_insert,
|
|
||||||
errors,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (id, schema_arc) in to_insert {
|
for (punc_name, punc_def) in &self.puncs {
|
||||||
|
for (id, schema_arc) in &punc_def.schemas {
|
||||||
|
global_insert.push((id.clone(), Arc::clone(schema_arc)));
|
||||||
|
let mut local_insert = Vec::new();
|
||||||
|
crate::database::schema::Schema::collect_schemas(
|
||||||
|
schema_arc,
|
||||||
|
id,
|
||||||
|
id.clone(),
|
||||||
|
&mut local_insert,
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
for entry in &local_insert {
|
||||||
|
punc_insert.push((punc_name.clone(), entry.0.clone(), Arc::clone(&entry.1)));
|
||||||
|
global_insert.push((entry.0.clone(), Arc::clone(&entry.1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (enum_name, enum_def) in &self.enums {
|
||||||
|
for (id, schema_arc) in &enum_def.schemas {
|
||||||
|
global_insert.push((id.clone(), Arc::clone(schema_arc)));
|
||||||
|
let mut local_insert = Vec::new();
|
||||||
|
crate::database::schema::Schema::collect_schemas(
|
||||||
|
schema_arc,
|
||||||
|
id,
|
||||||
|
id.clone(),
|
||||||
|
&mut local_insert,
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
for entry in &local_insert {
|
||||||
|
enum_insert.push((enum_name.clone(), entry.0.clone(), Arc::clone(&entry.1)));
|
||||||
|
global_insert.push((entry.0.clone(), Arc::clone(&entry.1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply global inserts
|
||||||
|
for (id, schema_arc) in global_insert {
|
||||||
self.schemas.insert(id, schema_arc);
|
self.schemas.insert(id, schema_arc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply local scopes
|
||||||
|
for (origin_name, id, schema_arc) in type_insert {
|
||||||
|
if let Some(t) = self.types.get_mut(&origin_name) {
|
||||||
|
t.schemas.insert(id, schema_arc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (origin_name, id, schema_arc) in punc_insert {
|
||||||
|
if let Some(p) = self.puncs.get_mut(&origin_name) {
|
||||||
|
p.schemas.insert(id, schema_arc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (origin_name, id, schema_arc) in enum_insert {
|
||||||
|
if let Some(e) = self.enums.get_mut(&origin_name) {
|
||||||
|
e.schemas.insert(id, schema_arc);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inspects the Postgres pg_constraint relations catalog to securely identify
|
/// Inspects the Postgres pg_constraint relations catalog to securely identify
|
||||||
|
|||||||
@ -15,6 +15,7 @@ pub struct Punc {
|
|||||||
pub public: bool,
|
pub public: bool,
|
||||||
pub form: bool,
|
pub form: bool,
|
||||||
pub get: Option<String>,
|
pub get: Option<String>,
|
||||||
|
pub save: Option<String>,
|
||||||
pub page: Option<Page>,
|
pub page: Option<Page>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
|
pub schemas: std::collections::BTreeMap<String, Arc<Schema>>,
|
||||||
|
|||||||
@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Relation {
|
pub struct Relation {
|
||||||
|
pub id: String,
|
||||||
|
pub r#type: String,
|
||||||
pub constraint: String,
|
pub constraint: String,
|
||||||
pub source_type: String,
|
pub source_type: String,
|
||||||
pub source_columns: Vec<String>,
|
pub source_columns: Vec<String>,
|
||||||
|
|||||||
10
src/lib.rs
10
src/lib.rs
@ -72,7 +72,7 @@ pub fn jspg_merge(schema_id: &str, data: JsonB) -> JsonB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(test), pg_extern)]
|
#[cfg_attr(not(test), pg_extern)]
|
||||||
pub fn jspg_query(schema_id: &str, filters: Option<JsonB>) -> JsonB {
|
pub fn jspg_query(schema_id: &str, filter: Option<JsonB>) -> JsonB {
|
||||||
let engine_opt = {
|
let engine_opt = {
|
||||||
let lock = GLOBAL_JSPG.read().unwrap();
|
let lock = GLOBAL_JSPG.read().unwrap();
|
||||||
lock.clone()
|
lock.clone()
|
||||||
@ -82,7 +82,7 @@ pub fn jspg_query(schema_id: &str, filters: Option<JsonB>) -> JsonB {
|
|||||||
Some(engine) => {
|
Some(engine) => {
|
||||||
let drop = engine
|
let drop = engine
|
||||||
.queryer
|
.queryer
|
||||||
.query(schema_id, filters.as_ref().map(|f| &f.0));
|
.query(schema_id, filter.as_ref().map(|f| &f.0));
|
||||||
JsonB(serde_json::to_value(drop).unwrap())
|
JsonB(serde_json::to_value(drop).unwrap())
|
||||||
}
|
}
|
||||||
None => jspg_failure(),
|
None => jspg_failure(),
|
||||||
@ -109,7 +109,7 @@ pub fn jspg_validate(schema_id: &str, instance: JsonB) -> JsonB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(test), pg_extern)]
|
#[cfg_attr(not(test), pg_extern)]
|
||||||
pub fn jspg_schemas() -> JsonB {
|
pub fn jspg_database() -> JsonB {
|
||||||
let engine_opt = {
|
let engine_opt = {
|
||||||
let lock = GLOBAL_JSPG.read().unwrap();
|
let lock = GLOBAL_JSPG.read().unwrap();
|
||||||
lock.clone()
|
lock.clone()
|
||||||
@ -117,9 +117,9 @@ pub fn jspg_schemas() -> JsonB {
|
|||||||
|
|
||||||
match engine_opt {
|
match engine_opt {
|
||||||
Some(engine) => {
|
Some(engine) => {
|
||||||
let schemas_json = serde_json::to_value(&engine.database.schemas)
|
let database_json = serde_json::to_value(&engine.database)
|
||||||
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
|
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
|
||||||
let drop = crate::drop::Drop::success_with_val(schemas_json);
|
let drop = crate::drop::Drop::success_with_val(database_json);
|
||||||
JsonB(serde_json::to_value(drop).unwrap())
|
JsonB(serde_json::to_value(drop).unwrap())
|
||||||
}
|
}
|
||||||
None => jspg_failure(),
|
None => jspg_failure(),
|
||||||
|
|||||||
@ -717,8 +717,8 @@ impl<'a> Compiler<'a> {
|
|||||||
let param_index = i + 1;
|
let param_index = i + 1;
|
||||||
let p_val = format!("${}#>>'{{}}'", param_index);
|
let p_val = format!("${}#>>'{{}}'", param_index);
|
||||||
|
|
||||||
if op == "$in" || op == "$nin" {
|
if op == "$of" || op == "$nof" {
|
||||||
let sql_op = if op == "$in" { "IN" } else { "NOT IN" };
|
let sql_op = if op == "$of" { "IN" } else { "NOT IN" };
|
||||||
let subquery = format!(
|
let subquery = format!(
|
||||||
"(SELECT value{} FROM jsonb_array_elements_text(({})::jsonb))",
|
"(SELECT value{} FROM jsonb_array_elements_text(({})::jsonb))",
|
||||||
cast, p_val
|
cast, p_val
|
||||||
|
|||||||
@ -21,9 +21,9 @@ impl Queryer {
|
|||||||
pub fn query(
|
pub fn query(
|
||||||
&self,
|
&self,
|
||||||
schema_id: &str,
|
schema_id: &str,
|
||||||
filters: Option<&serde_json::Value>,
|
filter: Option<&serde_json::Value>,
|
||||||
) -> crate::drop::Drop {
|
) -> crate::drop::Drop {
|
||||||
let filters_map = filters.and_then(|f| f.as_object());
|
let filters_map = filter.and_then(|f| f.as_object());
|
||||||
|
|
||||||
// 1. Process filters into structured $op keys and linear values
|
// 1. Process filters into structured $op keys and linear values
|
||||||
let (filter_keys, args) = match self.parse_filter_entries(filters_map) {
|
let (filter_keys, args) = match self.parse_filter_entries(filters_map) {
|
||||||
@ -35,7 +35,7 @@ impl Queryer {
|
|||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: None, // filters apply to the root query
|
path: None, // filters apply to the root query
|
||||||
cause: Some(msg),
|
cause: Some(msg),
|
||||||
context: filters.cloned(),
|
context: filter.cloned(),
|
||||||
schema: Some(schema_id.to_string()),
|
schema: Some(schema_id.to_string()),
|
||||||
},
|
},
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
106
src/tests/mod.rs
106
src/tests/mod.rs
@ -81,54 +81,116 @@ fn test_library_api() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Validate jspg_schemas
|
// 3. Validate jspg_database mapping natively!
|
||||||
let schemas_drop = jspg_schemas();
|
let db_drop = jspg_database();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
schemas_drop.0,
|
db_drop.0,
|
||||||
json!({
|
json!({
|
||||||
"type": "drop",
|
"type": "drop",
|
||||||
"response": {
|
"response": {
|
||||||
"source_schema": {
|
"enums": {},
|
||||||
"type": "object",
|
"puncs": {},
|
||||||
"properties": {
|
"relations": {
|
||||||
"type": { "type": "string" },
|
"fk_test_target": {
|
||||||
"name": { "type": "string" },
|
"id": "11111111-1111-1111-1111-111111111111",
|
||||||
"target": {
|
"type": "relation",
|
||||||
"type": "target_schema",
|
"constraint": "fk_test_target",
|
||||||
"compiledPropertyNames": ["value"]
|
"destination_columns": ["id"],
|
||||||
|
"destination_type": "target_schema",
|
||||||
|
"prefix": "target",
|
||||||
|
"source_columns": ["target_id"],
|
||||||
|
"source_type": "source_schema"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"types": {
|
||||||
"compiledPropertyNames": ["name", "target", "type"],
|
"source_schema": {
|
||||||
|
"default_fields": [],
|
||||||
|
"field_types": null,
|
||||||
|
"fields": [],
|
||||||
|
"grouped_fields": null,
|
||||||
|
"hierarchy": ["source_schema", "entity"],
|
||||||
|
"historical": false,
|
||||||
|
"id": "",
|
||||||
|
"longevity": null,
|
||||||
|
"lookup_fields": [],
|
||||||
|
"module": "",
|
||||||
|
"name": "source_schema",
|
||||||
|
"notify": false,
|
||||||
|
"null_fields": [],
|
||||||
|
"ownable": false,
|
||||||
|
"relationship": false,
|
||||||
|
"schemas": {
|
||||||
|
"source_schema": {
|
||||||
"compiledEdges": {
|
"compiledEdges": {
|
||||||
"target": {
|
"target": {
|
||||||
"constraint": "fk_test_target",
|
"constraint": "fk_test_target",
|
||||||
"forward": true
|
"forward": true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"compiledPropertyNames": ["name", "target", "type"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"target": {
|
||||||
|
"compiledPropertyNames": ["value"],
|
||||||
|
"type": "target_schema"
|
||||||
|
},
|
||||||
|
"type": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["name"],
|
||||||
|
"type": "object"
|
||||||
},
|
},
|
||||||
"source_schema.filter": {
|
"source_schema.filter": {
|
||||||
"type": "object",
|
"compiledPropertyNames": ["name", "target", "type"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": { "type": ["string.condition", "null"] },
|
|
||||||
"name": { "type": ["string.condition", "null"] },
|
"name": { "type": ["string.condition", "null"] },
|
||||||
"target": { "type": ["target_schema.filter", "null"] }
|
"target": { "type": ["target_schema.filter", "null"] },
|
||||||
|
"type": { "type": ["string.condition", "null"] }
|
||||||
},
|
},
|
||||||
"compiledPropertyNames": ["name", "target", "type"]
|
"type": "filter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensitive": false,
|
||||||
|
"source": "",
|
||||||
|
"type": "",
|
||||||
|
"variations": ["source_schema"]
|
||||||
},
|
},
|
||||||
"target_schema": {
|
"target_schema": {
|
||||||
"type": "object",
|
"default_fields": [],
|
||||||
|
"field_types": null,
|
||||||
|
"fields": [],
|
||||||
|
"grouped_fields": null,
|
||||||
|
"hierarchy": ["target_schema", "entity"],
|
||||||
|
"historical": false,
|
||||||
|
"id": "",
|
||||||
|
"longevity": null,
|
||||||
|
"lookup_fields": [],
|
||||||
|
"module": "",
|
||||||
|
"name": "target_schema",
|
||||||
|
"notify": false,
|
||||||
|
"null_fields": [],
|
||||||
|
"ownable": false,
|
||||||
|
"relationship": false,
|
||||||
|
"schemas": {
|
||||||
|
"target_schema": {
|
||||||
|
"compiledPropertyNames": ["value"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"value": { "type": "number" }
|
"value": { "type": "number" }
|
||||||
},
|
},
|
||||||
"compiledPropertyNames": ["value"]
|
"type": "object"
|
||||||
},
|
},
|
||||||
"target_schema.filter": {
|
"target_schema.filter": {
|
||||||
"type": "object",
|
"compiledPropertyNames": ["value"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"value": { "type": ["number.condition", "null"] }
|
"value": { "type": ["number.condition", "null"] }
|
||||||
},
|
},
|
||||||
"compiledPropertyNames": ["value"]
|
"type": "filter"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensitive": false,
|
||||||
|
"source": "",
|
||||||
|
"type": "",
|
||||||
|
"variations": ["target_schema"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -17,7 +17,7 @@ pub struct Case {
|
|||||||
|
|
||||||
// For Query
|
// For Query
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub filters: Option<serde_json::Value>,
|
pub filter: Option<serde_json::Value>,
|
||||||
|
|
||||||
// For Merge & Validate
|
// For Merge & Validate
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -122,7 +122,7 @@ impl Case {
|
|||||||
use crate::queryer::Queryer;
|
use crate::queryer::Queryer;
|
||||||
let queryer = Queryer::new(db.clone());
|
let queryer = Queryer::new(db.clone());
|
||||||
|
|
||||||
let result = queryer.query(&self.schema_id, self.filters.as_ref());
|
let result = queryer.query(&self.schema_id, self.filter.as_ref());
|
||||||
|
|
||||||
let return_val = if let Some(expect) = &self.expect {
|
let return_val = if let Some(expect) = &self.expect {
|
||||||
if let Err(e) = expect.assert_drop(&result) {
|
if let Err(e) = expect.assert_drop(&result) {
|
||||||
|
|||||||
Reference in New Issue
Block a user