From 6fdc63aa92bb5df9a28139b94274b40a7b920433 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 12 Sep 2026 17:06:43 +1200 Subject: [PATCH] feat: add Query.relationship helper Expose relationship queries on generated Query classes so SDKs can constrain populated related documents the same way as Query::relationship in utopia-php/database. --- .../src/main/java/io/package/Query.kt.twig | 9 ++++++++ templates/dart/lib/query.dart.twig | 10 +++++++++ templates/dart/test/query_test.dart.twig | 18 +++++++++++++++ templates/deno/src/query.ts.twig | 14 ++++++++++++ templates/deno/test/query.test.ts.twig | 16 ++++++++++++++ templates/dotnet/Package/Query.cs.twig | 14 ++++++++++++ templates/go/query.go.twig | 21 ++++++++++++++++++ templates/go/query_test.go.twig | 22 +++++++++++++++++++ .../src/main/kotlin/io/appwrite/Query.kt.twig | 9 ++++++++ templates/php/src/Query.php.twig | 13 +++++++++++ templates/php/tests/QueryTest.php.twig | 19 ++++++++++++++++ templates/python/package/query.py.twig | 14 ++++++++++++ templates/react-native/src/query.ts.twig | 14 ++++++++++++ templates/ruby/lib/container/query.rb.twig | 9 ++++++++ templates/rust/src/query.rs.twig | 13 +++++++++++ templates/rust/tests/tests.rs | 4 ++++ templates/skills/dart.md.twig | 1 + templates/skills/dotnet.md.twig | 1 + templates/skills/go.md.twig | 1 + templates/skills/kotlin.md.twig | 1 + templates/skills/php.md.twig | 1 + templates/skills/python.md.twig | 1 + templates/skills/ruby.md.twig | 1 + templates/skills/rust.md.twig | 9 ++++++++ templates/skills/swift.md.twig | 1 + templates/skills/typescript.md.twig | 1 + templates/swift/Sources/Query.swift.twig | 22 +++++++++++++++++++ templates/web/src/query.ts.twig | 14 ++++++++++++ tests/e2e/Base.php | 1 + tests/e2e/languages/android/Tests.kt | 4 ++++ tests/e2e/languages/apple/Tests.swift | 4 ++++ tests/e2e/languages/dart/tests.dart | 4 ++++ tests/e2e/languages/deno/tests.ts | 4 ++++ tests/e2e/languages/dotnet/Tests.cs | 4 ++++ tests/e2e/languages/flutter/tests.dart | 4 ++++ tests/e2e/languages/go-v2/tests.go | 4 ++++ tests/e2e/languages/go/tests.go | 4 ++++ tests/e2e/languages/kotlin/Tests.kt | 4 ++++ tests/e2e/languages/node/test.js | 4 ++++ tests/e2e/languages/php/test.php | 4 ++++ tests/e2e/languages/python/tests.py | 4 ++++ tests/e2e/languages/react-native/browser.js | 4 ++++ tests/e2e/languages/ruby/tests.rb | 4 ++++ tests/e2e/languages/swift/Tests.swift | 4 ++++ tests/e2e/languages/unity/Tests.cs | 4 ++++ tests/e2e/languages/web/index.html | 4 ++++ tests/e2e/languages/web/node.js | 4 ++++ 47 files changed, 346 insertions(+) diff --git a/templates/android/library/src/main/java/io/package/Query.kt.twig b/templates/android/library/src/main/java/io/package/Query.kt.twig index aa20ea032e..9af96aa9dc 100644 --- a/templates/android/library/src/main/java/io/package/Query.kt.twig +++ b/templates/android/library/src/main/java/io/package/Query.kt.twig @@ -369,6 +369,15 @@ class Query( */ fun elemMatch(attribute: String, queries: List) = Query("elemMatch", attribute, queries.map { it.fromJson() }).toJson() + /** + * Constrain populated related documents on a relationship attribute. + * + * @param attribute The relationship attribute to query. + * @param queries Filter, order, select, and pagination queries applied to related documents. + * @returns The query string. + */ + fun relationship(attribute: String, queries: List) = Query("relationship", attribute, queries.map { it.fromJson() }).toJson() + /** * Filter resources where attribute is at a specific distance from the given coordinates. * diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index c117b33a1f..ad898d1f77 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -181,6 +181,16 @@ class Query { queries.map((query) => jsonDecode(query)).toList(), ).toString(); + /// Constrain populated related documents on a relationship attribute. + /// + /// [attribute] The relationship attribute to query. + /// [queries] Filter, order, select, and pagination queries applied to related documents. + static String relationship(String attribute, List queries) => Query._( + 'relationship', + attribute, + queries.map((query) => jsonDecode(query)).toList(), + ).toString(); + /// Specify which attributes should be returned by the API call. static String select(List attributes) => Query._('select', null, attributes).toString(); diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index cfe0f76e98..86b28b7de6 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -316,6 +316,24 @@ void main() { expect(query['values'][1]['values'], [18]); }); + test('returns relationship', () { + final inner1 = Query.equal('approved', true); + final inner2 = Query.limit(2); + + final query = jsonDecode(Query.relationship('comments', [inner1, inner2])); + + expect(query['attribute'], 'comments'); + expect(query['method'], 'relationship'); + expect(query['values'].length, 2); + + expect(query['values'][0]['method'], 'equal'); + expect(query['values'][0]['attribute'], 'approved'); + expect(query['values'][0]['values'], [true]); + + expect(query['values'][1]['method'], 'limit'); + expect(query['values'][1]['values'], [2]); + }); + test('returns createdBefore', () { final query = jsonDecode(Query.createdBefore('2023-01-01')); expect(query['attribute'], '\$createdAt'); diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index e5d829dd4f..716b2475cc 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -288,6 +288,20 @@ export class Query { queries.map((query) => JSON.parse(query)) ).toString(); + /** + * Constrain populated related documents on a relationship attribute. + * + * @param {string} attribute The relationship attribute to query. + * @param {string[]} queries Filter, order, select, and pagination queries applied to related documents. + * @returns {string} + */ + static relationship = (attribute: string, queries: string[]): string => + new Query( + "relationship", + attribute, + queries.map((query) => JSON.parse(query)) + ).toString(); + /** * Filter resources where attribute is at a specific distance from the given coordinates. * diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index f4aad6381e..16979b4be9 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -245,6 +245,22 @@ describe('Query', () => { assertEquals(parsed.values[1].values, [18]); }); + test('relationship', () => { + const inner1 = Query.equal('approved', true); + const inner2 = Query.limit(2); + const result = Query.relationship('comments', [inner1, inner2]); + const parsed = JSON.parse(result); + + assertEquals(parsed.method, 'relationship'); + assertEquals(parsed.attribute, 'comments'); + assertEquals(parsed.values.length, 2); + assertEquals(parsed.values[0].method, 'equal'); + assertEquals(parsed.values[0].attribute, 'approved'); + assertEquals(parsed.values[0].values, [true]); + assertEquals(parsed.values[1].method, 'limit'); + assertEquals(parsed.values[1].values, [2]); + }); + test('createdBefore', () => assertEquals( Query.createdBefore('2023-01-01').toString(), `{"method":"lessThan","attribute":"$createdAt","values":["2023-01-01"]}`, diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 3b32e28c91..277caf77a8 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -305,6 +305,20 @@ namespace {{ spec.info.title | caseUcfirst }} return new Query("elemMatch", attribute, parsed).ToString(); } + /// + /// Constrain populated related documents on a relationship attribute. + /// + /// The relationship attribute to query. + /// Filter, order, select, and pagination queries applied to related documents. + /// The query string. + public static string Relationship(string attribute, List queries) + { + var parsed = queries + .Select(q => JsonSerializer.Deserialize(q, Client.DeserializerOptions) as object) + .ToList(); + return new Query("relationship", attribute, parsed).ToString(); + } + public static string DistanceEqual(string attribute, object values, double distance, bool meters = true) { return new Query("distanceEqual", attribute, new List { new List { values, distance, meters } }).ToString(); diff --git a/templates/go/query.go.twig b/templates/go/query.go.twig index b5361f5e3e..2e1e854aac 100644 --- a/templates/go/query.go.twig +++ b/templates/go/query.go.twig @@ -520,3 +520,24 @@ func ElemMatch(attribute string, queries []string) string { Values: &parsedQueries, }) } + +// Relationship constrains populated related documents on a relationship attribute. +// +// attribute: The relationship attribute to query. +// queries: Filter, order, select, and pagination queries applied to related documents. +// Returns the query string. +func Relationship(attribute string, queries []string) string { + var parsedQueries []interface{} + for _, query := range queries { + var q interface{} + if err := json.Unmarshal([]byte(query), &q); err != nil { + continue + } + parsedQueries = append(parsedQueries, q) + } + return parseQuery(queryOptions{ + Method: "relationship", + Attribute: &attribute, + Values: &parsedQueries, + }) +} diff --git a/templates/go/query_test.go.twig b/templates/go/query_test.go.twig index 804e46492b..78966226d1 100644 --- a/templates/go/query_test.go.twig +++ b/templates/go/query_test.go.twig @@ -125,4 +125,26 @@ func TestOr(t *testing.T) { t.Errorf("Expected 2 subqueries, got %d", len(values)) } } + +func TestRelationship(t *testing.T) { + q1 := Equal("approved", true) + q2 := Limit(2) + q := Relationship("comments", []string{q1, q2}) + + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "relationship" { + t.Errorf("Expected relationship, got %v", data["method"]) + } + if data["attribute"] != "comments" { + t.Errorf("Expected comments, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if len(values) != 2 { + t.Errorf("Expected 2 subqueries, got %d", len(values)) + } +} {% endautoescape %} diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig index 961874d154..106e9e09fa 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -133,6 +133,15 @@ class Query( */ fun elemMatch(attribute: String, queries: List) = Query("elemMatch", attribute, queries.map { it.fromJson() }).toJson() + /** + * Constrain populated related documents on a relationship attribute. + * + * @param attribute The relationship attribute to query. + * @param queries Filter, order, select, and pagination queries applied to related documents. + * @returns The query string. + */ + fun relationship(attribute: String, queries: List) = Query("relationship", attribute, queries.map { it.fromJson() }).toJson() + /** * Filter resources where attribute is at a specific distance from the given coordinates. * diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 51b15c048c..34c684721d 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -414,6 +414,19 @@ class Query implements \JsonSerializable, \Stringable return new Query('elemMatch', $attribute, $queries)->__toString(); } + /** + * Constrain populated related documents on a relationship attribute. + * + */ + public static function relationship(string $attribute, array $queries): string + { + foreach ($queries as &$query) { + $query = \json_decode($query, true); + } + + return new Query('relationship', $attribute, $queries)->__toString(); + } + /** * Distance Equal * diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index f3e3590433..cf8ca7fb7d 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -314,6 +314,25 @@ final class QueryTest extends TestCase $this->assertSame([18], $query['values'][1]['values']); } + public function testRelationship(): void + { + $inner1 = Query::equal('approved', true); + $inner2 = Query::limit(2); + + $query = json_decode(Query::relationship('comments', [$inner1, $inner2]), true); + + $this->assertSame('comments', $query['attribute']); + $this->assertSame('relationship', $query['method']); + $this->assertCount(2, $query['values']); + + $this->assertSame('equal', $query['values'][0]['method']); + $this->assertSame('approved', $query['values'][0]['attribute']); + $this->assertSame([true], $query['values'][0]['values']); + + $this->assertSame('limit', $query['values'][1]['method']); + $this->assertSame([2], $query['values'][1]['values']); + } + public function testCreatedBefore(): void { $query = json_decode(Query::createdBefore('2023-01-01'), true); diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index 154a19e6d9..1cc1e3005b 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -229,6 +229,20 @@ class Query: """ return str(Query("elemMatch", attribute, [json.loads(query) for query in queries])) + @staticmethod + def relationship(attribute, queries): + """ + Constrain populated related documents on a relationship attribute. + + Args: + attribute: The relationship attribute to query. + queries: Filter, order, select, and pagination queries applied to related documents. + + Returns: + The query string. + """ + return str(Query("relationship", attribute, [json.loads(query) for query in queries])) + @staticmethod def distance_equal(attribute, values, distance, meters=True): return str(Query("distanceEqual", attribute, [[values, distance, meters]])) diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index 104b1ab970..2ec629c47c 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -310,6 +310,20 @@ export class Query { queries.map((query) => JSONbig.parse(query)), ).toString(); + /** + * Constrain populated related documents on a relationship attribute. + * + * @param {string} attribute The relationship attribute to query. + * @param {string[]} queries Filter, order, select, and pagination queries applied to related documents. + * @returns {string} + */ + static relationship = (attribute: string, queries: string[]): string => + new Query( + 'relationship', + attribute, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + /** * Filter resources where attribute is at a specific distance from the given coordinates. * diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index cde5739790..d2b0548677 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -216,6 +216,15 @@ module {{spec.info.title | caseUcfirst}} return Query.new("elemMatch", attribute, queries.map { |query| JSON.parse(query) }).to_s end + # Constrain populated related documents on a relationship attribute. + # + # @param attribute [String] The relationship attribute to query. + # @param queries [Array] Filter, order, select, and pagination queries applied to related documents. + # @return [String] The query string. + def relationship(attribute, queries) + return Query.new("relationship", attribute, queries.map { |query| JSON.parse(query) }).to_s + end + def distance_equal(attribute, values, distance, meters = true) return Query.new("distanceEqual", attribute, [[values, distance, meters]]).to_s end diff --git a/templates/rust/src/query.rs.twig b/templates/rust/src/query.rs.twig index 57664a9ae8..0dc3693042 100644 --- a/templates/rust/src/query.rs.twig +++ b/templates/rust/src/query.rs.twig @@ -458,6 +458,19 @@ impl Query { Self::new("elemMatch".to_string(), Some(attribute.into()), values) } + pub fn relationship(attribute: S, queries: I) -> Self + where + S: Into, + I: IntoIterator, + Q: AsRef, + { + let values: Vec = queries + .into_iter() + .filter_map(|query| serde_json::from_str(query.as_ref()).ok()) + .collect(); + Self::new("relationship".to_string(), Some(attribute.into()), values) + } + /// Convert query to JSON value (matches Go/Python SDK format) pub fn to_value(self) -> Value { let mut obj = serde_json::Map::new(); diff --git a/templates/rust/tests/tests.rs b/templates/rust/tests/tests.rs index 227457870c..a8283a8ba2 100644 --- a/templates/rust/tests/tests.rs +++ b/templates/rust/tests/tests.rs @@ -301,6 +301,10 @@ fn test_queries() { Query::equal("name", "Alice").to_string(), Query::greater_than("age", 18).to_string(), ])); + println!("{}", Query::relationship("comments", vec![ + Query::equal("approved", true).to_string(), + Query::limit(2).to_string(), + ])); } fn test_permission_helpers() { diff --git a/templates/skills/dart.md.twig b/templates/skills/dart.md.twig index 751d63ce0d..921457ba0d 100644 --- a/templates/skills/dart.md.twig +++ b/templates/skills/dart.md.twig @@ -189,6 +189,7 @@ Query.cursorBefore('[ROW_ID]') Query.select(['field1', 'field2']) // return only specified fields Query.or([Query.equal('a', 1), Query.equal('b', 2)]) // OR Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)]) // AND (default) +Query.relationship('comments', [Query.equal('approved', true), Query.limit(25)]) // related documents ``` ### File Storage diff --git a/templates/skills/dotnet.md.twig b/templates/skills/dotnet.md.twig index 2cd7350986..bc4cce82ed 100644 --- a/templates/skills/dotnet.md.twig +++ b/templates/skills/dotnet.md.twig @@ -132,6 +132,7 @@ Query.CursorBefore("[ROW_ID]") Query.Select(new List { "field1", "field2" }) Query.Or(new List { Query.Equal("a", 1), Query.Equal("b", 2) }) // OR Query.And(new List { Query.GreaterThan("age", 18), Query.LessThan("age", 65) }) // AND (default) +Query.Relationship("comments", new List { Query.Equal("approved", true), Query.Limit(25) }) // related documents ``` ### File Storage diff --git a/templates/skills/go.md.twig b/templates/skills/go.md.twig index 3e88f95d23..9796bbe126 100644 --- a/templates/skills/go.md.twig +++ b/templates/skills/go.md.twig @@ -157,6 +157,7 @@ query.CursorBefore("[ROW_ID]") query.Select([]string{"field1", "field2"}) query.Or([]string{query.Equal("a", 1), query.Equal("b", 2)}) // OR query.And([]string{query.GreaterThan("age", 18), query.LessThan("age", 65)}) // AND (default) +query.Relationship("comments", []string{query.Equal("approved", true), query.Limit(25)}) // related documents ``` ### File Storage diff --git a/templates/skills/kotlin.md.twig b/templates/skills/kotlin.md.twig index b7c49276c8..34912f39c7 100644 --- a/templates/skills/kotlin.md.twig +++ b/templates/skills/kotlin.md.twig @@ -213,6 +213,7 @@ Query.cursorBefore("[ROW_ID]") Query.select(listOf("field1", "field2")) Query.or(listOf(Query.equal("a", 1), Query.equal("b", 2))) // OR Query.and(listOf(Query.greaterThan("age", 18), Query.lessThan("age", 65))) // AND (default) +Query.relationship("comments", listOf(Query.equal("approved", true), Query.limit(25))) // related documents ``` ### File Storage diff --git a/templates/skills/php.md.twig b/templates/skills/php.md.twig index 705f8f262f..14cc2d5b1c 100644 --- a/templates/skills/php.md.twig +++ b/templates/skills/php.md.twig @@ -140,6 +140,7 @@ Query::cursorBefore('[ROW_ID]') Query::select(['field1', 'field2']) // return only specified fields Query::or([Query::equal('a', [1]), Query::equal('b', [2])]) // OR Query::and([Query::greaterThan('age', 18), Query::lessThan('age', 65)]) // AND (default) +Query::relationship('comments', [Query::equal('approved', [true]), Query::limit(25)]) // related documents ``` ### File Storage diff --git a/templates/skills/python.md.twig b/templates/skills/python.md.twig index 658eed487c..09eb05c008 100644 --- a/templates/skills/python.md.twig +++ b/templates/skills/python.md.twig @@ -147,6 +147,7 @@ Query.cursor_before('[ROW_ID]') Query.select(['field1', 'field2']) # return only specified fields Query.or_queries([Query.equal('a', 1), Query.equal('b', 2)]) # OR Query.and_queries([Query.greater_than('age', 18), Query.less_than('age', 65)]) # AND (default) +Query.relationship('comments', [Query.equal('approved', True), Query.limit(25)]) # related documents ``` ### File Storage diff --git a/templates/skills/ruby.md.twig b/templates/skills/ruby.md.twig index 485de2a07f..1f5a15eea5 100644 --- a/templates/skills/ruby.md.twig +++ b/templates/skills/ruby.md.twig @@ -147,6 +147,7 @@ Query.cursor_before('[ROW_ID]') Query.select(['field1', 'field2']) # return only specified fields Query.or([Query.equal('a', 1), Query.equal('b', 2)]) # OR Query.and([Query.greater_than('age', 18), Query.less_than('age', 65)]) # AND (default) +Query.relationship('comments', [Query.equal('approved', true), Query.limit(25)]) # related documents ``` ### File Storage diff --git a/templates/skills/rust.md.twig b/templates/skills/rust.md.twig index ea7b918e0f..e5a728c209 100644 --- a/templates/skills/rust.md.twig +++ b/templates/skills/rust.md.twig @@ -273,6 +273,15 @@ let multi_value = Query::equal( ]), ) .to_string(); + +let related = Query::relationship( + "comments", + vec![ + Query::equal("approved", true).to_string(), + Query::limit(25).to_string(), + ], +) +.to_string(); ``` ### File Storage diff --git a/templates/skills/swift.md.twig b/templates/skills/swift.md.twig index 10d229e719..ce8cec5c97 100644 --- a/templates/skills/swift.md.twig +++ b/templates/skills/swift.md.twig @@ -171,6 +171,7 @@ Query.cursorBefore("[ROW_ID]") Query.select(["field1", "field2"]) Query.or([Query.equal("a", value: 1), Query.equal("b", value: 2)]) // OR Query.and([Query.greaterThan("age", value: 18), Query.lessThan("age", value: 65)]) // AND (default) +Query.relationship("comments", queries: [Query.equal("approved", value: true), Query.limit(25)]) // related documents ``` ### File Storage diff --git a/templates/skills/typescript.md.twig b/templates/skills/typescript.md.twig index d7afc9e2c7..7b25101133 100644 --- a/templates/skills/typescript.md.twig +++ b/templates/skills/typescript.md.twig @@ -330,6 +330,7 @@ Query.select(['field1', 'field2']) // return only specified fields // Logical Query.or([Query.equal('a', 1), Query.equal('b', 2)]) // OR condition Query.and([Query.greaterThan('age', 18), Query.lessThan('age', 65)]) // explicit AND (queries are AND by default) +Query.relationship('comments', [Query.equal('approved', true), Query.limit(25)]) // related documents ``` ### File Storage diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index 8aa122a7c6..c16d6c2488 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -516,6 +516,28 @@ public struct Query: Codable, CustomStringConvertible { ).description } + /// Constrain populated related documents on a relationship attribute. + /// + /// - Parameters: + /// - attribute: The relationship attribute to query. + /// - queries: Filter, order, select, and pagination queries applied to related documents. + /// - Returns: The query string. + public static func relationship(_ attribute: String, queries: [String]) -> String { + let decoder = JSONDecoder() + let decodedQueries = queries.compactMap { queryStr -> Query? in + guard let data = queryStr.data(using: .utf8) else { + return nil + } + return try? decoder.decode(Query.self, from: data) + } + + return Query( + method: "relationship", + attribute: attribute, + values: decodedQueries + ).description + } + public static func distanceEqual( _ attribute: String, values: [Any], distance: Double, meters: Bool = true ) -> String { diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index 7a7da885ef..9842610540 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -468,6 +468,20 @@ export class Query { queries.map((query) => JSONbig.parse(query)), ).toString(); + /** + * Constrain populated related documents on a relationship attribute. + * + * @param {string} attribute The relationship attribute to query. + * @param {string[]} queries Filter, order, select, and pagination queries applied to related documents. + * @returns {string} + */ + static relationship = (attribute: string, queries: string[]): string => + new Query( + 'relationship', + attribute, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + /** * Filter resources where attribute is at a specific distance from the given coordinates. * diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 05840af2d3..14e42283bf 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -206,6 +206,7 @@ abstract class Base extends TestCase '{"method":"exists","values":["attr1","attr2"]}', '{"method":"notExists","values":["attr1","attr2"]}', '{"method":"elemMatch","attribute":"friends","values":[{"method":"equal","attribute":"name","values":["Alice"]},{"method":"greaterThan","attribute":"age","values":[18]}]}', + '{"method":"relationship","attribute":"comments","values":[{"method":"equal","attribute":"approved","values":[true]},{"method":"limit","values":[2]}]}', ]; protected const PERMISSION_HELPER_RESPONSES = [ diff --git a/tests/e2e/languages/android/Tests.kt b/tests/e2e/languages/android/Tests.kt index 0a452eff55..64942065c9 100644 --- a/tests/e2e/languages/android/Tests.kt +++ b/tests/e2e/languages/android/Tests.kt @@ -409,6 +409,10 @@ class ServiceTest { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ))) + writeToFile(Query.relationship("comments", listOf( + Query.equal("approved", true), + Query.limit(2) + ))) // Permission & Roles helper tests writeToFile(Permission.read(Role.any())) diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index fd68a57ea7..1cdbf87840 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -386,6 +386,10 @@ class Tests: XCTestCase { Query.equal("name", value: "Alice"), Query.greaterThan("age", value: 18) ])) + print(Query.relationship("comments", queries: [ + Query.equal("approved", value: true), + Query.limit(2) + ])) // Permission & Role helper tests print(Permission.read(Role.any())) diff --git a/tests/e2e/languages/dart/tests.dart b/tests/e2e/languages/dart/tests.dart index 3cbd915553..af32b63bec 100644 --- a/tests/e2e/languages/dart/tests.dart +++ b/tests/e2e/languages/dart/tests.dart @@ -236,6 +236,10 @@ void main() async { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + print(Query.relationship("comments", [ + Query.equal("approved", true), + Query.limit(2) + ])); // Permission & Role helper tests print(Permission.read(Role.any())); diff --git a/tests/e2e/languages/deno/tests.ts b/tests/e2e/languages/deno/tests.ts index af760f9890..fa08bb66d5 100644 --- a/tests/e2e/languages/deno/tests.ts +++ b/tests/e2e/languages/deno/tests.ts @@ -268,6 +268,10 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.relationship("comments", [ + Query.equal("approved", true), + Query.limit(2) + ])); // Permission & Role helper tests console.log(Permission.read(Role.any())); diff --git a/tests/e2e/languages/dotnet/Tests.cs b/tests/e2e/languages/dotnet/Tests.cs index 717bdfa196..5f31e0c76a 100644 --- a/tests/e2e/languages/dotnet/Tests.cs +++ b/tests/e2e/languages/dotnet/Tests.cs @@ -251,6 +251,10 @@ public async Task Test1() Query.Equal("name", "Alice"), Query.GreaterThan("age", 18) })); + TestContext.WriteLine(Query.Relationship("comments", new List { + Query.Equal("approved", true), + Query.Limit(2) + })); // Permission & Roles helper tests TestContext.WriteLine(Permission.Read(Role.Any())); diff --git a/tests/e2e/languages/flutter/tests.dart b/tests/e2e/languages/flutter/tests.dart index 2a1e10ec75..65536a1c1c 100644 --- a/tests/e2e/languages/flutter/tests.dart +++ b/tests/e2e/languages/flutter/tests.dart @@ -382,6 +382,10 @@ void main() async { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + print(Query.relationship("comments", [ + Query.equal("approved", true), + Query.limit(2) + ])); // Permission & Role helper tests print(Permission.read(Role.any())); diff --git a/tests/e2e/languages/go-v2/tests.go b/tests/e2e/languages/go-v2/tests.go index 96f5a51dc4..805d43345b 100644 --- a/tests/e2e/languages/go-v2/tests.go +++ b/tests/e2e/languages/go-v2/tests.go @@ -303,6 +303,10 @@ func testQueries() { query.Equal("name", "Alice"), query.GreaterThan("age", 18), })) + fmt.Println(query.Relationship("comments", []string{ + query.Equal("approved", true), + query.Limit(2), + })) } func testPermissionHelpers() { diff --git a/tests/e2e/languages/go/tests.go b/tests/e2e/languages/go/tests.go index d729c468e3..5a2fba7a24 100644 --- a/tests/e2e/languages/go/tests.go +++ b/tests/e2e/languages/go/tests.go @@ -303,6 +303,10 @@ func testQueries() { query.Equal("name", "Alice"), query.GreaterThan("age", 18), })) + fmt.Println(query.Relationship("comments", []string{ + query.Equal("approved", true), + query.Limit(2), + })) } func testPermissionHelpers() { diff --git a/tests/e2e/languages/kotlin/Tests.kt b/tests/e2e/languages/kotlin/Tests.kt index f512c28507..cc061c07ba 100644 --- a/tests/e2e/languages/kotlin/Tests.kt +++ b/tests/e2e/languages/kotlin/Tests.kt @@ -270,6 +270,10 @@ class ServiceTest { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ))) + writeToFile(Query.relationship("comments", listOf( + Query.equal("approved", true), + Query.limit(2) + ))) // Permission & Roles helper tests writeToFile(Permission.read(Role.any())) diff --git a/tests/e2e/languages/node/test.js b/tests/e2e/languages/node/test.js index e79f3ff513..f96d1bc25c 100644 --- a/tests/e2e/languages/node/test.js +++ b/tests/e2e/languages/node/test.js @@ -361,6 +361,10 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.relationship("comments", [ + Query.equal("approved", true), + Query.limit(2) + ])); // Permission & Role helper tests console.log(Permission.read(Role.any())); diff --git a/tests/e2e/languages/php/test.php b/tests/e2e/languages/php/test.php index 23481eec3d..0bc2e17c78 100644 --- a/tests/e2e/languages/php/test.php +++ b/tests/e2e/languages/php/test.php @@ -366,6 +366,10 @@ public function toArray(): array Query::equal('name', ['Alice']), Query::greaterThan('age', 18) ]) . "\n"; +echo Query::relationship('comments', [ + Query::equal('approved', [true]), + Query::limit(2) +]) . "\n"; // Permission & Role helper tests echo Permission::read(Role::any()) . "\n"; diff --git a/tests/e2e/languages/python/tests.py b/tests/e2e/languages/python/tests.py index d4180a49b8..ee8568cd37 100644 --- a/tests/e2e/languages/python/tests.py +++ b/tests/e2e/languages/python/tests.py @@ -219,6 +219,10 @@ Query.equal("name", "Alice"), Query.greater_than("age", 18) ])) +print(Query.relationship("comments", [ + Query.equal("approved", True), + Query.limit(2) +])) # Permission & Role helper tests print(Permission.read(Role.any())) diff --git a/tests/e2e/languages/react-native/browser.js b/tests/e2e/languages/react-native/browser.js index 22162aa277..c5a8e0e98e 100644 --- a/tests/e2e/languages/react-native/browser.js +++ b/tests/e2e/languages/react-native/browser.js @@ -315,6 +315,10 @@ import { Query.equal('name', 'Alice'), Query.greaterThan('age', 18), ])); + console.log(Query.relationship('comments', [ + Query.equal('approved', true), + Query.limit(2), + ])); // Permission & Role helper tests console.log(Permission.read(Role.any())); diff --git a/tests/e2e/languages/ruby/tests.rb b/tests/e2e/languages/ruby/tests.rb index 6dd975900b..cc5c0a8b4d 100644 --- a/tests/e2e/languages/ruby/tests.rb +++ b/tests/e2e/languages/ruby/tests.rb @@ -227,6 +227,10 @@ Query.equal("name", "Alice"), Query.greater_than("age", 18) ]) +puts Query.relationship("comments", [ + Query.equal("approved", true), + Query.limit(2) +]) # Permission & Role helper tests puts Permission.read(Role.any()) diff --git a/tests/e2e/languages/swift/Tests.swift b/tests/e2e/languages/swift/Tests.swift index 0fa927654e..7ddd0161e4 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -265,6 +265,10 @@ class Tests: XCTestCase { Query.equal("name", value: "Alice"), Query.greaterThan("age", value: 18) ])) + print(Query.relationship("comments", queries: [ + Query.equal("approved", value: true), + Query.limit(2) + ])) // Permission & Role helper tests print(Permission.read(Role.any())) diff --git a/tests/e2e/languages/unity/Tests.cs b/tests/e2e/languages/unity/Tests.cs index 961c04019a..7c8b1e41d3 100644 --- a/tests/e2e/languages/unity/Tests.cs +++ b/tests/e2e/languages/unity/Tests.cs @@ -387,6 +387,10 @@ private async Task RunAsyncTest() Query.Equal("name", "Alice"), Query.GreaterThan("age", 18) })); + LogResult(Query.Relationship("comments", new List { + Query.Equal("approved", true), + Query.Limit(2) + })); // Permission & Roles helper tests LogResult(Permission.Read(Role.Any())); LogResult(Permission.Write(Role.User(ID.Custom("userid")))); diff --git a/tests/e2e/languages/web/index.html b/tests/e2e/languages/web/index.html index ebcc1ff538..d159cf3dc7 100644 --- a/tests/e2e/languages/web/index.html +++ b/tests/e2e/languages/web/index.html @@ -450,6 +450,10 @@ Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.relationship("comments", [ + Query.equal("approved", true), + Query.limit(2) + ])); // Permission & Role helper tests console.log(Permission.read(Role.any())); diff --git a/tests/e2e/languages/web/node.js b/tests/e2e/languages/web/node.js index b8b3b30a15..b0d04543b9 100644 --- a/tests/e2e/languages/web/node.js +++ b/tests/e2e/languages/web/node.js @@ -288,6 +288,10 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.relationship("comments", [ + Query.equal("approved", true), + Query.limit(2) + ])); // Permission & Role helper tests console.log(Permission.read(Role.any()));