···153153 where_input = where_input.field(InputValue::new(&field.name, TypeRef::named(filter_type)));
154154 }
155155156156+ // Add nested and/or support
157157+ where_input = where_input
158158+ .field(InputValue::new("and", TypeRef::named_list(format!("{}WhereInput", type_name))))
159159+ .field(InputValue::new("or", TypeRef::named_list(format!("{}WhereInput", type_name))));
160160+156161 // Create GroupByField enum for this collection
157162 let mut group_by_enum = Enum::new(format!("{}GroupByField", type_name));
158163 group_by_enum = group_by_enum.item(EnumItem::new("indexedAt"));
···236241 let mut where_clause = crate::models::WhereClause {
237242 conditions: HashMap::new(),
238243 or_conditions: None,
244244+ and: None,
245245+ or: None,
239246 };
240247241248 // Always filter by collection
···254261255262 // Parse where argument if provided
256263 if let Some(where_val) = ctx.args.get("where") {
257257- // Try to parse as JSON object
258264 if let Ok(where_obj) = where_val.object() {
259259- for (field_name, condition_val) in where_obj.iter() {
260260- if let Ok(condition_obj) = condition_val.object() {
261261- let mut where_condition = crate::models::WhereCondition {
262262- gt: None,
263263- gte: None,
264264- lt: None,
265265- lte: None,
266266- eq: None,
267267- in_values: None,
268268- contains: None,
269269- };
270270-271271- // Parse eq condition
272272- if let Some(eq_val) = condition_obj.get("eq") {
273273- if let Ok(eq_str) = eq_val.string() {
274274- where_condition.eq = Some(serde_json::Value::String(eq_str.to_string()));
275275- } else if let Ok(eq_i64) = eq_val.i64() {
276276- where_condition.eq = Some(serde_json::Value::Number(eq_i64.into()));
277277- }
278278- }
279279-280280- // Parse in condition
281281- if let Some(in_val) = condition_obj.get("in") {
282282- if let Ok(in_list) = in_val.list() {
283283- let mut values = Vec::new();
284284- for item in in_list.iter() {
285285- if let Ok(s) = item.string() {
286286- values.push(serde_json::Value::String(s.to_string()));
287287- } else if let Ok(i) = item.i64() {
288288- values.push(serde_json::Value::Number(i.into()));
289289- }
290290- }
291291- where_condition.in_values = Some(values);
292292- }
293293- }
294294-295295- // Parse contains condition
296296- if let Some(contains_val) = condition_obj.get("contains") {
297297- if let Ok(contains_str) = contains_val.string() {
298298- where_condition.contains = Some(contains_str.to_string());
299299- }
300300- }
301301-302302- // Parse gt condition
303303- if let Some(gt_val) = condition_obj.get("gt") {
304304- if let Ok(gt_str) = gt_val.string() {
305305- where_condition.gt = Some(serde_json::Value::String(gt_str.to_string()));
306306- } else if let Ok(gt_i64) = gt_val.i64() {
307307- where_condition.gt = Some(serde_json::Value::Number(gt_i64.into()));
308308- }
309309- }
310310-311311- // Parse gte condition
312312- if let Some(gte_val) = condition_obj.get("gte") {
313313- if let Ok(gte_str) = gte_val.string() {
314314- where_condition.gte = Some(serde_json::Value::String(gte_str.to_string()));
315315- } else if let Ok(gte_i64) = gte_val.i64() {
316316- where_condition.gte = Some(serde_json::Value::Number(gte_i64.into()));
317317- }
318318- }
319319-320320- // Parse lt condition
321321- if let Some(lt_val) = condition_obj.get("lt") {
322322- if let Ok(lt_str) = lt_val.string() {
323323- where_condition.lt = Some(serde_json::Value::String(lt_str.to_string()));
324324- } else if let Ok(lt_i64) = lt_val.i64() {
325325- where_condition.lt = Some(serde_json::Value::Number(lt_i64.into()));
326326- }
327327- }
328328-329329- // Parse lte condition
330330- if let Some(lte_val) = condition_obj.get("lte") {
331331- if let Ok(lte_str) = lte_val.string() {
332332- where_condition.lte = Some(serde_json::Value::String(lte_str.to_string()));
333333- } else if let Ok(lte_i64) = lte_val.i64() {
334334- where_condition.lte = Some(serde_json::Value::Number(lte_i64.into()));
335335- }
336336- }
337337-338338- // Convert indexedAt to indexed_at for database column
339339- let db_field_name = if field_name == "indexedAt" {
340340- "indexed_at".to_string()
341341- } else {
342342- field_name.to_string()
343343- };
344344-345345- where_clause.conditions.insert(db_field_name, where_condition);
346346- }
347347- }
265265+ let parsed_where = parse_where_clause(where_obj);
266266+ // Merge parsed conditions with existing collection filter
267267+ where_clause.conditions.extend(parsed_where.conditions);
268268+ where_clause.or_conditions = parsed_where.or_conditions;
269269+ where_clause.and = parsed_where.and;
270270+ where_clause.or = parsed_where.or;
348271 }
349272 }
350273···583506 let mut where_clause = crate::models::WhereClause {
584507 conditions: HashMap::new(),
585508 or_conditions: None,
509509+ and: None,
510510+ or: None,
586511 };
587512588513 // Always filter by collection
···602527 // Parse where argument if provided
603528 if let Some(where_val) = ctx.args.get("where") {
604529 if let Ok(where_obj) = where_val.object() {
605605- for (field_name, condition_val) in where_obj.iter() {
606606- if let Ok(condition_obj) = condition_val.object() {
607607- let mut where_condition = crate::models::WhereCondition {
608608- gt: None,
609609- gte: None,
610610- lt: None,
611611- lte: None,
612612- eq: None,
613613- in_values: None,
614614- contains: None,
615615- };
616616-617617- // Parse eq condition
618618- if let Some(eq_val) = condition_obj.get("eq") {
619619- if let Ok(eq_str) = eq_val.string() {
620620- where_condition.eq = Some(serde_json::Value::String(eq_str.to_string()));
621621- } else if let Ok(eq_i64) = eq_val.i64() {
622622- where_condition.eq = Some(serde_json::Value::Number(eq_i64.into()));
623623- }
624624- }
625625-626626- // Parse in condition
627627- if let Some(in_val) = condition_obj.get("in") {
628628- if let Ok(in_list) = in_val.list() {
629629- let mut values = Vec::new();
630630- for item in in_list.iter() {
631631- if let Ok(s) = item.string() {
632632- values.push(serde_json::Value::String(s.to_string()));
633633- } else if let Ok(i) = item.i64() {
634634- values.push(serde_json::Value::Number(i.into()));
635635- }
636636- }
637637- where_condition.in_values = Some(values);
638638- }
639639- }
640640-641641- // Parse contains condition
642642- if let Some(contains_val) = condition_obj.get("contains") {
643643- if let Ok(contains_str) = contains_val.string() {
644644- where_condition.contains = Some(contains_str.to_string());
645645- }
646646- }
647647-648648- // Parse gt condition
649649- if let Some(gt_val) = condition_obj.get("gt") {
650650- if let Ok(gt_str) = gt_val.string() {
651651- where_condition.gt = Some(serde_json::Value::String(gt_str.to_string()));
652652- } else if let Ok(gt_i64) = gt_val.i64() {
653653- where_condition.gt = Some(serde_json::Value::Number(gt_i64.into()));
654654- }
655655- }
656656-657657- // Parse gte condition
658658- if let Some(gte_val) = condition_obj.get("gte") {
659659- if let Ok(gte_str) = gte_val.string() {
660660- where_condition.gte = Some(serde_json::Value::String(gte_str.to_string()));
661661- } else if let Ok(gte_i64) = gte_val.i64() {
662662- where_condition.gte = Some(serde_json::Value::Number(gte_i64.into()));
663663- }
664664- }
665665-666666- // Parse lt condition
667667- if let Some(lt_val) = condition_obj.get("lt") {
668668- if let Ok(lt_str) = lt_val.string() {
669669- where_condition.lt = Some(serde_json::Value::String(lt_str.to_string()));
670670- } else if let Ok(lt_i64) = lt_val.i64() {
671671- where_condition.lt = Some(serde_json::Value::Number(lt_i64.into()));
672672- }
673673- }
674674-675675- // Parse lte condition
676676- if let Some(lte_val) = condition_obj.get("lte") {
677677- if let Ok(lte_str) = lte_val.string() {
678678- where_condition.lte = Some(serde_json::Value::String(lte_str.to_string()));
679679- } else if let Ok(lte_i64) = lte_val.i64() {
680680- where_condition.lte = Some(serde_json::Value::Number(lte_i64.into()));
681681- }
682682- }
683683-684684- // Convert indexedAt to indexed_at for database column
685685- let db_field_name = if field_name == "indexedAt" {
686686- "indexed_at".to_string()
687687- } else {
688688- field_name.to_string()
689689- };
690690-691691- where_clause.conditions.insert(db_field_name, where_condition);
692692- }
693693- }
530530+ let parsed_where = parse_where_clause(where_obj);
531531+ // Merge parsed conditions with existing collection filter
532532+ where_clause.conditions.extend(parsed_where.conditions);
533533+ where_clause.or_conditions = parsed_where.or_conditions;
534534+ where_clause.and = parsed_where.and;
535535+ where_clause.or = parsed_where.or;
694536 }
695537 }
696538···985827 let mut where_clause = crate::models::WhereClause {
986828 conditions: std::collections::HashMap::new(),
987829 or_conditions: None,
830830+ and: None,
831831+ or: None,
988832 };
989833 where_clause.conditions.insert(
990834 "did".to_string(),
···13221166 let mut where_clause = crate::models::WhereClause {
13231167 conditions: HashMap::new(),
13241168 or_conditions: None,
11691169+ and: None,
11701170+ or: None,
13251171 };
13261172 where_clause.conditions.insert(
13271173 "collection".to_string(),
···15261372 let mut where_clause = crate::models::WhereClause {
15271373 conditions: HashMap::new(),
15281374 or_conditions: None,
13751375+ and: None,
13761376+ or: None,
15291377 };
1530137815311379 where_clause.conditions.insert(
···2520236825212369 subscription
25222370}
23712371+23722372+/// Helper function to parse GraphQL where clause recursively
23732373+fn parse_where_clause(where_obj: async_graphql::dynamic::ObjectAccessor) -> crate::models::WhereClause {
23742374+ let mut where_clause = crate::models::WhereClause {
23752375+ conditions: HashMap::new(),
23762376+ or_conditions: None,
23772377+ and: None,
23782378+ or: None,
23792379+ };
23802380+23812381+ for (field_name, condition_val) in where_obj.iter() {
23822382+ let field_str = field_name.as_str();
23832383+23842384+ // Handle nested AND array
23852385+ if field_str == "and" {
23862386+ if let Ok(and_list) = condition_val.list() {
23872387+ let mut and_clauses = Vec::new();
23882388+ for item in and_list.iter() {
23892389+ if let Ok(obj) = item.object() {
23902390+ and_clauses.push(parse_where_clause(obj));
23912391+ }
23922392+ }
23932393+ if !and_clauses.is_empty() {
23942394+ where_clause.and = Some(and_clauses);
23952395+ }
23962396+ }
23972397+ continue;
23982398+ }
23992399+24002400+ // Handle nested OR array
24012401+ if field_str == "or" {
24022402+ if let Ok(or_list) = condition_val.list() {
24032403+ let mut or_clauses = Vec::new();
24042404+ for item in or_list.iter() {
24052405+ if let Ok(obj) = item.object() {
24062406+ or_clauses.push(parse_where_clause(obj));
24072407+ }
24082408+ }
24092409+ if !or_clauses.is_empty() {
24102410+ where_clause.or = Some(or_clauses);
24112411+ }
24122412+ }
24132413+ continue;
24142414+ }
24152415+24162416+ // Handle regular field conditions
24172417+ if let Ok(condition_obj) = condition_val.object() {
24182418+ let mut where_condition = crate::models::WhereCondition {
24192419+ eq: None,
24202420+ in_values: None,
24212421+ contains: None,
24222422+ gt: None,
24232423+ gte: None,
24242424+ lt: None,
24252425+ lte: None,
24262426+ };
24272427+24282428+ // Parse eq condition
24292429+ if let Some(eq_val) = condition_obj.get("eq") {
24302430+ if let Ok(eq_str) = eq_val.string() {
24312431+ where_condition.eq = Some(serde_json::Value::String(eq_str.to_string()));
24322432+ } else if let Ok(eq_i64) = eq_val.i64() {
24332433+ where_condition.eq = Some(serde_json::Value::Number(eq_i64.into()));
24342434+ }
24352435+ }
24362436+24372437+ // Parse in condition
24382438+ if let Some(in_val) = condition_obj.get("in") {
24392439+ if let Ok(in_list) = in_val.list() {
24402440+ let mut values = Vec::new();
24412441+ for item in in_list.iter() {
24422442+ if let Ok(s) = item.string() {
24432443+ values.push(serde_json::Value::String(s.to_string()));
24442444+ } else if let Ok(i) = item.i64() {
24452445+ values.push(serde_json::Value::Number(i.into()));
24462446+ }
24472447+ }
24482448+ where_condition.in_values = Some(values);
24492449+ }
24502450+ }
24512451+24522452+ // Parse contains condition
24532453+ if let Some(contains_val) = condition_obj.get("contains") {
24542454+ if let Ok(contains_str) = contains_val.string() {
24552455+ where_condition.contains = Some(contains_str.to_string());
24562456+ }
24572457+ }
24582458+24592459+ // Parse gt condition
24602460+ if let Some(gt_val) = condition_obj.get("gt") {
24612461+ if let Ok(gt_str) = gt_val.string() {
24622462+ where_condition.gt = Some(serde_json::Value::String(gt_str.to_string()));
24632463+ } else if let Ok(gt_i64) = gt_val.i64() {
24642464+ where_condition.gt = Some(serde_json::Value::Number(gt_i64.into()));
24652465+ }
24662466+ }
24672467+24682468+ // Parse gte condition
24692469+ if let Some(gte_val) = condition_obj.get("gte") {
24702470+ if let Ok(gte_str) = gte_val.string() {
24712471+ where_condition.gte = Some(serde_json::Value::String(gte_str.to_string()));
24722472+ } else if let Ok(gte_i64) = gte_val.i64() {
24732473+ where_condition.gte = Some(serde_json::Value::Number(gte_i64.into()));
24742474+ }
24752475+ }
24762476+24772477+ // Parse lt condition
24782478+ if let Some(lt_val) = condition_obj.get("lt") {
24792479+ if let Ok(lt_str) = lt_val.string() {
24802480+ where_condition.lt = Some(serde_json::Value::String(lt_str.to_string()));
24812481+ } else if let Ok(lt_i64) = lt_val.i64() {
24822482+ where_condition.lt = Some(serde_json::Value::Number(lt_i64.into()));
24832483+ }
24842484+ }
24852485+24862486+ // Parse lte condition
24872487+ if let Some(lte_val) = condition_obj.get("lte") {
24882488+ if let Ok(lte_str) = lte_val.string() {
24892489+ where_condition.lte = Some(serde_json::Value::String(lte_str.to_string()));
24902490+ } else if let Ok(lte_i64) = lte_val.i64() {
24912491+ where_condition.lte = Some(serde_json::Value::Number(lte_i64.into()));
24922492+ }
24932493+ }
24942494+24952495+ // Convert indexedAt to indexed_at for database column
24962496+ let db_field_name = if field_str == "indexedAt" {
24972497+ "indexed_at".to_string()
24982498+ } else {
24992499+ field_str.to_string()
25002500+ };
25012501+25022502+ where_clause.conditions.insert(db_field_name, where_condition);
25032503+ }
25042504+ }
25052505+25062506+ where_clause
25072507+}
+138-12
docs/graphql-api.md
···11# GraphQL API
2233-Slices provides a powerful GraphQL API for querying indexed AT Protocol data. The API automatically generates schema from your lexicons and provides efficient querying with relationship traversal.
33+Slices provides a powerful GraphQL API for querying indexed AT Protocol data.
44+The API automatically generates schema from your lexicons and provides efficient
55+querying with relationship traversal.
4657## Accessing the API
68···22242325The GraphQL schema is automatically generated from your slice's lexicons:
24262525-- **Types**: One GraphQL type per collection (e.g., `social.grain.gallery` → `SocialGrainGallery`)
2727+- **Types**: One GraphQL type per collection (e.g., `social.grain.gallery` →
2828+ `SocialGrainGallery`)
2629- **Queries**: Collection queries with filtering, sorting, and pagination
2730- **Mutations**: Create, update, delete operations per collection
2831- **Subscriptions**: Real-time updates for record changes
···48514952### Filtering
50535151-Use `where` clauses with typed filter conditions. Each collection has its own `{Collection}WhereInput` type with appropriate filters for each field.
5454+Use `where` clauses with typed filter conditions. Each collection has its own
5555+`{Collection}WhereInput` type with appropriate filters for each field.
52565357```graphql
5458query {
···7175The API provides three filter types based on field data types:
72767377**StringFilter** - For string fields:
7878+7479- `eq`: Exact match
7580- `in`: Match any value in array
7681- `contains`: Substring match (case-insensitive)
···8085- `lte`: Less than or equal to
81868287**IntFilter** - For integer fields:
8888+8389- `eq`: Exact match
8490- `in`: Match any value in array
8591- `gt`: Greater than
···8894- `lte`: Less than or equal to
89959096**DateTimeFilter** - For datetime fields:
9797+9198- `eq`: Exact match
9299- `gt`: After datetime
93100- `gte`: At or after datetime
···141148}
142149```
143150151151+#### Nested AND/OR Queries
152152+153153+Build complex filter logic with arbitrarily nestable `and` and `or` arrays:
154154+155155+**Simple OR - Match any condition:**
156156+157157+```graphql
158158+query {
159159+ networkSlicesSlices(
160160+ where: {
161161+ or: [
162162+ { name: { contains: "grain" } }
163163+ { name: { contains: "teal" } }
164164+ ]
165165+ }
166166+ ) {
167167+ edges {
168168+ node {
169169+ name
170170+ }
171171+ }
172172+ }
173173+}
174174+```
175175+176176+**Simple AND - Match all conditions:**
177177+178178+```graphql
179179+query {
180180+ networkSlicesSlices(
181181+ where: {
182182+ and: [
183183+ { name: { contains: "grain" } }
184184+ { name: { contains: "teal" } }
185185+ ]
186186+ }
187187+ ) {
188188+ edges {
189189+ node {
190190+ name
191191+ }
192192+ }
193193+ }
194194+}
195195+```
196196+197197+**Complex Nested Logic:**
198198+199199+```graphql
200200+query {
201201+ appBskyFeedPost(
202202+ where: {
203203+ and: [
204204+ {
205205+ or: [
206206+ { text: { contains: "music" } }
207207+ { text: { contains: "song" } }
208208+ ]
209209+ }
210210+ {
211211+ and: [
212212+ { uri: { contains: "app.bsky" } }
213213+ { uri: { contains: "post" } }
214214+ ]
215215+ }
216216+ { createdAt: { gte: "2025-01-01T00:00:00Z" } }
217217+ ]
218218+ }
219219+ ) {
220220+ edges {
221221+ node {
222222+ uri
223223+ text
224224+ createdAt
225225+ }
226226+ }
227227+ }
228228+}
229229+```
230230+231231+This example finds posts where:
232232+233233+- (text contains "music" OR text contains "song") AND
234234+- (uri contains "app.bsky" AND uri contains "post") AND
235235+- createdAt is after 2025-01-01
236236+237237+**Key Features:**
238238+239239+- Unlimited nesting depth - `and`/`or` can be nested arbitrarily
240240+- Mix with field filters - combine nested logic with regular field conditions
241241+- Type-safe - Each collection's `WhereInput` supports `and` and `or` arrays
242242+- Available in queries and aggregations
243243+144244### Pagination
145245146246Relay-style cursor pagination:
···165265166266### Sorting
167267168168-Each collection has its own typed `{Collection}SortFieldInput` for type-safe sorting:
268268+Each collection has its own typed `{Collection}SortFieldInput` for type-safe
269269+sorting:
169270170271```graphql
171272query {
···186287```
187288188289**Multi-field sorting:**
290290+189291```graphql
190292query {
191293 socialGrainGalleries(
···206308}
207309```
208310209209-The `field` enum values are collection-specific (e.g., `SocialGrainGallerySortFieldInput`). Use GraphQL introspection or the playground to see available fields for each collection.
311311+The `field` enum values are collection-specific (e.g.,
312312+`SocialGrainGallerySortFieldInput`). Use GraphQL introspection or the playground
313313+to see available fields for each collection.
210314211315## Aggregations
212316213213-Aggregation queries allow you to group records and perform calculations. Each collection has a corresponding `{Collection}Aggregated` query.
317317+Aggregation queries allow you to group records and perform calculations. Each
318318+collection has a corresponding `{Collection}Aggregated` query.
214319215320### Basic Aggregation
216321···274379275380### Aggregation Features
276381277277-- **Typed GroupBy**: Each collection has a `{Collection}GroupByField` enum for type-safe field selection
382382+- **Typed GroupBy**: Each collection has a `{Collection}GroupByField` enum for
383383+ type-safe field selection
278384- **Typed Filters**: Use the same `{Collection}WhereInput` as regular queries
279385- **Sorting**: Order by `count` (ascending or descending) or any grouped field
280386- **Pagination**: Use `limit` to control result count
281387- **Multiple Fields**: Group by any combination of fields from your lexicon
282282-- **Date Truncation**: Group by time intervals (second, minute, hour, day, week, month, quarter, year)
388388+- **Date Truncation**: Group by time intervals (second, minute, hour, day, week,
389389+ month, quarter, year)
283390284391### Date Truncation
285392···301408```
302409303410**Supported Intervals:**
411411+304412- `second` - Group by second
305413- `minute` - Group by minute
306414- `hour` - Group by hour
···311419- `year` - Group by year
312420313421**Combining with Regular Fields:**
422422+314423```graphql
315424query TrackPlaysByDay {
316425 fmTealAlphaFeedPlaysAggregated(
···329438```
330439331440**How it Works:**
441441+332442- Uses PostgreSQL's `date_trunc()` function for efficient time bucketing
333443- Automatically handles timestamp casting for JSON fields
334444- Returns truncated timestamps (e.g., `2025-01-15 00:00:00` for day interval)
···337447### Use Cases
338448339449**Daily/Weekly/Monthly Reports**:
450450+340451```graphql
341452query WeeklyPlays {
342453 fmTealAlphaFeedPlaysAggregated(
···357468```
358469359470**Trend Analysis**:
471471+360472```graphql
361473query TrendingArtists {
362474 fmTealAlphaFeedPlaysAggregated(
···375487376488## Relationships
377489378378-The GraphQL API automatically generates relationship fields based on your lexicon's `at-uri` fields.
490490+The GraphQL API automatically generates relationship fields based on your
491491+lexicon's `at-uri` fields.
379492380493### Forward Joins (References)
381494382382-When a record has an `at-uri` field, you get a **singular** field that resolves to the referenced record.
495495+When a record has an `at-uri` field, you get a **singular** field that resolves
496496+to the referenced record.
383497384498**Lexicon Schema (social.grain.gallery.item):**
499499+385500```json
386501{
387502 "lexicon": 1,
···412527```
413528414529**Generated GraphQL Type:**
530530+415531```graphql
416532type SocialGrainGalleryItem {
417533 uri: String!
···427543```
428544429545**Example Query:**
546546+430547```graphql
431548query {
432549 socialGrainGalleryItems(limit: 5) {
···448565449566### Reverse Joins (Backlinks)
450567451451-When other records reference this record via `at-uri` fields, you get **plural** fields that find all records pointing here.
568568+When other records reference this record via `at-uri` fields, you get **plural**
569569+fields that find all records pointing here.
452570453571**Lexicon Schema (social.grain.favorite):**
572572+454573```json
455574{
456575 "lexicon": 1,
···476595```
477596478597**Generated GraphQL Types:**
598598+479599```graphql
480600type SocialGrainFavorite {
481601 uri: String!
···499619```
500620501621**Example Query:**
622622+502623```graphql
503624query {
504625 socialGrainGalleries(where: {
···582703The GraphQL API uses DataLoader for efficient batching:
583704584705### CollectionDidLoader
706706+585707- Batches queries by `(slice_uri, collection, did)`
586708- Used for forward joins where the DID is known
587709- Eliminates N+1 queries when following references
588710589711### CollectionUriLoader
712712+590713- Batches queries by `(slice_uri, collection, parent_uri, reference_field)`
591714- Used for reverse joins based on at-uri fields
592715- Efficiently loads all records that reference a parent URI
593716- Supports multiple at-uri fields (tries each until match found)
594717595718Example: Loading 100 galleries with favorites
719719+596720- **Without DataLoader**: 1 + 100 queries (N+1 problem)
597721- **With DataLoader**: 1 + 1 query (batched)
598722···696820697821## Subscriptions
698822699699-Real-time updates for record changes. Each collection has three subscription fields:
823823+Real-time updates for record changes. Each collection has three subscription
824824+fields:
700825701826### Created Records
702827···757882## Error Handling
758883759884GraphQL errors include:
885885+760886- `"Query is nested too deep"` - Exceeds depth limit (50)
761887- `"Query is too complex"` - Exceeds complexity limit (5000)
762888- `"Schema error"` - Invalid slice or missing lexicons