From c6dbc47ae846b3f2494c79d15593b10bf1ba2792 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 16:07:01 +1200 Subject: [PATCH 01/42] feat: add Query builder, aggregates, joins, and query normalize Query helpers now cover the query-lib methods Appwrite allows (aggregates, groupBy, having, distinct, joins, extra spatial). Query.builder()/build() collects filters as a flat string list, and Client.normalize unwraps the common extra-array wrap of a builder or build()/page() result so list and POST /query calls stay valid. --- src/SDK/Language/PHP.php | 5 + .../src/main/java/io/package/Client.kt.twig | 7 +- .../src/main/java/io/package/Query.kt.twig | 265 ++++++ templates/apple/Sources/Client.swift.twig | 4 + .../generator/testdata/client.databases.ts | 21 + .../generator/testdata/client.types.ts | 29 +- .../generator/testdata/server.databases.ts | 21 + .../generator/testdata/server.types.ts | 41 +- .../cli/internal/generator/typescript.go | 2 +- .../typegen/templates/databases.ts.hbs | 21 + .../internal/typegen/templates/types.ts.hbs | 21 + templates/dart/lib/query.dart.twig | 476 ++++++++++ templates/dart/lib/src/client_mixin.dart.twig | 26 + templates/dart/test/query_test.dart.twig | 154 ++++ templates/deno/mod.ts.twig | 5 +- templates/deno/src/client.ts.twig | 5 + templates/deno/src/query.ts.twig | 544 ++++++++++++ templates/deno/test/query.test.ts.twig | 89 ++ templates/dotnet/Package/Client.cs.twig | 5 + templates/dotnet/Package/Query.cs.twig | 654 ++++++++++++++ .../flutter/lib/src/client_mixin.dart.twig | 26 + templates/go/client.go.twig | 7 + templates/go/query.go.twig | 560 ++++++++++++ templates/go/query_test.go.twig | 300 +++++++ .../main/kotlin/io/appwrite/Client.kt.twig | 7 +- .../src/main/kotlin/io/appwrite/Query.kt.twig | 265 ++++++ templates/node/src/client.ts.twig | 9 +- templates/node/src/index.ts.twig | 4 +- templates/node/test/query.test.js.twig | 89 ++ templates/php/src/Client.php.twig | 3 + templates/php/src/Query.php.twig | 229 +++++ templates/php/src/QueryBuilder.php.twig | 535 +++++++++++ templates/php/tests/QueryTest.php.twig | 148 ++++ templates/python/package/client.py.twig | 4 + templates/python/package/query.py.twig | 416 +++++++++ templates/python/test/test_query.py.twig | 34 +- templates/react-native/src/client.ts.twig | 4 + templates/react-native/src/index.ts.twig | 4 +- templates/react-native/src/query.ts.twig | 544 ++++++++++++ templates/ruby/lib/container/client.rb.twig | 6 + templates/ruby/lib/container/query.rb.twig | 514 +++++++++++ templates/rust/src/client.rs.twig | 18 +- templates/rust/src/query.rs.twig | 832 ++++++++++++++++++ templates/rust/tests/tests.rs | 6 + templates/skills/dart.md.twig | 38 + templates/skills/dotnet.md.twig | 31 + templates/skills/go.md.twig | 31 + templates/skills/kotlin.md.twig | 38 + templates/skills/php.md.twig | 31 + templates/skills/python.md.twig | 33 + templates/skills/ruby.md.twig | 31 + templates/skills/swift.md.twig | 31 + templates/skills/typescript.md.twig | 38 + templates/swift/Sources/Client.swift.twig | 4 + templates/swift/Sources/Query.swift.twig | 723 +++++++++++++++ templates/web/src/client.ts.twig | 15 +- templates/web/src/index.ts.twig | 4 +- templates/web/src/query.ts.twig | 544 ++++++++++++ templates/web/src/services/realtime.ts.twig | 28 +- tests/e2e/Base.php | 6 + tests/e2e/languages/android/Tests.kt | 6 + tests/e2e/languages/apple/Tests.swift | 6 + tests/e2e/languages/dart/tests.dart | 6 + tests/e2e/languages/deno/tests.ts | 6 + tests/e2e/languages/dotnet/Tests.cs | 6 + tests/e2e/languages/flutter/tests.dart | 6 + tests/e2e/languages/go-v2/tests.go | 6 + tests/e2e/languages/go/tests.go | 6 + tests/e2e/languages/kotlin/Tests.kt | 6 + tests/e2e/languages/node/test.js | 6 + tests/e2e/languages/php/test.php | 7 + tests/e2e/languages/python/tests.py | 6 + tests/e2e/languages/react-native/browser.js | 6 + tests/e2e/languages/ruby/tests.rb | 6 + tests/e2e/languages/swift/Tests.swift | 6 + tests/e2e/languages/unity/Tests.cs | 6 + tests/e2e/languages/web/index.html | 6 + tests/e2e/languages/web/node.js | 6 + 78 files changed, 8638 insertions(+), 56 deletions(-) create mode 100644 templates/php/src/QueryBuilder.php.twig diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 2181661beb..f889223604 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -237,6 +237,11 @@ public function getFiles(): array 'destination' => 'src/{{ namespace | caseNamespacePath }}/Query.php', 'template' => 'php/src/Query.php.twig', ], + [ + 'scope' => 'default', + 'destination' => 'src/{{ namespace | caseNamespacePath }}/QueryBuilder.php', + 'template' => 'php/src/QueryBuilder.php.twig', + ], [ 'scope' => 'default', 'destination' => 'tests/{{ namespace | caseNamespacePath }}/QueryTest.php', diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 5469e3647b..9aba4cf562 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -299,7 +299,12 @@ class Client @JvmOverloads constructor( responseType: Class, converter: ((Any) -> T)? = null ): T { - val filteredParams = params.filterValues { it != null } + val requestParams = if (params.containsKey("queries")) { + params + ("queries" to Query.normalize(params["queries"])) + } else { + params + } + val filteredParams = requestParams.filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) 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 74ad08d83a..e5d58d3062 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 @@ -512,6 +512,95 @@ class Query( */ fun notTouches(attribute: String, values: List) = Query("notTouches", attribute, listOf(values)).toJson() + fun count(attribute: String = "*", alias: String = "") = Query("count", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun countDistinct(attribute: String, alias: String = "") = Query("countDistinct", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun sum(attribute: String, alias: String = "") = Query("sum", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun avg(attribute: String, alias: String = "") = Query("avg", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun min(attribute: String, alias: String = "") = Query("min", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun max(attribute: String, alias: String = "") = Query("max", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun stddev(attribute: String, alias: String = "") = Query("stddev", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun stddevPop(attribute: String, alias: String = "") = Query("stddevPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun stddevSamp(attribute: String, alias: String = "") = Query("stddevSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun variance(attribute: String, alias: String = "") = Query("variance", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun varPop(attribute: String, alias: String = "") = Query("varPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun varSamp(attribute: String, alias: String = "") = Query("varSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun bitAnd(attribute: String, alias: String = "") = Query("bitAnd", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun bitOr(attribute: String, alias: String = "") = Query("bitOr", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun bitXor(attribute: String, alias: String = "") = Query("bitXor", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun groupBy(attributes: List) = Query("groupBy", null, attributes).toJson() + + fun having(queries: List) = Query("having", null, queries.map { it.fromJson() }).toJson() + + fun distinct() = Query("distinct").toJson() + + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + + fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() + + fun notCovers(attribute: String, values: List) = Query("notCovers", attribute, listOf(values)).toJson() + + fun spatialEquals(attribute: String, values: List) = Query("spatialEquals", attribute, listOf(values)).toJson() + + fun notSpatialEquals(attribute: String, values: List) = Query("notSpatialEquals", attribute, listOf(values)).toJson() + + fun page(page: Int, perPage: Int = 25): List { + require(page >= 1) { "Page must be >= 1, got $page" } + require(perPage >= 1) { "Per page must be >= 1, got $perPage" } + return listOf(limit(perPage), offset((page - 1) * perPage)) + } + + fun builder() = QueryBuilder() + + fun normalize(queries: Any?): List { + if (queries == null) { + return emptyList() + } + if (queries is QueryBuilder) { + return queries.build() + } + if (queries is Query) { + return listOf(queries.toString()) + } + if (queries is String) { + return listOf(queries) + } + if (queries is Array<*>) { + return queries.flatMap { normalize(it) } + } + if (queries is Collection<*>) { + return queries.flatMap { normalize(it) } + } + return emptyList() + } + + private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { + val values = if (alias.isNotEmpty()) listOf(left, operator, right, alias) else listOf(left, operator, right) + return Query(method, table, values).toJson() + } + /** * Parse the value to a list of values. * @@ -526,3 +615,179 @@ class Query( } } } + +class QueryBuilder { + private val queries = mutableListOf() + + fun build(): List = queries.toList() + + fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) + + fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) + + fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) + + fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) + + fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) + + fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) + + fun search(attribute: String, value: String) = push(Query.search(attribute, value)) + + fun isNull(attribute: String) = push(Query.isNull(attribute)) + + fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) + + fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) + + fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) + + fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) + + fun select(attributes: List) = push(Query.select(attributes)) + + fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) + + fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) + + fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) + + fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) + + fun orderRandom() = push(Query.orderRandom()) + + fun limit(limit: Int) = push(Query.limit(limit)) + + fun offset(offset: Int) = push(Query.offset(offset)) + + fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) + + fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) + + fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) + + fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) + + fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) + + fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) + + fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) + + fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) + + fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) + + fun exists(attributes: List) = push(Query.exists(attributes)) + + fun notExists(attributes: List) = push(Query.notExists(attributes)) + + fun createdBefore(value: String) = push(Query.createdBefore(value)) + + fun createdAfter(value: String) = push(Query.createdAfter(value)) + + fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) + + fun updatedBefore(value: String) = push(Query.updatedBefore(value)) + + fun updatedAfter(value: String) = push(Query.updatedAfter(value)) + + fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) + + fun or(queries: List) = push(Query.or(queries)) + + fun and(queries: List) = push(Query.and(queries)) + + fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) + + fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) + + fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) + + fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) + + fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) + + fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) + + fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) + + fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) + + fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) + + fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) + + fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) + + fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) + + fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) + + fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) + + fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) + + fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) + + fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) + + fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) + + fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) + + fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) + + fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) + + fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) + + fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) + + fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) + + fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) + + fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) + + fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) + + fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) + + fun groupBy(attributes: List) = push(Query.groupBy(attributes)) + + fun having(queries: List) = push(Query.having(queries)) + + fun distinct() = push(Query.distinct()) + + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) + + fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) + + fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) + + fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) + + fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) + + fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) + + fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) + + fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) + + fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) + + private fun push(query: String) = apply { queries.add(query) } + + private fun push(query: List) = apply { queries.addAll(query) } +} diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 0231776548..25fa2894ec 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -313,6 +313,10 @@ open class Client { sink: ((ByteBuffer) -> Void)? = nil, converter: ((Any) throws -> T)? = nil ) async throws -> T { + var params = params + if params.keys.contains("queries") { + params["queries"] = Query.normalize(params["queries"] as Any?) + } let validParams = params.filter { $0.value != nil } let queryParameters = method == "GET" && !validParams.isEmpty diff --git a/templates/cli/internal/generator/testdata/client.databases.ts b/templates/cli/internal/generator/testdata/client.databases.ts index 2d86c2007a..b0e5ba169a 100644 --- a/templates/cli/internal/generator/testdata/client.databases.ts +++ b/templates/cli/internal/generator/testdata/client.databases.ts @@ -25,6 +25,27 @@ const createQueryBuilder = (): QueryBuilder => ({ cursorBefore: (documentId) => Query.cursorBefore(documentId), or: (...queries) => Query.or(queries), and: (...queries) => Query.and(queries), + count: (attribute = '*', alias = '') => Query.count(attribute, alias), + countDistinct: (attribute, alias = '') => Query.countDistinct(attribute, alias), + sum: (attribute, alias = '') => Query.sum(attribute, alias), + avg: (attribute, alias = '') => Query.avg(attribute, alias), + min: (attribute, alias = '') => Query.min(attribute, alias), + max: (attribute, alias = '') => Query.max(attribute, alias), + groupBy: (attributes) => Query.groupBy(attributes), + having: (queries) => Query.having(queries), + distinct: () => Query.distinct(), + join: (table, left, right, operator = '=', alias = '') => Query.join(table, left, right, operator, alias), + leftJoin: (table, left, right, operator = '=', alias = '') => Query.leftJoin(table, left, right, operator, alias), + rightJoin: (table, left, right, operator = '=', alias = '') => Query.rightJoin(table, left, right, operator, alias), + fullOuterJoin: (table, left, right, operator = '=', alias = '') => Query.fullOuterJoin(table, left, right, operator, alias), + crossJoin: (table, alias = '') => Query.crossJoin(table, alias), + vectorDot: (attribute, vector) => Query.vectorDot(attribute, vector), + vectorCosine: (attribute, vector) => Query.vectorCosine(attribute, vector), + vectorEuclidean: (attribute, vector) => Query.vectorEuclidean(attribute, vector), + covers: (attribute, values) => Query.covers(attribute, values), + notCovers: (attribute, values) => Query.notCovers(attribute, values), + spatialEquals: (attribute, values) => Query.spatialEquals(attribute, values), + notSpatialEquals: (attribute, values) => Query.notSpatialEquals(attribute, values), }); const tableIdMap: Record> = Object.create(null); diff --git a/templates/cli/internal/generator/testdata/client.types.ts b/templates/cli/internal/generator/testdata/client.types.ts index f686e08720..f3c0c77fd2 100644 --- a/templates/cli/internal/generator/testdata/client.types.ts +++ b/templates/cli/internal/generator/testdata/client.types.ts @@ -111,6 +111,27 @@ export type QueryBuilder = { cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string; + count: (attribute?: string, alias?: string) => string; + countDistinct: (attribute: string, alias?: string) => string; + sum: (attribute: string, alias?: string) => string; + avg: (attribute: string, alias?: string) => string; + min: (attribute: string, alias?: string) => string; + max: (attribute: string, alias?: string) => string; + groupBy: (attributes: string[]) => string; + having: (queries: string[]) => string; + distinct: () => string; + join: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + leftJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + rightJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + fullOuterJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + crossJoin: (table: string, alias?: string) => string; + vectorDot: (attribute: string, vector: number[]) => string; + vectorCosine: (attribute: string, vector: number[]) => string; + vectorEuclidean: (attribute: string, vector: number[]) => string; + covers: (attribute: string, values: any[]) => string; + notCovers: (attribute: string, values: any[]) => string; + spatialEquals: (attribute: string, values: any[]) => string; + notSpatialEquals: (attribute: string, values: any[]) => string; } export type DatabaseId = "main" | "analytics"; @@ -140,7 +161,7 @@ export type DatabaseTableMap = { "books"?: ((BooksZinesCreate & { $id?: string; $permissions?: string[] }) | string)[] | null; }>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: Authors[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: Authors[] }>; }; "Books & Zines": { create: (data: { @@ -153,7 +174,7 @@ export type DatabaseTableMap = { "pages"?: number | null; }>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: BooksZines[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: BooksZines[] }>; } }; "analytics": { @@ -168,14 +189,14 @@ export type DatabaseTableMap = { "at": string; }>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: Events[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: Events[] }>; }; "Empty": { create: (data: Record, options?: { rowId?: string; permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; get: (id: string) => Promise; update: (id: string, data: Partial>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: Empty[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: Empty[] }>; } } }; diff --git a/templates/cli/internal/generator/testdata/server.databases.ts b/templates/cli/internal/generator/testdata/server.databases.ts index bbc31051db..e65f710035 100644 --- a/templates/cli/internal/generator/testdata/server.databases.ts +++ b/templates/cli/internal/generator/testdata/server.databases.ts @@ -25,6 +25,27 @@ const createQueryBuilder = (): QueryBuilder => ({ cursorBefore: (documentId) => Query.cursorBefore(documentId), or: (...queries) => Query.or(queries), and: (...queries) => Query.and(queries), + count: (attribute = '*', alias = '') => Query.count(attribute, alias), + countDistinct: (attribute, alias = '') => Query.countDistinct(attribute, alias), + sum: (attribute, alias = '') => Query.sum(attribute, alias), + avg: (attribute, alias = '') => Query.avg(attribute, alias), + min: (attribute, alias = '') => Query.min(attribute, alias), + max: (attribute, alias = '') => Query.max(attribute, alias), + groupBy: (attributes) => Query.groupBy(attributes), + having: (queries) => Query.having(queries), + distinct: () => Query.distinct(), + join: (table, left, right, operator = '=', alias = '') => Query.join(table, left, right, operator, alias), + leftJoin: (table, left, right, operator = '=', alias = '') => Query.leftJoin(table, left, right, operator, alias), + rightJoin: (table, left, right, operator = '=', alias = '') => Query.rightJoin(table, left, right, operator, alias), + fullOuterJoin: (table, left, right, operator = '=', alias = '') => Query.fullOuterJoin(table, left, right, operator, alias), + crossJoin: (table, alias = '') => Query.crossJoin(table, alias), + vectorDot: (attribute, vector) => Query.vectorDot(attribute, vector), + vectorCosine: (attribute, vector) => Query.vectorCosine(attribute, vector), + vectorEuclidean: (attribute, vector) => Query.vectorEuclidean(attribute, vector), + covers: (attribute, values) => Query.covers(attribute, values), + notCovers: (attribute, values) => Query.notCovers(attribute, values), + spatialEquals: (attribute, values) => Query.spatialEquals(attribute, values), + notSpatialEquals: (attribute, values) => Query.notSpatialEquals(attribute, values), }); const tableIdMap: Record> = Object.create(null); diff --git a/templates/cli/internal/generator/testdata/server.types.ts b/templates/cli/internal/generator/testdata/server.types.ts index 0245ce6a05..4d43095224 100644 --- a/templates/cli/internal/generator/testdata/server.types.ts +++ b/templates/cli/internal/generator/testdata/server.types.ts @@ -111,6 +111,27 @@ export type QueryBuilder = { cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string; + count: (attribute?: string, alias?: string) => string; + countDistinct: (attribute: string, alias?: string) => string; + sum: (attribute: string, alias?: string) => string; + avg: (attribute: string, alias?: string) => string; + min: (attribute: string, alias?: string) => string; + max: (attribute: string, alias?: string) => string; + groupBy: (attributes: string[]) => string; + having: (queries: string[]) => string; + distinct: () => string; + join: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + leftJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + rightJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + fullOuterJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + crossJoin: (table: string, alias?: string) => string; + vectorDot: (attribute: string, vector: number[]) => string; + vectorCosine: (attribute: string, vector: number[]) => string; + vectorEuclidean: (attribute: string, vector: number[]) => string; + covers: (attribute: string, values: any[]) => string; + notCovers: (attribute: string, values: any[]) => string; + spatialEquals: (attribute: string, values: any[]) => string; + notSpatialEquals: (attribute: string, values: any[]) => string; } export type DatabaseId = "main" | "analytics"; @@ -140,7 +161,7 @@ export type DatabaseTableMap = { "books"?: ((BooksZinesCreate & { $id?: string; $permissions?: string[] }) | string)[] | null; }>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: Authors[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: Authors[] }>; }; "Books & Zines": { create: (data: { @@ -153,7 +174,7 @@ export type DatabaseTableMap = { "pages"?: number | null; }>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: BooksZines[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: BooksZines[] }>; createMany: (rows: Array<{ "title": string; "pages"?: number | null; @@ -161,8 +182,8 @@ export type DatabaseTableMap = { updateMany: (data: Partial<{ "title": string; "pages"?: number | null; - }>, options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[]; transactionId?: string }) => Promise<{ total: number; rows: BooksZines[] }>; - deleteMany: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[]; transactionId?: string }) => Promise<{ total: number; rows: BooksZines[] }>; + }>, options?: { queries?: (q: QueryBuilder) => string[]; transactionId?: string }) => Promise<{ total: number; rows: BooksZines[] }>; + deleteMany: (options?: { queries?: (q: QueryBuilder) => string[]; transactionId?: string }) => Promise<{ total: number; rows: BooksZines[] }>; } }; "analytics": { @@ -177,7 +198,7 @@ export type DatabaseTableMap = { "at": string; }>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: Events[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: Events[] }>; createMany: (rows: Array<{ "name": string; "at": string; @@ -185,18 +206,18 @@ export type DatabaseTableMap = { updateMany: (data: Partial<{ "name": string; "at": string; - }>, options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Events[] }>; - deleteMany: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Events[] }>; + }>, options?: { queries?: (q: QueryBuilder) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Events[] }>; + deleteMany: (options?: { queries?: (q: QueryBuilder) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Events[] }>; }; "Empty": { create: (data: Record, options?: { rowId?: string; permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; get: (id: string) => Promise; update: (id: string, data: Partial>, options?: { permissions?: (permission: { read: (role: RoleString) => string; write: (role: RoleString) => string; create: (role: RoleString) => string; update: (role: RoleString) => string; delete: (role: RoleString) => string }, role: { any: () => RoleString; user: (userId: string, status?: string) => RoleString; users: (status?: string) => RoleString; guests: () => RoleString; team: (teamId: string, role?: string) => RoleString; member: (memberId: string) => RoleString; label: (label: string) => RoleString }) => string[]; transactionId?: string }) => Promise; delete: (id: string, options?: { transactionId?: string }) => Promise; - list: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[] }) => Promise<{ total: number; rows: Empty[] }>; + list: (options?: { queries?: (q: QueryBuilder) => string[] }) => Promise<{ total: number; rows: Empty[] }>; createMany: (rows: Array & { $id?: string; $permissions?: string[] }>, options?: { transactionId?: string }) => Promise<{ total: number; rows: Empty[] }>; - updateMany: (data: Partial>, options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Empty[] }>; - deleteMany: (options?: { queries?: (q: { equal: >(field: K, value: QueryableFieldValue) => string; notEqual: >(field: K, value: QueryableFieldValue) => string; lessThan: >(field: K, value: QueryableFieldValue) => string; lessThanEqual: >(field: K, value: QueryableFieldValue) => string; greaterThan: >(field: K, value: QueryableFieldValue) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue) => string; contains: >(field: K, value: QueryableFieldValue) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue, end: QueryableFieldValue) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Empty[] }>; + updateMany: (data: Partial>, options?: { queries?: (q: QueryBuilder) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Empty[] }>; + deleteMany: (options?: { queries?: (q: QueryBuilder) => string[]; transactionId?: string }) => Promise<{ total: number; rows: Empty[] }>; } } }; diff --git a/templates/cli/internal/generator/typescript.go b/templates/cli/internal/generator/typescript.go index 0dbf71c800..2d3cd50577 100644 --- a/templates/cli/internal/generator/typescript.go +++ b/templates/cli/internal/generator/typescript.go @@ -34,7 +34,7 @@ const permissionCallbackInline = `(permission: { read: (role: RoleString) => str // queryCallbackInline is the same idea for the query callback, parameterised by // the row type so field names are checked. func queryCallbackInline(typeName string) string { - return `(q: { equal: >(field: K, value: QueryableFieldValue<` + typeName + `, K>) => string; notEqual: >(field: K, value: QueryableFieldValue<` + typeName + `, K>) => string; lessThan: >(field: K, value: QueryableFieldValue<` + typeName + `, K>) => string; lessThanEqual: >(field: K, value: QueryableFieldValue<` + typeName + `, K>) => string; greaterThan: >(field: K, value: QueryableFieldValue<` + typeName + `, K>) => string; greaterThanEqual: >(field: K, value: QueryableFieldValue<` + typeName + `, K>) => string; contains: >(field: K, value: QueryableFieldValue<` + typeName + `, K>) => string; search: >(field: K, value: string) => string; isNull: >(field: K) => string; isNotNull: >(field: K) => string; startsWith: >(field: K, value: string) => string; endsWith: >(field: K, value: string) => string; between: >(field: K, start: QueryableFieldValue<` + typeName + `, K>, end: QueryableFieldValue<` + typeName + `, K>) => string; select: (fields: K[]) => string; orderAsc: (field: K) => string; orderDesc: (field: K) => string; limit: (value: number) => string; offset: (value: number) => string; cursorAfter: (documentId: string) => string; cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string }) => string[]` + return `(q: QueryBuilder<` + typeName + `>) => string[]` } // TypeScript generates a typed SDK for a TypeScript project. diff --git a/templates/cli/internal/typegen/templates/databases.ts.hbs b/templates/cli/internal/typegen/templates/databases.ts.hbs index 3cbb215c76..661ffab65b 100644 --- a/templates/cli/internal/typegen/templates/databases.ts.hbs +++ b/templates/cli/internal/typegen/templates/databases.ts.hbs @@ -29,6 +29,27 @@ const createQueryBuilder = (): QueryBuilder => ({ cursorBefore: (documentId) => Query.cursorBefore(documentId), or: (...queries) => Query.or(queries), and: (...queries) => Query.and(queries), + count: (attribute = '*', alias = '') => Query.count(attribute, alias), + countDistinct: (attribute, alias = '') => Query.countDistinct(attribute, alias), + sum: (attribute, alias = '') => Query.sum(attribute, alias), + avg: (attribute, alias = '') => Query.avg(attribute, alias), + min: (attribute, alias = '') => Query.min(attribute, alias), + max: (attribute, alias = '') => Query.max(attribute, alias), + groupBy: (attributes) => Query.groupBy(attributes), + having: (queries) => Query.having(queries), + distinct: () => Query.distinct(), + join: (table, left, right, operator = '=', alias = '') => Query.join(table, left, right, operator, alias), + leftJoin: (table, left, right, operator = '=', alias = '') => Query.leftJoin(table, left, right, operator, alias), + rightJoin: (table, left, right, operator = '=', alias = '') => Query.rightJoin(table, left, right, operator, alias), + fullOuterJoin: (table, left, right, operator = '=', alias = '') => Query.fullOuterJoin(table, left, right, operator, alias), + crossJoin: (table, alias = '') => Query.crossJoin(table, alias), + vectorDot: (attribute, vector) => Query.vectorDot(attribute, vector), + vectorCosine: (attribute, vector) => Query.vectorCosine(attribute, vector), + vectorEuclidean: (attribute, vector) => Query.vectorEuclidean(attribute, vector), + covers: (attribute, values) => Query.covers(attribute, values), + notCovers: (attribute, values) => Query.notCovers(attribute, values), + spatialEquals: (attribute, values) => Query.spatialEquals(attribute, values), + notSpatialEquals: (attribute, values) => Query.notSpatialEquals(attribute, values), }); {{{TABLE_ID_MAP}}} diff --git a/templates/cli/internal/typegen/templates/types.ts.hbs b/templates/cli/internal/typegen/templates/types.ts.hbs index ba0937dd1f..16c70d7135 100644 --- a/templates/cli/internal/typegen/templates/types.ts.hbs +++ b/templates/cli/internal/typegen/templates/types.ts.hbs @@ -61,6 +61,27 @@ export type QueryBuilder = { cursorBefore: (documentId: string) => string; or: (...queries: string[]) => string; and: (...queries: string[]) => string; + count: (attribute?: string, alias?: string) => string; + countDistinct: (attribute: string, alias?: string) => string; + sum: (attribute: string, alias?: string) => string; + avg: (attribute: string, alias?: string) => string; + min: (attribute: string, alias?: string) => string; + max: (attribute: string, alias?: string) => string; + groupBy: (attributes: string[]) => string; + having: (queries: string[]) => string; + distinct: () => string; + join: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + leftJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + rightJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + fullOuterJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + crossJoin: (table: string, alias?: string) => string; + vectorDot: (attribute: string, vector: number[]) => string; + vectorCosine: (attribute: string, vector: number[]) => string; + vectorEuclidean: (attribute: string, vector: number[]) => string; + covers: (attribute: string, values: any[]) => string; + notCovers: (attribute: string, values: any[]) => string; + spatialEquals: (attribute: string, values: any[]) => string; + notSpatialEquals: (attribute: string, values: any[]) => string; } export type DatabaseId = {{{databaseIdType}}}; diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index c117b33a1f..e852e72fb9 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -307,4 +307,480 @@ class Query { /// Filter resources where [attribute] does not touch the given geometry. static String notTouches(String attribute, List values) => Query._('notTouches', attribute, [values]).toString(); + + /// Count documents, optionally aliased. + static String count([String attribute = '*', String alias = '']) => + Query._('count', attribute, alias.isNotEmpty ? alias : null).toString(); + + /// Count distinct values of [attribute], optionally aliased. + static String countDistinct(String attribute, [String alias = '']) => + Query._('countDistinct', attribute, alias.isNotEmpty ? alias : null) + .toString(); + + static String sum(String attribute, [String alias = '']) => + Query._('sum', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String avg(String attribute, [String alias = '']) => + Query._('avg', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String min(String attribute, [String alias = '']) => + Query._('min', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String max(String attribute, [String alias = '']) => + Query._('max', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String stddev(String attribute, [String alias = '']) => + Query._('stddev', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String stddevPop(String attribute, [String alias = '']) => + Query._('stddevPop', attribute, alias.isNotEmpty ? alias : null) + .toString(); + + static String stddevSamp(String attribute, [String alias = '']) => + Query._('stddevSamp', attribute, alias.isNotEmpty ? alias : null) + .toString(); + + static String variance(String attribute, [String alias = '']) => + Query._('variance', attribute, alias.isNotEmpty ? alias : null) + .toString(); + + static String varPop(String attribute, [String alias = '']) => + Query._('varPop', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String varSamp(String attribute, [String alias = '']) => + Query._('varSamp', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String bitAnd(String attribute, [String alias = '']) => + Query._('bitAnd', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String bitOr(String attribute, [String alias = '']) => + Query._('bitOr', attribute, alias.isNotEmpty ? alias : null).toString(); + + static String bitXor(String attribute, [String alias = '']) => + Query._('bitXor', attribute, alias.isNotEmpty ? alias : null).toString(); + + /// Group results by [attributes]. + static String groupBy(List attributes) => + Query._('groupBy', null, attributes).toString(); + + /// Filter grouped results. [queries] are nested like [and]/[or]. + static String having(List queries) => Query._( + 'having', + null, + queries.map((query) => jsonDecode(query)).toList(), + ).toString(); + + static String distinct() => Query._('distinct').toString(); + + /// Inner join. [table] is the joined collection/table ID. + static String join( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + Query._joinQuery('join', table, left, right, operator, alias); + + static String leftJoin( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + Query._joinQuery('leftJoin', table, left, right, operator, alias); + + static String rightJoin( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + Query._joinQuery('rightJoin', table, left, right, operator, alias); + + static String fullOuterJoin( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + Query._joinQuery('fullOuterJoin', table, left, right, operator, alias); + + static String crossJoin(String table, [String alias = '']) => + Query._('crossJoin', table, alias.isNotEmpty ? alias : null).toString(); + + static String covers(String attribute, List values) => + Query._('covers', attribute, [values]).toString(); + + static String notCovers(String attribute, List values) => + Query._('notCovers', attribute, [values]).toString(); + + static String spatialEquals(String attribute, List values) => + Query._('spatialEquals', attribute, [values]).toString(); + + static String notSpatialEquals(String attribute, List values) => + Query._('notSpatialEquals', attribute, [values]).toString(); + + /// Return limit and offset queries for 1-based page pagination. + static List page(int page, [int perPage = 25]) { + if (page < 1) { + throw ArgumentError('Page must be >= 1, got $page'); + } + if (perPage < 1) { + throw ArgumentError('Per page must be >= 1, got $perPage'); + } + + return [ + Query.limit(perPage), + Query.offset((page - 1) * perPage), + ]; + } + + static QueryBuilder builder() => QueryBuilder(); + + /// Flatten a queries argument into query strings. + /// + /// Accepts a [QueryBuilder], a list of query strings, builders, or nested + /// lists (the common `[builder]` / `[builder.build()]` mistakes). + static List normalize(dynamic queries) { + if (queries is QueryBuilder) { + return queries.build(); + } + if (queries is Query) { + return [queries.toString()]; + } + if (queries is String) { + return [queries]; + } + if (queries is! List) { + return []; + } + + final out = []; + for (final item in queries) { + out.addAll(Query.normalize(item)); + } + return out; + } + + static String _joinQuery( + String method, + String table, + String left, + String right, + String operator, + String alias, + ) { + final values = [left, operator, right]; + if (alias.isNotEmpty) { + values.add(alias); + } + + return Query._(method, table, values).toString(); + } +} + +/// Fluent collector of query strings. Call [build] to produce the list +/// expected by list methods. +class QueryBuilder { + final List _queries = []; + + List build() => List.from(_queries); + + QueryBuilder equal(String attribute, dynamic value) => + _push(Query.equal(attribute, value)); + + QueryBuilder notEqual(String attribute, dynamic value) => + _push(Query.notEqual(attribute, value)); + + QueryBuilder lessThan(String attribute, dynamic value) => + _push(Query.lessThan(attribute, value)); + + QueryBuilder lessThanEqual(String attribute, dynamic value) => + _push(Query.lessThanEqual(attribute, value)); + + QueryBuilder greaterThan(String attribute, dynamic value) => + _push(Query.greaterThan(attribute, value)); + + QueryBuilder greaterThanEqual(String attribute, dynamic value) => + _push(Query.greaterThanEqual(attribute, value)); + + QueryBuilder search(String attribute, String value) => + _push(Query.search(attribute, value)); + + QueryBuilder isNull(String attribute) => _push(Query.isNull(attribute)); + + QueryBuilder isNotNull(String attribute) => + _push(Query.isNotNull(attribute)); + + QueryBuilder between(String attribute, dynamic start, dynamic end) => + _push(Query.between(attribute, start, end)); + + QueryBuilder startsWith(String attribute, String value) => + _push(Query.startsWith(attribute, value)); + + QueryBuilder endsWith(String attribute, String value) => + _push(Query.endsWith(attribute, value)); + + QueryBuilder select(List attributes) => + _push(Query.select(attributes)); + + QueryBuilder cursorAfter(String documentId) => + _push(Query.cursorAfter(documentId)); + + QueryBuilder cursorBefore(String documentId) => + _push(Query.cursorBefore(documentId)); + + QueryBuilder orderAsc(String attribute) => + _push(Query.orderAsc(attribute)); + + QueryBuilder orderDesc(String attribute) => + _push(Query.orderDesc(attribute)); + + QueryBuilder orderRandom() => _push(Query.orderRandom()); + + QueryBuilder limit(int limit) => _push(Query.limit(limit)); + + QueryBuilder offset(int offset) => _push(Query.offset(offset)); + + QueryBuilder contains(String attribute, dynamic value) => + _push(Query.contains(attribute, value)); + + QueryBuilder containsAny(String attribute, List value) => + _push(Query.containsAny(attribute, value)); + + QueryBuilder containsAll(String attribute, List value) => + _push(Query.containsAll(attribute, value)); + + QueryBuilder notContains(String attribute, dynamic value) => + _push(Query.notContains(attribute, value)); + + QueryBuilder notSearch(String attribute, String value) => + _push(Query.notSearch(attribute, value)); + + QueryBuilder notBetween(String attribute, dynamic start, dynamic end) => + _push(Query.notBetween(attribute, start, end)); + + QueryBuilder notStartsWith(String attribute, String value) => + _push(Query.notStartsWith(attribute, value)); + + QueryBuilder notEndsWith(String attribute, String value) => + _push(Query.notEndsWith(attribute, value)); + + QueryBuilder regex(String attribute, String pattern) => + _push(Query.regex(attribute, pattern)); + + QueryBuilder exists(List attributes) => + _push(Query.exists(attributes)); + + QueryBuilder notExists(List attributes) => + _push(Query.notExists(attributes)); + + QueryBuilder createdBefore(String value) => + _push(Query.createdBefore(value)); + + QueryBuilder createdAfter(String value) => + _push(Query.createdAfter(value)); + + QueryBuilder createdBetween(String start, String end) => + _push(Query.createdBetween(start, end)); + + QueryBuilder updatedBefore(String value) => + _push(Query.updatedBefore(value)); + + QueryBuilder updatedAfter(String value) => + _push(Query.updatedAfter(value)); + + QueryBuilder updatedBetween(String start, String end) => + _push(Query.updatedBetween(start, end)); + + QueryBuilder or(List queries) => _push(Query.or(queries)); + + QueryBuilder and(List queries) => _push(Query.and(queries)); + + QueryBuilder elemMatch(String attribute, List queries) => + _push(Query.elemMatch(attribute, queries)); + + QueryBuilder distanceEqual( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceEqual(attribute, values, distance, meters)); + + QueryBuilder distanceNotEqual( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceNotEqual(attribute, values, distance, meters)); + + QueryBuilder distanceGreaterThan( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceGreaterThan(attribute, values, distance, meters)); + + QueryBuilder distanceLessThan( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceLessThan(attribute, values, distance, meters)); + + QueryBuilder intersects(String attribute, List values) => + _push(Query.intersects(attribute, values)); + + QueryBuilder notIntersects(String attribute, List values) => + _push(Query.notIntersects(attribute, values)); + + QueryBuilder crosses(String attribute, List values) => + _push(Query.crosses(attribute, values)); + + QueryBuilder notCrosses(String attribute, List values) => + _push(Query.notCrosses(attribute, values)); + + QueryBuilder overlaps(String attribute, List values) => + _push(Query.overlaps(attribute, values)); + + QueryBuilder notOverlaps(String attribute, List values) => + _push(Query.notOverlaps(attribute, values)); + + QueryBuilder touches(String attribute, List values) => + _push(Query.touches(attribute, values)); + + QueryBuilder notTouches(String attribute, List values) => + _push(Query.notTouches(attribute, values)); + + QueryBuilder count([String attribute = '*', String alias = '']) => + _push(Query.count(attribute, alias)); + + QueryBuilder countDistinct(String attribute, [String alias = '']) => + _push(Query.countDistinct(attribute, alias)); + + QueryBuilder sum(String attribute, [String alias = '']) => + _push(Query.sum(attribute, alias)); + + QueryBuilder avg(String attribute, [String alias = '']) => + _push(Query.avg(attribute, alias)); + + QueryBuilder min(String attribute, [String alias = '']) => + _push(Query.min(attribute, alias)); + + QueryBuilder max(String attribute, [String alias = '']) => + _push(Query.max(attribute, alias)); + + QueryBuilder stddev(String attribute, [String alias = '']) => + _push(Query.stddev(attribute, alias)); + + QueryBuilder stddevPop(String attribute, [String alias = '']) => + _push(Query.stddevPop(attribute, alias)); + + QueryBuilder stddevSamp(String attribute, [String alias = '']) => + _push(Query.stddevSamp(attribute, alias)); + + QueryBuilder variance(String attribute, [String alias = '']) => + _push(Query.variance(attribute, alias)); + + QueryBuilder varPop(String attribute, [String alias = '']) => + _push(Query.varPop(attribute, alias)); + + QueryBuilder varSamp(String attribute, [String alias = '']) => + _push(Query.varSamp(attribute, alias)); + + QueryBuilder bitAnd(String attribute, [String alias = '']) => + _push(Query.bitAnd(attribute, alias)); + + QueryBuilder bitOr(String attribute, [String alias = '']) => + _push(Query.bitOr(attribute, alias)); + + QueryBuilder bitXor(String attribute, [String alias = '']) => + _push(Query.bitXor(attribute, alias)); + + QueryBuilder groupBy(List attributes) => + _push(Query.groupBy(attributes)); + + QueryBuilder having(List queries) => _push(Query.having(queries)); + + QueryBuilder distinct() => _push(Query.distinct()); + + QueryBuilder join( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + _push(Query.join(table, left, right, operator, alias)); + + QueryBuilder leftJoin( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + _push(Query.leftJoin(table, left, right, operator, alias)); + + QueryBuilder rightJoin( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + _push(Query.rightJoin(table, left, right, operator, alias)); + + QueryBuilder fullOuterJoin( + String table, + String left, + String right, [ + String operator = '=', + String alias = '', + ]) => + _push(Query.fullOuterJoin(table, left, right, operator, alias)); + + QueryBuilder crossJoin(String table, [String alias = '']) => + _push(Query.crossJoin(table, alias)); + + QueryBuilder vectorDot(String attribute, List vector) => + _push(Query.vectorDot(attribute, vector)); + + QueryBuilder vectorCosine(String attribute, List vector) => + _push(Query.vectorCosine(attribute, vector)); + + QueryBuilder vectorEuclidean(String attribute, List vector) => + _push(Query.vectorEuclidean(attribute, vector)); + + QueryBuilder covers(String attribute, List values) => + _push(Query.covers(attribute, values)); + + QueryBuilder notCovers(String attribute, List values) => + _push(Query.notCovers(attribute, values)); + + QueryBuilder spatialEquals(String attribute, List values) => + _push(Query.spatialEquals(attribute, values)); + + QueryBuilder notSpatialEquals(String attribute, List values) => + _push(Query.notSpatialEquals(attribute, values)); + + QueryBuilder page(int page, [int perPage = 25]) => + _push(Query.page(page, perPage)); + + QueryBuilder _push(Object query) { + if (query is List) { + _queries.addAll(query.cast()); + } else { + _queries.add(query as String); + } + return this; + } } diff --git a/templates/dart/lib/src/client_mixin.dart.twig b/templates/dart/lib/src/client_mixin.dart.twig index 66ec831560..d93fea9473 100644 --- a/templates/dart/lib/src/client_mixin.dart.twig +++ b/templates/dart/lib/src/client_mixin.dart.twig @@ -12,6 +12,11 @@ mixin ClientMixin { required Map headers, required Map params, }) { + if (params.containsKey('queries')) { + params = Map.from(params); + params['queries'] = _flattenQueries(params['queries']); + } + http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { request = http.MultipartRequest(method.name(), uri); @@ -141,3 +146,24 @@ mixin ClientMixin { } } } + +List _flattenQueries(dynamic queries) { + if (queries == null) { + return []; + } + if (queries is String) { + return [queries]; + } + if (queries is List) { + final out = []; + for (final item in queries) { + out.addAll(_flattenQueries(item)); + } + return out; + } + try { + return _flattenQueries(queries.build()); + } catch (_) { + return [queries.toString()]; + } +} diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index cfe0f76e98..7905f16255 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -357,4 +357,158 @@ void main() { expect(query['values'], ['2023-01-01', '2023-12-31']); expect(query['method'], 'between'); }); + + test('returns count with alias', () { + final query = jsonDecode(Query.count('*', 'total')); + expect(query['method'], 'count'); + expect(query['attribute'], '*'); + expect(query['values'], ['total']); + }); + + test('returns count without alias', () { + final query = jsonDecode(Query.count()); + expect(query['method'], 'count'); + expect(query['attribute'], '*'); + expect(query.containsKey('values'), isFalse); + }); + + test('returns countDistinct', () { + final query = jsonDecode(Query.countDistinct('year', 'uniqueYears')); + expect(query['method'], 'countDistinct'); + expect(query['attribute'], 'year'); + expect(query['values'], ['uniqueYears']); + }); + + test('returns sum avg min max', () { + expect(jsonDecode(Query.sum('price', 'total'))['method'], 'sum'); + expect(jsonDecode(Query.avg('price', 'avgPrice'))['method'], 'avg'); + expect(jsonDecode(Query.min('price', 'lowest'))['method'], 'min'); + expect(jsonDecode(Query.max('price', 'highest'))['method'], 'max'); + }); + + test('returns groupBy', () { + final query = jsonDecode(Query.groupBy(['status', 'year'])); + expect(query['method'], 'groupBy'); + expect(query['values'], ['status', 'year']); + expect(query.containsKey('attribute'), isFalse); + }); + + test('returns having', () { + final query = jsonDecode(Query.having([Query.greaterThan('total', 1)])); + expect(query['method'], 'having'); + expect(query['values'][0]['method'], 'greaterThan'); + expect(query['values'][0]['attribute'], 'total'); + expect(query['values'][0]['values'], [1]); + }); + + test('returns distinct', () { + final query = jsonDecode(Query.distinct()); + expect(query['method'], 'distinct'); + expect(query.containsKey('attribute'), isFalse); + expect(query.containsKey('values'), isFalse); + }); + + test('returns join', () { + final query = jsonDecode(Query.join('orders', '\$id', 'customerId')); + expect(query['method'], 'join'); + expect(query['attribute'], 'orders'); + expect(query['values'], ['\$id', '=', 'customerId']); + }); + + test('returns leftJoin with alias', () { + final query = jsonDecode( + Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord'), + ); + expect(query['method'], 'leftJoin'); + expect(query['attribute'], 'orders'); + expect(query['values'], ['\$id', '=', 'customerId', 'ord']); + }); + + test('returns crossJoin', () { + final query = jsonDecode(Query.crossJoin('orders', 'ord')); + expect(query['method'], 'crossJoin'); + expect(query['attribute'], 'orders'); + expect(query['values'], ['ord']); + }); + + test('returns vectorDot', () { + final query = jsonDecode(Query.vectorDot('embedding', [0.1, 0.2])); + expect(query['method'], 'vectorDot'); + expect(query['attribute'], 'embedding'); + expect(query['values'], [ + [0.1, 0.2] + ]); + }); + + test('returns covers', () { + final query = jsonDecode(Query.covers('location', [0, 0])); + expect(query['method'], 'covers'); + expect(query['values'], [ + [0, 0] + ]); + }); + + test('returns page', () { + final queries = Query.page(2, 10); + expect(queries.length, 2); + final limit = jsonDecode(queries[0]); + final offset = jsonDecode(queries[1]); + expect(limit['method'], 'limit'); + expect(limit['values'], [10]); + expect(offset['method'], 'offset'); + expect(offset['values'], [10]); + }); + + test('page rejects invalid', () { + expect(() => Query.page(0, 10), throwsA(isA())); + expect(() => Query.page(1, 0), throwsA(isA())); + }); + + test('builder build', () { + final expected = [ + Query.equal('status', 'published'), + Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord'), + Query.count('*', 'total'), + Query.groupBy(['status']), + Query.limit(25), + ]; + + final built = Query.builder() + .equal('status', 'published') + .leftJoin('orders', '\$id', 'customerId', '=', 'ord') + .count('*', 'total') + .groupBy(['status']) + .limit(25) + .build(); + + expect(built, expected); + }); + + test('normalize builder direct', () { + final builder = Query.builder().equal('status', 'active').limit(10); + expect(Query.normalize(builder), builder.build()); + }); + + test('normalize builder wrapped in array', () { + final builder = Query.builder().equal('status', 'active').limit(10); + expect(Query.normalize([builder]), builder.build()); + }); + + test('normalize build result wrapped in array', () { + final built = Query.builder().equal('status', 'active').limit(10).build(); + expect(Query.normalize([built]), built); + }); + + test('normalize mixed and page', () { + final normalized = Query.normalize([ + Query.equal('status', 'active'), + Query.page(2, 10), + ]); + + expect(normalized, [ + Query.equal('status', 'active'), + Query.limit(10), + Query.offset(10), + ]); + }); } diff --git a/templates/deno/mod.ts.twig b/templates/deno/mod.ts.twig index 54e59c3d65..b1564bf717 100644 --- a/templates/deno/mod.ts.twig +++ b/templates/deno/mod.ts.twig @@ -1,5 +1,5 @@ import { Client } from "./src/client.ts"; -import { Query } from "./src/query.ts"; +import { Query, QueryBuilder } from "./src/query.ts"; import { Permission } from "./src/permission.ts"; import { Role } from "./src/role.ts"; import { ID } from "./src/id.ts"; @@ -16,6 +16,7 @@ import { {{enum.title | caseUcfirst}} } from "./src/enums/{{enum.title | caseKeb export { Client, Query, + QueryBuilder, Permission, Role, ID, @@ -32,4 +33,4 @@ export { }; export type { Models } from "./src/models.d.ts"; -export type { QueryTypes, QueryTypesList } from "./src/query.ts"; +export type { QueryTypes, QueryTypesList, QueryInput } from "./src/query.ts"; diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 7f670efafb..0631484fda 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -1,4 +1,5 @@ import { {{ spec.info.title | caseUcfirst}}Exception } from './exception.ts'; +import { Query } from './query.ts'; export interface Payload { [key: string]: any; @@ -81,6 +82,10 @@ export class Client { } async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}, responseType: string = "json") { + if (params.queries) { + params.queries = Query.normalize(params.queries); + } + headers = {...this.headers, ...headers}; const url = new URL(this.endpoint + path); diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index e5d829dd4f..8925695e30 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -445,4 +445,548 @@ export class Query { */ static notTouches = (attribute: string, values: any[]): string => new Query("notTouches", attribute, [values]).toString(); + + /** + * Count documents, optionally aliased. + */ + static count = (attribute: string = "*", alias: string = ""): string => + new Query("count", attribute, alias !== "" ? alias : undefined).toString(); + + /** + * Count distinct values of attribute, optionally aliased. + */ + static countDistinct = (attribute: string, alias: string = ""): string => + new Query("countDistinct", attribute, alias !== "" ? alias : undefined).toString(); + + static sum = (attribute: string, alias: string = ""): string => + new Query("sum", attribute, alias !== "" ? alias : undefined).toString(); + + static avg = (attribute: string, alias: string = ""): string => + new Query("avg", attribute, alias !== "" ? alias : undefined).toString(); + + static min = (attribute: string, alias: string = ""): string => + new Query("min", attribute, alias !== "" ? alias : undefined).toString(); + + static max = (attribute: string, alias: string = ""): string => + new Query("max", attribute, alias !== "" ? alias : undefined).toString(); + + static stddev = (attribute: string, alias: string = ""): string => + new Query("stddev", attribute, alias !== "" ? alias : undefined).toString(); + + static stddevPop = (attribute: string, alias: string = ""): string => + new Query("stddevPop", attribute, alias !== "" ? alias : undefined).toString(); + + static stddevSamp = (attribute: string, alias: string = ""): string => + new Query("stddevSamp", attribute, alias !== "" ? alias : undefined).toString(); + + static variance = (attribute: string, alias: string = ""): string => + new Query("variance", attribute, alias !== "" ? alias : undefined).toString(); + + static varPop = (attribute: string, alias: string = ""): string => + new Query("varPop", attribute, alias !== "" ? alias : undefined).toString(); + + static varSamp = (attribute: string, alias: string = ""): string => + new Query("varSamp", attribute, alias !== "" ? alias : undefined).toString(); + + static bitAnd = (attribute: string, alias: string = ""): string => + new Query("bitAnd", attribute, alias !== "" ? alias : undefined).toString(); + + static bitOr = (attribute: string, alias: string = ""): string => + new Query("bitOr", attribute, alias !== "" ? alias : undefined).toString(); + + static bitXor = (attribute: string, alias: string = ""): string => + new Query("bitXor", attribute, alias !== "" ? alias : undefined).toString(); + + /** + * Group results by attributes. + */ + static groupBy = (attributes: string[]): string => + new Query("groupBy", undefined, attributes).toString(); + + /** + * Filter grouped results. `queries` are nested like `and`/`or`. + */ + static having = (queries: string[]): string => + new Query( + "having", + undefined, + queries.map((query) => JSON.parse(query)) + ).toString(); + + static distinct = (): string => + new Query("distinct").toString(); + + /** + * Inner join. `table` is the joined collection/table ID. + */ + static join = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "join", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static leftJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "leftJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static rightJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "rightJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static fullOuterJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "fullOuterJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static crossJoin = (table: string, alias: string = ""): string => + new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); + + static covers = (attribute: string, values: any[]): string => + new Query("covers", attribute, [values]).toString(); + + static notCovers = (attribute: string, values: any[]): string => + new Query("notCovers", attribute, [values]).toString(); + + static spatialEquals = (attribute: string, values: any[]): string => + new Query("spatialEquals", attribute, [values]).toString(); + + static notSpatialEquals = (attribute: string, values: any[]): string => + new Query("notSpatialEquals", attribute, [values]).toString(); + + /** + * Return limit and offset queries for 1-based page pagination. + */ + static page = (page: number, perPage: number = 25): string[] => { + if (page < 1) { + throw new Error("Page must be >= 1, got " + page); + } + if (perPage < 1) { + throw new Error("Per page must be >= 1, got " + perPage); + } + return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; + }; + + static builder = (): QueryBuilder => new QueryBuilder(); + + /** + * Flatten a queries argument into query strings. + * + * Accepts a QueryBuilder, a list of query strings, builders, or nested + * lists (the common `[builder]` / `[builder.build()]` mistakes). + */ + static normalize = (queries?: QueryInput | null): string[] => { + if (queries == null) { + return []; + } + if (queries instanceof QueryBuilder) { + return queries.build(); + } + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === "string") { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...Query.normalize(item)); + } + return out; + }; +} + +export type QueryInput = + | string + | Query + | QueryBuilder + | QueryInput[]; + +/** + * Fluent collector of Query fragments. Call {@link QueryBuilder.build} to + * produce the `string[]` expected by list/createQuery methods. + */ +export class QueryBuilder { + private readonly queries: string[] = []; + + build(): string[] { + return [...this.queries]; + } + + equal(attribute: string, value: QueryTypes): this { + return this.push(Query.equal(attribute, value)); + } + + notEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.notEqual(attribute, value)); + } + + lessThan(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThan(attribute, value)); + } + + lessThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThanEqual(attribute, value)); + } + + greaterThan(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThan(attribute, value)); + } + + greaterThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThanEqual(attribute, value)); + } + + search(attribute: string, value: string): this { + return this.push(Query.search(attribute, value)); + } + + isNull(attribute: string): this { + return this.push(Query.isNull(attribute)); + } + + isNotNull(attribute: string): this { + return this.push(Query.isNotNull(attribute)); + } + + between(attribute: string, start: string | number, end: string | number): this { + return this.push(Query.between(attribute, start, end)); + } + + startsWith(attribute: string, value: string): this { + return this.push(Query.startsWith(attribute, value)); + } + + endsWith(attribute: string, value: string): this { + return this.push(Query.endsWith(attribute, value)); + } + + select(attributes: string[]): this { + return this.push(Query.select(attributes)); + } + + cursorAfter(documentId: string): this { + return this.push(Query.cursorAfter(documentId)); + } + + cursorBefore(documentId: string): this { + return this.push(Query.cursorBefore(documentId)); + } + + orderAsc(attribute: string): this { + return this.push(Query.orderAsc(attribute)); + } + + orderDesc(attribute: string): this { + return this.push(Query.orderDesc(attribute)); + } + + orderRandom(): this { + return this.push(Query.orderRandom()); + } + + limit(limit: number): this { + return this.push(Query.limit(limit)); + } + + offset(offset: number): this { + return this.push(Query.offset(offset)); + } + + contains(attribute: string, value: string | any[]): this { + return this.push(Query.contains(attribute, value)); + } + + containsAny(attribute: string, value: any[]): this { + return this.push(Query.containsAny(attribute, value)); + } + + containsAll(attribute: string, value: any[]): this { + return this.push(Query.containsAll(attribute, value)); + } + + notContains(attribute: string, value: string | any[]): this { + return this.push(Query.notContains(attribute, value)); + } + + notSearch(attribute: string, value: string): this { + return this.push(Query.notSearch(attribute, value)); + } + + notBetween(attribute: string, start: string | number, end: string | number): this { + return this.push(Query.notBetween(attribute, start, end)); + } + + notStartsWith(attribute: string, value: string): this { + return this.push(Query.notStartsWith(attribute, value)); + } + + notEndsWith(attribute: string, value: string): this { + return this.push(Query.notEndsWith(attribute, value)); + } + + regex(attribute: string, pattern: string): this { + return this.push(Query.regex(attribute, pattern)); + } + + exists(attributes: string[]): this { + return this.push(Query.exists(attributes)); + } + + notExists(attributes: string[]): this { + return this.push(Query.notExists(attributes)); + } + + createdBefore(value: string): this { + return this.push(Query.createdBefore(value)); + } + + createdAfter(value: string): this { + return this.push(Query.createdAfter(value)); + } + + createdBetween(start: string, end: string): this { + return this.push(Query.createdBetween(start, end)); + } + + updatedBefore(value: string): this { + return this.push(Query.updatedBefore(value)); + } + + updatedAfter(value: string): this { + return this.push(Query.updatedAfter(value)); + } + + updatedBetween(start: string, end: string): this { + return this.push(Query.updatedBetween(start, end)); + } + + or(queries: string[]): this { + return this.push(Query.or(queries)); + } + + and(queries: string[]): this { + return this.push(Query.and(queries)); + } + + elemMatch(attribute: string, queries: string[]): this { + return this.push(Query.elemMatch(attribute, queries)); + } + + distanceEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceEqual(attribute, values, distance, meters)); + } + + distanceNotEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceNotEqual(attribute, values, distance, meters)); + } + + distanceGreaterThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceGreaterThan(attribute, values, distance, meters)); + } + + distanceLessThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceLessThan(attribute, values, distance, meters)); + } + + intersects(attribute: string, values: any[]): this { + return this.push(Query.intersects(attribute, values)); + } + + notIntersects(attribute: string, values: any[]): this { + return this.push(Query.notIntersects(attribute, values)); + } + + crosses(attribute: string, values: any[]): this { + return this.push(Query.crosses(attribute, values)); + } + + notCrosses(attribute: string, values: any[]): this { + return this.push(Query.notCrosses(attribute, values)); + } + + overlaps(attribute: string, values: any[]): this { + return this.push(Query.overlaps(attribute, values)); + } + + notOverlaps(attribute: string, values: any[]): this { + return this.push(Query.notOverlaps(attribute, values)); + } + + touches(attribute: string, values: any[]): this { + return this.push(Query.touches(attribute, values)); + } + + notTouches(attribute: string, values: any[]): this { + return this.push(Query.notTouches(attribute, values)); + } + + count(attribute: string = "*", alias: string = ""): this { + return this.push(Query.count(attribute, alias)); + } + + countDistinct(attribute: string, alias: string = ""): this { + return this.push(Query.countDistinct(attribute, alias)); + } + + sum(attribute: string, alias: string = ""): this { + return this.push(Query.sum(attribute, alias)); + } + + avg(attribute: string, alias: string = ""): this { + return this.push(Query.avg(attribute, alias)); + } + + min(attribute: string, alias: string = ""): this { + return this.push(Query.min(attribute, alias)); + } + + max(attribute: string, alias: string = ""): this { + return this.push(Query.max(attribute, alias)); + } + + stddev(attribute: string, alias: string = ""): this { + return this.push(Query.stddev(attribute, alias)); + } + + stddevPop(attribute: string, alias: string = ""): this { + return this.push(Query.stddevPop(attribute, alias)); + } + + stddevSamp(attribute: string, alias: string = ""): this { + return this.push(Query.stddevSamp(attribute, alias)); + } + + variance(attribute: string, alias: string = ""): this { + return this.push(Query.variance(attribute, alias)); + } + + varPop(attribute: string, alias: string = ""): this { + return this.push(Query.varPop(attribute, alias)); + } + + varSamp(attribute: string, alias: string = ""): this { + return this.push(Query.varSamp(attribute, alias)); + } + + bitAnd(attribute: string, alias: string = ""): this { + return this.push(Query.bitAnd(attribute, alias)); + } + + bitOr(attribute: string, alias: string = ""): this { + return this.push(Query.bitOr(attribute, alias)); + } + + bitXor(attribute: string, alias: string = ""): this { + return this.push(Query.bitXor(attribute, alias)); + } + + groupBy(attributes: string[]): this { + return this.push(Query.groupBy(attributes)); + } + + having(queries: string[]): this { + return this.push(Query.having(queries)); + } + + distinct(): this { + return this.push(Query.distinct()); + } + + join(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.join(table, left, right, operator, alias)); + } + + leftJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.leftJoin(table, left, right, operator, alias)); + } + + rightJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.rightJoin(table, left, right, operator, alias)); + } + + fullOuterJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.fullOuterJoin(table, left, right, operator, alias)); + } + + crossJoin(table: string, alias: string = ""): this { + return this.push(Query.crossJoin(table, alias)); + } + + vectorDot(attribute: string, vector: number[]): this { + return this.push(Query.vectorDot(attribute, vector)); + } + + vectorCosine(attribute: string, vector: number[]): this { + return this.push(Query.vectorCosine(attribute, vector)); + } + + vectorEuclidean(attribute: string, vector: number[]): this { + return this.push(Query.vectorEuclidean(attribute, vector)); + } + + covers(attribute: string, values: any[]): this { + return this.push(Query.covers(attribute, values)); + } + + notCovers(attribute: string, values: any[]): this { + return this.push(Query.notCovers(attribute, values)); + } + + spatialEquals(attribute: string, values: any[]): this { + return this.push(Query.spatialEquals(attribute, values)); + } + + notSpatialEquals(attribute: string, values: any[]): this { + return this.push(Query.notSpatialEquals(attribute, values)); + } + + page(page: number, perPage: number = 25): this { + return this.push(Query.page(page, perPage)); + } + + private push(query: string | string[]): this { + if (Array.isArray(query)) { + this.queries.push(...query); + } else { + this.queries.push(query); + } + return this; + } } diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index f4aad6381e..c32151519c 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -274,4 +274,93 @@ describe('Query', () => { Query.updatedBetween('2023-01-01', '2023-12-31').toString(), `{"method":"between","attribute":"$updatedAt","values":["2023-01-01","2023-12-31"]}`, )); + + test('count with alias', () => assertEquals( + Query.count('*', 'total').toString(), + '{"method":"count","attribute":"*","values":["total"]}', + )); + + test('count without alias', () => assertEquals( + Query.count().toString(), + '{"method":"count","attribute":"*"}', + )); + + test('groupBy', () => assertEquals( + Query.groupBy(['status', 'year']).toString(), + '{"method":"groupBy","values":["status","year"]}', + )); + + test('having nests parsed queries', () => { + const having = JSON.parse(Query.having([Query.greaterThan('total', 1)])); + assertEquals(having.method, 'having'); + assertEquals(having.values[0].method, 'greaterThan'); + assertEquals(having.values[0].attribute, 'total'); + assertEquals(having.values[0].values, [1]); + }); + + test('distinct', () => assertEquals( + Query.distinct().toString(), + '{"method":"distinct"}', + )); + + test('join', () => assertEquals( + Query.join('orders', '$id', 'customerId').toString(), + '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}', + )); + + test('leftJoin with alias', () => assertEquals( + Query.leftJoin('orders', '$id', 'customerId', '=', 'ord').toString(), + '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', + )); + + test('crossJoin', () => assertEquals( + Query.crossJoin('orders', 'ord').toString(), + '{"method":"crossJoin","attribute":"orders","values":["ord"]}', + )); + + test('vectorDot', () => assertEquals( + Query.vectorDot('embedding', [0.1, 0.2]).toString(), + '{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}', + )); + + test('page', () => { + const queries = Query.page(2, 10); + assertEquals(queries, [ + '{"method":"limit","values":[10]}', + '{"method":"offset","values":[10]}', + ]); + }); + + test('builder build', () => { + const built = Query.builder() + .equal('status', 'published') + .leftJoin('orders', '$id', 'customerId', '=', 'ord') + .count('*', 'total') + .groupBy(['status']) + .limit(25) + .build(); + + assertEquals(built, [ + Query.equal('status', 'published'), + Query.leftJoin('orders', '$id', 'customerId', '=', 'ord'), + Query.count('*', 'total'), + Query.groupBy(['status']), + Query.limit(25), + ]); + }); + + test('normalize builder wrapped in array', () => { + const builder = Query.builder().equal('status', 'active').limit(10); + assertEquals(Query.normalize([builder]), builder.build()); + assertEquals(Query.normalize([builder.build()]), builder.build()); + assertEquals(Query.normalize(builder), builder.build()); + }); + + test('normalize page wrapped in array', () => { + assertEquals(Query.normalize([Query.equal('status', 'active'), Query.page(2, 10)]), [ + Query.equal('status', 'active'), + Query.limit(10), + Query.offset(10), + ]); + }); }) diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index bb3eb7e3cd..4b38aade39 100644 --- a/templates/dotnet/Package/Client.cs.twig +++ b/templates/dotnet/Package/Client.cs.twig @@ -151,6 +151,11 @@ namespace {{ spec.info.title | caseUcfirst }} Dictionary headers, Dictionary parameters) { + if (parameters.ContainsKey("queries")) + { + parameters["queries"] = Query.Normalize(parameters["queries"]); + } + var methodGet = "GET".Equals(method, StringComparison.OrdinalIgnoreCase); var queryString = methodGet && parameters.Count > 0 ? diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 95b53cb3bf..a97ca995a8 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -354,5 +355,658 @@ namespace {{ spec.info.title | caseUcfirst }} { return new Query("notTouches", attribute, new List { values }).ToString(); } + + public static string Count(string attribute = "*", string alias = "") + { + return new Query("count", attribute, alias != "" ? alias : null).ToString(); + } + + public static string CountDistinct(string attribute, string alias = "") + { + return new Query("countDistinct", attribute, alias != "" ? alias : null).ToString(); + } + + public static string Sum(string attribute, string alias = "") + { + return new Query("sum", attribute, alias != "" ? alias : null).ToString(); + } + + public static string Avg(string attribute, string alias = "") + { + return new Query("avg", attribute, alias != "" ? alias : null).ToString(); + } + + public static string Min(string attribute, string alias = "") + { + return new Query("min", attribute, alias != "" ? alias : null).ToString(); + } + + public static string Max(string attribute, string alias = "") + { + return new Query("max", attribute, alias != "" ? alias : null).ToString(); + } + + public static string Stddev(string attribute, string alias = "") + { + return new Query("stddev", attribute, alias != "" ? alias : null).ToString(); + } + + public static string StddevPop(string attribute, string alias = "") + { + return new Query("stddevPop", attribute, alias != "" ? alias : null).ToString(); + } + + public static string StddevSamp(string attribute, string alias = "") + { + return new Query("stddevSamp", attribute, alias != "" ? alias : null).ToString(); + } + + public static string Variance(string attribute, string alias = "") + { + return new Query("variance", attribute, alias != "" ? alias : null).ToString(); + } + + public static string VarPop(string attribute, string alias = "") + { + return new Query("varPop", attribute, alias != "" ? alias : null).ToString(); + } + + public static string VarSamp(string attribute, string alias = "") + { + return new Query("varSamp", attribute, alias != "" ? alias : null).ToString(); + } + + public static string BitAnd(string attribute, string alias = "") + { + return new Query("bitAnd", attribute, alias != "" ? alias : null).ToString(); + } + + public static string BitOr(string attribute, string alias = "") + { + return new Query("bitOr", attribute, alias != "" ? alias : null).ToString(); + } + + public static string BitXor(string attribute, string alias = "") + { + return new Query("bitXor", attribute, alias != "" ? alias : null).ToString(); + } + + public static string GroupBy(List attributes) + { + return new Query("groupBy", null, attributes).ToString(); + } + + public static string Having(List queries) + { + return new Query("having", null, queries.Select(q => JsonSerializer.Deserialize(q, Client.DeserializerOptions)).ToList()).ToString(); + } + + public static string Distinct() + { + return new Query("distinct", null, null).ToString(); + } + + public static string Join(string table, string left, string right, string @operator = "=", string alias = "") + { + return JoinQuery("join", table, left, right, @operator, alias); + } + + public static string LeftJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return JoinQuery("leftJoin", table, left, right, @operator, alias); + } + + public static string RightJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return JoinQuery("rightJoin", table, left, right, @operator, alias); + } + + public static string FullOuterJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return JoinQuery("fullOuterJoin", table, left, right, @operator, alias); + } + + public static string CrossJoin(string table, string alias = "") + { + return new Query("crossJoin", table, alias != "" ? alias : null).ToString(); + } + + public static string Covers(string attribute, object values) + { + return new Query("covers", attribute, new List { values }).ToString(); + } + + public static string NotCovers(string attribute, object values) + { + return new Query("notCovers", attribute, new List { values }).ToString(); + } + + public static string SpatialEquals(string attribute, object values) + { + return new Query("spatialEquals", attribute, new List { values }).ToString(); + } + + public static string NotSpatialEquals(string attribute, object values) + { + return new Query("notSpatialEquals", attribute, new List { values }).ToString(); + } + + public static List Page(int page, int perPage = 25) + { + if (page < 1) + { + throw new ArgumentException("Page must be >= 1, got " + page); + } + if (perPage < 1) + { + throw new ArgumentException("Per page must be >= 1, got " + perPage); + } + + return new List { Limit(perPage), Offset((page - 1) * perPage) }; + } + + public static QueryBuilder Builder() + { + return new QueryBuilder(); + } + + public static List Normalize(object? queries) + { + if (queries == null) + { + return new List(); + } + if (queries is QueryBuilder builder) + { + return builder.Build(); + } + if (queries is Query query) + { + return new List { query.ToString() }; + } + if (queries is string s) + { + return new List { s }; + } + if (queries is IEnumerable enumerable) + { + var output = new List(); + foreach (var item in enumerable) + { + output.AddRange(Normalize(item)); + } + return output; + } + + return new List(); + } + + private static string JoinQuery(string method, string table, string left, string right, string @operator, string alias) + { + var values = new List { left, @operator, right }; + if (alias != "") + { + values.Add(alias); + } + + return new Query(method, table, values).ToString(); + } + } + + public class QueryBuilder + { + private readonly List queries = new List(); + + public List Build() + { + return new List(queries); + } + + public QueryBuilder Equal(string attribute, object value) + { + return Push(Query.Equal(attribute, value)); + } + + public QueryBuilder NotEqual(string attribute, object value) + { + return Push(Query.NotEqual(attribute, value)); + } + + public QueryBuilder LessThan(string attribute, object value) + { + return Push(Query.LessThan(attribute, value)); + } + + public QueryBuilder LessThanEqual(string attribute, object value) + { + return Push(Query.LessThanEqual(attribute, value)); + } + + public QueryBuilder GreaterThan(string attribute, object value) + { + return Push(Query.GreaterThan(attribute, value)); + } + + public QueryBuilder GreaterThanEqual(string attribute, object value) + { + return Push(Query.GreaterThanEqual(attribute, value)); + } + + public QueryBuilder Search(string attribute, string value) + { + return Push(Query.Search(attribute, value)); + } + + public QueryBuilder IsNull(string attribute) + { + return Push(Query.IsNull(attribute)); + } + + public QueryBuilder IsNotNull(string attribute) + { + return Push(Query.IsNotNull(attribute)); + } + + public QueryBuilder Between(string attribute, string start, string end) + { + return Push(Query.Between(attribute, start, end)); + } + + public QueryBuilder Between(string attribute, int start, int end) + { + return Push(Query.Between(attribute, start, end)); + } + + public QueryBuilder Between(string attribute, double start, double end) + { + return Push(Query.Between(attribute, start, end)); + } + + public QueryBuilder StartsWith(string attribute, string value) + { + return Push(Query.StartsWith(attribute, value)); + } + + public QueryBuilder EndsWith(string attribute, string value) + { + return Push(Query.EndsWith(attribute, value)); + } + + public QueryBuilder Select(List attributes) + { + return Push(Query.Select(attributes)); + } + + public QueryBuilder CursorAfter(string documentId) + { + return Push(Query.CursorAfter(documentId)); + } + + public QueryBuilder CursorBefore(string documentId) + { + return Push(Query.CursorBefore(documentId)); + } + + public QueryBuilder OrderAsc(string attribute) + { + return Push(Query.OrderAsc(attribute)); + } + + public QueryBuilder OrderDesc(string attribute) + { + return Push(Query.OrderDesc(attribute)); + } + + public QueryBuilder OrderRandom() + { + return Push(Query.OrderRandom()); + } + + public QueryBuilder Limit(int limit) + { + return Push(Query.Limit(limit)); + } + + public QueryBuilder Offset(int offset) + { + return Push(Query.Offset(offset)); + } + + public QueryBuilder Contains(string attribute, object value) + { + return Push(Query.Contains(attribute, value)); + } + + public QueryBuilder ContainsAny(string attribute, IList value) + { + return Push(Query.ContainsAny(attribute, value)); + } + + public QueryBuilder ContainsAll(string attribute, IList value) + { + return Push(Query.ContainsAll(attribute, value)); + } + + public QueryBuilder NotContains(string attribute, object value) + { + return Push(Query.NotContains(attribute, value)); + } + + public QueryBuilder NotSearch(string attribute, string value) + { + return Push(Query.NotSearch(attribute, value)); + } + + public QueryBuilder NotBetween(string attribute, string start, string end) + { + return Push(Query.NotBetween(attribute, start, end)); + } + + public QueryBuilder NotBetween(string attribute, int start, int end) + { + return Push(Query.NotBetween(attribute, start, end)); + } + + public QueryBuilder NotBetween(string attribute, double start, double end) + { + return Push(Query.NotBetween(attribute, start, end)); + } + + public QueryBuilder NotStartsWith(string attribute, string value) + { + return Push(Query.NotStartsWith(attribute, value)); + } + + public QueryBuilder NotEndsWith(string attribute, string value) + { + return Push(Query.NotEndsWith(attribute, value)); + } + + public QueryBuilder Regex(string attribute, string pattern) + { + return Push(Query.Regex(attribute, pattern)); + } + + public QueryBuilder Exists(List attributes) + { + return Push(Query.Exists(attributes)); + } + + public QueryBuilder NotExists(List attributes) + { + return Push(Query.NotExists(attributes)); + } + + public QueryBuilder CreatedBefore(string value) + { + return Push(Query.CreatedBefore(value)); + } + + public QueryBuilder CreatedAfter(string value) + { + return Push(Query.CreatedAfter(value)); + } + + public QueryBuilder CreatedBetween(string start, string end) + { + return Push(Query.CreatedBetween(start, end)); + } + + public QueryBuilder UpdatedBefore(string value) + { + return Push(Query.UpdatedBefore(value)); + } + + public QueryBuilder UpdatedAfter(string value) + { + return Push(Query.UpdatedAfter(value)); + } + + public QueryBuilder UpdatedBetween(string start, string end) + { + return Push(Query.UpdatedBetween(start, end)); + } + + public QueryBuilder Or(List queries) + { + return Push(Query.Or(queries)); + } + + public QueryBuilder And(List queries) + { + return Push(Query.And(queries)); + } + + public QueryBuilder ElemMatch(string attribute, List queries) + { + return Push(Query.ElemMatch(attribute, queries)); + } + + public QueryBuilder DistanceEqual(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceEqual(attribute, values, distance, meters)); + } + + public QueryBuilder DistanceNotEqual(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceNotEqual(attribute, values, distance, meters)); + } + + public QueryBuilder DistanceGreaterThan(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceGreaterThan(attribute, values, distance, meters)); + } + + public QueryBuilder DistanceLessThan(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceLessThan(attribute, values, distance, meters)); + } + + public QueryBuilder Intersects(string attribute, object values) + { + return Push(Query.Intersects(attribute, values)); + } + + public QueryBuilder NotIntersects(string attribute, object values) + { + return Push(Query.NotIntersects(attribute, values)); + } + + public QueryBuilder Crosses(string attribute, object values) + { + return Push(Query.Crosses(attribute, values)); + } + + public QueryBuilder NotCrosses(string attribute, object values) + { + return Push(Query.NotCrosses(attribute, values)); + } + + public QueryBuilder Overlaps(string attribute, object values) + { + return Push(Query.Overlaps(attribute, values)); + } + + public QueryBuilder NotOverlaps(string attribute, object values) + { + return Push(Query.NotOverlaps(attribute, values)); + } + + public QueryBuilder Touches(string attribute, object values) + { + return Push(Query.Touches(attribute, values)); + } + + public QueryBuilder NotTouches(string attribute, object values) + { + return Push(Query.NotTouches(attribute, values)); + } + + public QueryBuilder Count(string attribute = "*", string alias = "") + { + return Push(Query.Count(attribute, alias)); + } + + public QueryBuilder CountDistinct(string attribute, string alias = "") + { + return Push(Query.CountDistinct(attribute, alias)); + } + + public QueryBuilder Sum(string attribute, string alias = "") + { + return Push(Query.Sum(attribute, alias)); + } + + public QueryBuilder Avg(string attribute, string alias = "") + { + return Push(Query.Avg(attribute, alias)); + } + + public QueryBuilder Min(string attribute, string alias = "") + { + return Push(Query.Min(attribute, alias)); + } + + public QueryBuilder Max(string attribute, string alias = "") + { + return Push(Query.Max(attribute, alias)); + } + + public QueryBuilder Stddev(string attribute, string alias = "") + { + return Push(Query.Stddev(attribute, alias)); + } + + public QueryBuilder StddevPop(string attribute, string alias = "") + { + return Push(Query.StddevPop(attribute, alias)); + } + + public QueryBuilder StddevSamp(string attribute, string alias = "") + { + return Push(Query.StddevSamp(attribute, alias)); + } + + public QueryBuilder Variance(string attribute, string alias = "") + { + return Push(Query.Variance(attribute, alias)); + } + + public QueryBuilder VarPop(string attribute, string alias = "") + { + return Push(Query.VarPop(attribute, alias)); + } + + public QueryBuilder VarSamp(string attribute, string alias = "") + { + return Push(Query.VarSamp(attribute, alias)); + } + + public QueryBuilder BitAnd(string attribute, string alias = "") + { + return Push(Query.BitAnd(attribute, alias)); + } + + public QueryBuilder BitOr(string attribute, string alias = "") + { + return Push(Query.BitOr(attribute, alias)); + } + + public QueryBuilder BitXor(string attribute, string alias = "") + { + return Push(Query.BitXor(attribute, alias)); + } + + public QueryBuilder GroupBy(List attributes) + { + return Push(Query.GroupBy(attributes)); + } + + public QueryBuilder Having(List queries) + { + return Push(Query.Having(queries)); + } + + public QueryBuilder Distinct() + { + return Push(Query.Distinct()); + } + + public QueryBuilder Join(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.Join(table, left, right, @operator, alias)); + } + + public QueryBuilder LeftJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.LeftJoin(table, left, right, @operator, alias)); + } + + public QueryBuilder RightJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.RightJoin(table, left, right, @operator, alias)); + } + + public QueryBuilder FullOuterJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.FullOuterJoin(table, left, right, @operator, alias)); + } + + public QueryBuilder CrossJoin(string table, string alias = "") + { + return Push(Query.CrossJoin(table, alias)); + } + + public QueryBuilder VectorDot(string attribute, List vector) + { + return Push(Query.VectorDot(attribute, vector)); + } + + public QueryBuilder VectorCosine(string attribute, List vector) + { + return Push(Query.VectorCosine(attribute, vector)); + } + + public QueryBuilder VectorEuclidean(string attribute, List vector) + { + return Push(Query.VectorEuclidean(attribute, vector)); + } + + public QueryBuilder Covers(string attribute, object values) + { + return Push(Query.Covers(attribute, values)); + } + + public QueryBuilder NotCovers(string attribute, object values) + { + return Push(Query.NotCovers(attribute, values)); + } + + public QueryBuilder SpatialEquals(string attribute, object values) + { + return Push(Query.SpatialEquals(attribute, values)); + } + + public QueryBuilder NotSpatialEquals(string attribute, object values) + { + return Push(Query.NotSpatialEquals(attribute, values)); + } + + public QueryBuilder Page(int page, int perPage = 25) + { + return Push(Query.Page(page, perPage)); + } + + private QueryBuilder Push(string query) + { + queries.Add(query); + return this; + } + + private QueryBuilder Push(IEnumerable query) + { + queries.AddRange(query); + return this; + } } } diff --git a/templates/flutter/lib/src/client_mixin.dart.twig b/templates/flutter/lib/src/client_mixin.dart.twig index 0eebe1d851..c7ff181ffe 100644 --- a/templates/flutter/lib/src/client_mixin.dart.twig +++ b/templates/flutter/lib/src/client_mixin.dart.twig @@ -12,6 +12,11 @@ mixin ClientMixin { required Map headers, required Map params, }) { + if (params.containsKey('queries')) { + params = Map.from(params); + params['queries'] = _flattenQueries(params['queries']); + } + http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { request = http.MultipartRequest(method.name(), uri); @@ -144,3 +149,24 @@ mixin ClientMixin { } } } + +List _flattenQueries(dynamic queries) { + if (queries == null) { + return []; + } + if (queries is String) { + return [queries]; + } + if (queries is List) { + final out = []; + for (final item in queries) { + out.addAll(_flattenQueries(item)); + } + return out; + } + try { + return _flattenQueries(queries.build()); + } catch (_) { + return [queries.toString()]; + } +} diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index 2ef0a514af..062093c8b8 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -20,6 +20,7 @@ import ( "time" "{{ sdk | goPackagePath }}/file" + "{{ sdk | goPackagePath }}/query" ) const ( @@ -435,6 +436,12 @@ func (client *Client) FileUpload(url string, headers map[string]interface{}, par // Call an API using Client func (client *Client) Call(method string, path string, headers map[string]interface{}, params map[string]interface{}) (*ClientResponse, error) { + if params != nil { + if _, ok := params["queries"]; ok { + params["queries"] = query.Normalize(params["queries"]) + } + } + if client.Client == nil { // Create HTTP client httpClient, err := GetDefaultClient(client.Timeout) diff --git a/templates/go/query.go.twig b/templates/go/query.go.twig index b5361f5e3e..7f11289ed8 100644 --- a/templates/go/query.go.twig +++ b/templates/go/query.go.twig @@ -2,6 +2,8 @@ package query import ( "encoding/json" + "fmt" + "reflect" ) func toArray(val interface{}) []interface{} { @@ -520,3 +522,561 @@ func ElemMatch(attribute string, queries []string) string { Values: &parsedQueries, }) } + +func aliasedQuery(method, attribute, alias string) string { + options := queryOptions{ + Method: method, + Attribute: &attribute, + } + if alias != "" { + values := toArray(alias) + options.Values = &values + } + return parseQuery(options) +} + +func joinQuery(method, table, left, right, operator, alias string) string { + if operator == "" { + operator = "=" + } + values := []interface{}{left, operator, right} + if alias != "" { + values = append(values, alias) + } + return parseQuery(queryOptions{ + Method: method, + Attribute: &table, + Values: &values, + }) +} + +func Count(attribute, alias string) string { + return aliasedQuery("count", attribute, alias) +} + +func CountDistinct(attribute, alias string) string { + return aliasedQuery("countDistinct", attribute, alias) +} + +func Sum(attribute, alias string) string { + return aliasedQuery("sum", attribute, alias) +} + +func Avg(attribute, alias string) string { + return aliasedQuery("avg", attribute, alias) +} + +func Min(attribute, alias string) string { + return aliasedQuery("min", attribute, alias) +} + +func Max(attribute, alias string) string { + return aliasedQuery("max", attribute, alias) +} + +func Stddev(attribute, alias string) string { + return aliasedQuery("stddev", attribute, alias) +} + +func StddevPop(attribute, alias string) string { + return aliasedQuery("stddevPop", attribute, alias) +} + +func StddevSamp(attribute, alias string) string { + return aliasedQuery("stddevSamp", attribute, alias) +} + +func Variance(attribute, alias string) string { + return aliasedQuery("variance", attribute, alias) +} + +func VarPop(attribute, alias string) string { + return aliasedQuery("varPop", attribute, alias) +} + +func VarSamp(attribute, alias string) string { + return aliasedQuery("varSamp", attribute, alias) +} + +func BitAnd(attribute, alias string) string { + return aliasedQuery("bitAnd", attribute, alias) +} + +func BitOr(attribute, alias string) string { + return aliasedQuery("bitOr", attribute, alias) +} + +func BitXor(attribute, alias string) string { + return aliasedQuery("bitXor", attribute, alias) +} + +func GroupBy(attributes []interface{}) string { + return parseQuery(queryOptions{ + Method: "groupBy", + Values: &attributes, + }) +} + +func Having(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: "having", + Values: &parsedQueries, + }) +} + +func Distinct() string { + return parseQuery(queryOptions{ + Method: "distinct", + }) +} + +func Join(table, left, right, operator, alias string) string { + return joinQuery("join", table, left, right, operator, alias) +} + +func LeftJoin(table, left, right, operator, alias string) string { + return joinQuery("leftJoin", table, left, right, operator, alias) +} + +func RightJoin(table, left, right, operator, alias string) string { + return joinQuery("rightJoin", table, left, right, operator, alias) +} + +func FullOuterJoin(table, left, right, operator, alias string) string { + return joinQuery("fullOuterJoin", table, left, right, operator, alias) +} + +func CrossJoin(table, alias string) string { + return aliasedQuery("crossJoin", table, alias) +} + +func Covers(attribute string, values []interface{}) string { + return parseQuery(queryOptions{ + Method: "covers", + Attribute: &attribute, + Values: &[]interface{}{values}, + }) +} + +func NotCovers(attribute string, values []interface{}) string { + return parseQuery(queryOptions{ + Method: "notCovers", + Attribute: &attribute, + Values: &[]interface{}{values}, + }) +} + +func SpatialEquals(attribute string, values []interface{}) string { + return parseQuery(queryOptions{ + Method: "spatialEquals", + Attribute: &attribute, + Values: &[]interface{}{values}, + }) +} + +func NotSpatialEquals(attribute string, values []interface{}) string { + return parseQuery(queryOptions{ + Method: "notSpatialEquals", + Attribute: &attribute, + Values: &[]interface{}{values}, + }) +} + +func Page(page, perPage int) []string { + if page < 1 { + panic(fmt.Sprintf("Page must be >= 1, got %d", page)) + } + if perPage < 1 { + panic(fmt.Sprintf("Per page must be >= 1, got %d", perPage)) + } + return []string{Limit(perPage), Offset((page - 1) * perPage)} +} + +type QueryBuilder struct { + queries []string +} + +func Builder() *QueryBuilder { + return &QueryBuilder{} +} + +func (b *QueryBuilder) Build() []string { + result := make([]string, len(b.queries)) + copy(result, b.queries) + return result +} + +func (b *QueryBuilder) push(queries ...string) *QueryBuilder { + b.queries = append(b.queries, queries...) + return b +} + +func (b *QueryBuilder) Equal(attribute string, value interface{}) *QueryBuilder { + return b.push(Equal(attribute, value)) +} + +func (b *QueryBuilder) NotEqual(attribute string, value interface{}) *QueryBuilder { + return b.push(NotEqual(attribute, value)) +} + +func (b *QueryBuilder) LessThan(attribute string, value interface{}) *QueryBuilder { + return b.push(LessThan(attribute, value)) +} + +func (b *QueryBuilder) LessThanEqual(attribute string, value interface{}) *QueryBuilder { + return b.push(LessThanEqual(attribute, value)) +} + +func (b *QueryBuilder) GreaterThan(attribute string, value interface{}) *QueryBuilder { + return b.push(GreaterThan(attribute, value)) +} + +func (b *QueryBuilder) GreaterThanEqual(attribute string, value interface{}) *QueryBuilder { + return b.push(GreaterThanEqual(attribute, value)) +} + +func (b *QueryBuilder) Search(attribute string, value interface{}) *QueryBuilder { + return b.push(Search(attribute, value)) +} + +func (b *QueryBuilder) IsNull(attribute string) *QueryBuilder { + return b.push(IsNull(attribute)) +} + +func (b *QueryBuilder) IsNotNull(attribute string) *QueryBuilder { + return b.push(IsNotNull(attribute)) +} + +func (b *QueryBuilder) Between(attribute string, start, end interface{}) *QueryBuilder { + return b.push(Between(attribute, start, end)) +} + +func (b *QueryBuilder) StartsWith(attribute string, value interface{}) *QueryBuilder { + return b.push(StartsWith(attribute, value)) +} + +func (b *QueryBuilder) EndsWith(attribute string, value interface{}) *QueryBuilder { + return b.push(EndsWith(attribute, value)) +} + +func (b *QueryBuilder) Select(attributes interface{}) *QueryBuilder { + return b.push(Select(attributes)) +} + +func (b *QueryBuilder) CursorAfter(documentId string) *QueryBuilder { + return b.push(CursorAfter(documentId)) +} + +func (b *QueryBuilder) CursorBefore(documentId interface{}) *QueryBuilder { + return b.push(CursorBefore(documentId)) +} + +func (b *QueryBuilder) OrderAsc(attribute string) *QueryBuilder { + return b.push(OrderAsc(attribute)) +} + +func (b *QueryBuilder) OrderDesc(attribute string) *QueryBuilder { + return b.push(OrderDesc(attribute)) +} + +func (b *QueryBuilder) OrderRandom() *QueryBuilder { + return b.push(OrderRandom()) +} + +func (b *QueryBuilder) Limit(limit int) *QueryBuilder { + return b.push(Limit(limit)) +} + +func (b *QueryBuilder) Offset(offset int) *QueryBuilder { + return b.push(Offset(offset)) +} + +func (b *QueryBuilder) Contains(attribute string, value interface{}) *QueryBuilder { + return b.push(Contains(attribute, value)) +} + +func (b *QueryBuilder) ContainsAny(attribute string, value []interface{}) *QueryBuilder { + return b.push(ContainsAny(attribute, value)) +} + +func (b *QueryBuilder) ContainsAll(attribute string, value []interface{}) *QueryBuilder { + return b.push(ContainsAll(attribute, value)) +} + +func (b *QueryBuilder) NotContains(attribute string, value interface{}) *QueryBuilder { + return b.push(NotContains(attribute, value)) +} + +func (b *QueryBuilder) NotSearch(attribute string, value interface{}) *QueryBuilder { + return b.push(NotSearch(attribute, value)) +} + +func (b *QueryBuilder) NotBetween(attribute string, start, end interface{}) *QueryBuilder { + return b.push(NotBetween(attribute, start, end)) +} + +func (b *QueryBuilder) NotStartsWith(attribute string, value interface{}) *QueryBuilder { + return b.push(NotStartsWith(attribute, value)) +} + +func (b *QueryBuilder) NotEndsWith(attribute string, value interface{}) *QueryBuilder { + return b.push(NotEndsWith(attribute, value)) +} + +func (b *QueryBuilder) Regex(attribute string, pattern string) *QueryBuilder { + return b.push(Regex(attribute, pattern)) +} + +func (b *QueryBuilder) Exists(attributes []interface{}) *QueryBuilder { + return b.push(Exists(attributes)) +} + +func (b *QueryBuilder) NotExists(attributes []interface{}) *QueryBuilder { + return b.push(NotExists(attributes)) +} + +func (b *QueryBuilder) CreatedBefore(value interface{}) *QueryBuilder { + return b.push(CreatedBefore(value)) +} + +func (b *QueryBuilder) CreatedAfter(value interface{}) *QueryBuilder { + return b.push(CreatedAfter(value)) +} + +func (b *QueryBuilder) CreatedBetween(start, end interface{}) *QueryBuilder { + return b.push(CreatedBetween(start, end)) +} + +func (b *QueryBuilder) UpdatedBefore(value interface{}) *QueryBuilder { + return b.push(UpdatedBefore(value)) +} + +func (b *QueryBuilder) UpdatedAfter(value interface{}) *QueryBuilder { + return b.push(UpdatedAfter(value)) +} + +func (b *QueryBuilder) UpdatedBetween(start, end interface{}) *QueryBuilder { + return b.push(UpdatedBetween(start, end)) +} + +func (b *QueryBuilder) Or(queries []string) *QueryBuilder { + return b.push(Or(queries)) +} + +func (b *QueryBuilder) And(queries []string) *QueryBuilder { + return b.push(And(queries)) +} + +func (b *QueryBuilder) ElemMatch(attribute string, queries []string) *QueryBuilder { + return b.push(ElemMatch(attribute, queries)) +} + +func (b *QueryBuilder) DistanceEqual(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { + return b.push(DistanceEqual(attribute, values, distance, meters)) +} + +func (b *QueryBuilder) DistanceNotEqual(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { + return b.push(DistanceNotEqual(attribute, values, distance, meters)) +} + +func (b *QueryBuilder) DistanceGreaterThan(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { + return b.push(DistanceGreaterThan(attribute, values, distance, meters)) +} + +func (b *QueryBuilder) DistanceLessThan(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { + return b.push(DistanceLessThan(attribute, values, distance, meters)) +} + +func (b *QueryBuilder) Intersects(attribute string, values []interface{}) *QueryBuilder { + return b.push(Intersects(attribute, values)) +} + +func (b *QueryBuilder) NotIntersects(attribute string, values []interface{}) *QueryBuilder { + return b.push(NotIntersects(attribute, values)) +} + +func (b *QueryBuilder) Crosses(attribute string, values []interface{}) *QueryBuilder { + return b.push(Crosses(attribute, values)) +} + +func (b *QueryBuilder) NotCrosses(attribute string, values []interface{}) *QueryBuilder { + return b.push(NotCrosses(attribute, values)) +} + +func (b *QueryBuilder) Overlaps(attribute string, values []interface{}) *QueryBuilder { + return b.push(Overlaps(attribute, values)) +} + +func (b *QueryBuilder) NotOverlaps(attribute string, values []interface{}) *QueryBuilder { + return b.push(NotOverlaps(attribute, values)) +} + +func (b *QueryBuilder) Touches(attribute string, values []interface{}) *QueryBuilder { + return b.push(Touches(attribute, values)) +} + +func (b *QueryBuilder) NotTouches(attribute string, values []interface{}) *QueryBuilder { + return b.push(NotTouches(attribute, values)) +} + +func (b *QueryBuilder) Count(attribute, alias string) *QueryBuilder { + return b.push(Count(attribute, alias)) +} + +func (b *QueryBuilder) CountDistinct(attribute, alias string) *QueryBuilder { + return b.push(CountDistinct(attribute, alias)) +} + +func (b *QueryBuilder) Sum(attribute, alias string) *QueryBuilder { + return b.push(Sum(attribute, alias)) +} + +func (b *QueryBuilder) Avg(attribute, alias string) *QueryBuilder { + return b.push(Avg(attribute, alias)) +} + +func (b *QueryBuilder) Min(attribute, alias string) *QueryBuilder { + return b.push(Min(attribute, alias)) +} + +func (b *QueryBuilder) Max(attribute, alias string) *QueryBuilder { + return b.push(Max(attribute, alias)) +} + +func (b *QueryBuilder) Stddev(attribute, alias string) *QueryBuilder { + return b.push(Stddev(attribute, alias)) +} + +func (b *QueryBuilder) StddevPop(attribute, alias string) *QueryBuilder { + return b.push(StddevPop(attribute, alias)) +} + +func (b *QueryBuilder) StddevSamp(attribute, alias string) *QueryBuilder { + return b.push(StddevSamp(attribute, alias)) +} + +func (b *QueryBuilder) Variance(attribute, alias string) *QueryBuilder { + return b.push(Variance(attribute, alias)) +} + +func (b *QueryBuilder) VarPop(attribute, alias string) *QueryBuilder { + return b.push(VarPop(attribute, alias)) +} + +func (b *QueryBuilder) VarSamp(attribute, alias string) *QueryBuilder { + return b.push(VarSamp(attribute, alias)) +} + +func (b *QueryBuilder) BitAnd(attribute, alias string) *QueryBuilder { + return b.push(BitAnd(attribute, alias)) +} + +func (b *QueryBuilder) BitOr(attribute, alias string) *QueryBuilder { + return b.push(BitOr(attribute, alias)) +} + +func (b *QueryBuilder) BitXor(attribute, alias string) *QueryBuilder { + return b.push(BitXor(attribute, alias)) +} + +func (b *QueryBuilder) GroupBy(attributes []interface{}) *QueryBuilder { + return b.push(GroupBy(attributes)) +} + +func (b *QueryBuilder) Having(queries []string) *QueryBuilder { + return b.push(Having(queries)) +} + +func (b *QueryBuilder) Distinct() *QueryBuilder { + return b.push(Distinct()) +} + +func (b *QueryBuilder) Join(table, left, right, operator, alias string) *QueryBuilder { + return b.push(Join(table, left, right, operator, alias)) +} + +func (b *QueryBuilder) LeftJoin(table, left, right, operator, alias string) *QueryBuilder { + return b.push(LeftJoin(table, left, right, operator, alias)) +} + +func (b *QueryBuilder) RightJoin(table, left, right, operator, alias string) *QueryBuilder { + return b.push(RightJoin(table, left, right, operator, alias)) +} + +func (b *QueryBuilder) FullOuterJoin(table, left, right, operator, alias string) *QueryBuilder { + return b.push(FullOuterJoin(table, left, right, operator, alias)) +} + +func (b *QueryBuilder) CrossJoin(table, alias string) *QueryBuilder { + return b.push(CrossJoin(table, alias)) +} + +func (b *QueryBuilder) VectorDot(attribute string, vector []float64) *QueryBuilder { + return b.push(VectorDot(attribute, vector)) +} + +func (b *QueryBuilder) VectorCosine(attribute string, vector []float64) *QueryBuilder { + return b.push(VectorCosine(attribute, vector)) +} + +func (b *QueryBuilder) VectorEuclidean(attribute string, vector []float64) *QueryBuilder { + return b.push(VectorEuclidean(attribute, vector)) +} + +func (b *QueryBuilder) Covers(attribute string, values []interface{}) *QueryBuilder { + return b.push(Covers(attribute, values)) +} + +func (b *QueryBuilder) NotCovers(attribute string, values []interface{}) *QueryBuilder { + return b.push(NotCovers(attribute, values)) +} + +func (b *QueryBuilder) SpatialEquals(attribute string, values []interface{}) *QueryBuilder { + return b.push(SpatialEquals(attribute, values)) +} + +func (b *QueryBuilder) NotSpatialEquals(attribute string, values []interface{}) *QueryBuilder { + return b.push(NotSpatialEquals(attribute, values)) +} + +func (b *QueryBuilder) Page(page, perPage int) *QueryBuilder { + return b.push(Page(page, perPage)...) +} + +func Normalize(queries any) []string { + if queries == nil { + return []string{} + } + + switch v := queries.(type) { + case *QueryBuilder: + if v == nil { + return []string{} + } + return v.Build() + case QueryBuilder: + return v.Build() + case string: + return []string{v} + } + + value := reflect.ValueOf(queries) + switch value.Kind() { + case reflect.Slice, reflect.Array: + out := make([]string, 0, value.Len()) + for i := 0; i < value.Len(); i++ { + out = append(out, Normalize(value.Index(i).Interface())...) + } + return out + default: + return []string{} + } +} diff --git a/templates/go/query_test.go.twig b/templates/go/query_test.go.twig index 804e46492b..e4dc4cfde7 100644 --- a/templates/go/query_test.go.twig +++ b/templates/go/query_test.go.twig @@ -125,4 +125,304 @@ func TestOr(t *testing.T) { t.Errorf("Expected 2 subqueries, got %d", len(values)) } } + +func TestCountWithAlias(t *testing.T) { + q := Count("*", "total") + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "count" { + t.Errorf("Expected count, got %v", data["method"]) + } + if data["attribute"] != "*" { + t.Errorf("Expected *, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if values[0] != "total" { + t.Errorf("Expected total, got %v", values[0]) + } +} + +func TestCountWithoutAlias(t *testing.T) { + q := Count("*", "") + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "count" { + t.Errorf("Expected count, got %v", data["method"]) + } + if data["attribute"] != "*" { + t.Errorf("Expected *, got %v", data["attribute"]) + } + if _, ok := data["values"]; ok { + t.Errorf("Expected no values, got %v", data["values"]) + } +} + +func TestCountDistinct(t *testing.T) { + q := CountDistinct("year", "uniqueYears") + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "countDistinct" { + t.Errorf("Expected countDistinct, got %v", data["method"]) + } + if data["attribute"] != "year" { + t.Errorf("Expected year, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if values[0] != "uniqueYears" { + t.Errorf("Expected uniqueYears, got %v", values[0]) + } +} + +func TestGroupBy(t *testing.T) { + q := GroupBy([]interface{}{"status", "year"}) + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "groupBy" { + t.Errorf("Expected groupBy, got %v", data["method"]) + } + if _, ok := data["attribute"]; ok { + t.Errorf("Expected no attribute, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if values[0] != "status" || values[1] != "year" { + t.Errorf("Expected [status, year], got %v", values) + } +} + +func TestHaving(t *testing.T) { + q := Having([]string{GreaterThan("total", 1)}) + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "having" { + t.Errorf("Expected having, got %v", data["method"]) + } + values := data["values"].([]interface{}) + inner := values[0].(map[string]interface{}) + if inner["method"] != "greaterThan" { + t.Errorf("Expected greaterThan, got %v", inner["method"]) + } + if inner["attribute"] != "total" { + t.Errorf("Expected total, got %v", inner["attribute"]) + } + if inner["values"].([]interface{})[0].(float64) != 1 { + t.Errorf("Expected [1], got %v", inner["values"]) + } +} + +func TestDistinct(t *testing.T) { + q := Distinct() + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "distinct" { + t.Errorf("Expected distinct, got %v", data["method"]) + } + if _, ok := data["attribute"]; ok { + t.Errorf("Expected no attribute, got %v", data["attribute"]) + } + if _, ok := data["values"]; ok { + t.Errorf("Expected no values, got %v", data["values"]) + } +} + +func TestJoin(t *testing.T) { + q := Join("orders", "$id", "customerId", "", "") + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "join" { + t.Errorf("Expected join, got %v", data["method"]) + } + if data["attribute"] != "orders" { + t.Errorf("Expected orders, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if values[0] != "$id" || values[1] != "=" || values[2] != "customerId" { + t.Errorf("Expected [$id, =, customerId], got %v", values) + } + if len(values) != 3 { + t.Errorf("Expected 3 values, got %d", len(values)) + } +} + +func TestJoinWithAlias(t *testing.T) { + q := LeftJoin("orders", "$id", "customerId", "=", "ord") + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "leftJoin" { + t.Errorf("Expected leftJoin, got %v", data["method"]) + } + if data["attribute"] != "orders" { + t.Errorf("Expected orders, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if values[0] != "$id" || values[1] != "=" || values[2] != "customerId" || values[3] != "ord" { + t.Errorf("Expected [$id, =, customerId, ord], got %v", values) + } +} + +func TestCrossJoin(t *testing.T) { + q := CrossJoin("orders", "ord") + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "crossJoin" { + t.Errorf("Expected crossJoin, got %v", data["method"]) + } + if data["attribute"] != "orders" { + t.Errorf("Expected orders, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if values[0] != "ord" { + t.Errorf("Expected ord, got %v", values[0]) + } +} + +func TestVectorDot(t *testing.T) { + q := VectorDot("embedding", []float64{0.1, 0.2}) + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + + if data["method"] != "vectorDot" { + t.Errorf("Expected vectorDot, got %v", data["method"]) + } + if data["attribute"] != "embedding" { + t.Errorf("Expected embedding, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + vector := values[0].([]interface{}) + if vector[0].(float64) != 0.1 || vector[1].(float64) != 0.2 { + t.Errorf("Expected [[0.1, 0.2]], got %v", values) + } +} + +func TestPage(t *testing.T) { + queries := Page(2, 10) + if len(queries) != 2 { + t.Fatalf("Expected 2 queries, got %d", len(queries)) + } + + var limit map[string]interface{} + if err := json.Unmarshal([]byte(queries[0]), &limit); err != nil { + t.Fatal(err) + } + if limit["method"] != "limit" { + t.Errorf("Expected limit, got %v", limit["method"]) + } + if limit["values"].([]interface{})[0].(float64) != 10 { + t.Errorf("Expected [10], got %v", limit["values"]) + } + + var offset map[string]interface{} + if err := json.Unmarshal([]byte(queries[1]), &offset); err != nil { + t.Fatal(err) + } + if offset["method"] != "offset" { + t.Errorf("Expected offset, got %v", offset["method"]) + } + if offset["values"].([]interface{})[0].(float64) != 10 { + t.Errorf("Expected [10], got %v", offset["values"]) + } +} + +func TestPageRejectsInvalid(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("Expected panic for page < 1") + } + }() + Page(0, 10) +} + +func TestBuilderBuild(t *testing.T) { + expected := []string{ + Equal("status", "published"), + LeftJoin("orders", "$id", "customerId", "=", "ord"), + Count("*", "total"), + GroupBy([]interface{}{"status"}), + Limit(25), + } + + built := Builder(). + Equal("status", "published"). + LeftJoin("orders", "$id", "customerId", "=", "ord"). + Count("*", "total"). + GroupBy([]interface{}{"status"}). + Limit(25). + Build() + + if len(built) != len(expected) { + t.Fatalf("Expected %d queries, got %d", len(expected), len(built)) + } + for i := range expected { + if built[i] != expected[i] { + t.Errorf("index %d: expected %s, got %s", i, expected[i], built[i]) + } + } +} + +func assertQueriesEqual(t *testing.T, got, expected []string) { + t.Helper() + if len(got) != len(expected) { + t.Fatalf("expected %d queries, got %d: %v", len(expected), len(got), got) + } + for i := range expected { + if got[i] != expected[i] { + t.Errorf("index %d: expected %s, got %s", i, expected[i], got[i]) + } + } +} + +func TestNormalizeBuilderDirect(t *testing.T) { + builder := Builder().Equal("status", "active").Limit(10) + assertQueriesEqual(t, Normalize(builder), builder.Build()) +} + +func TestNormalizeBuilderWrappedInArray(t *testing.T) { + builder := Builder().Equal("status", "active").Limit(10) + assertQueriesEqual(t, Normalize([]any{builder}), builder.Build()) +} + +func TestNormalizeBuildResultWrappedInArray(t *testing.T) { + built := Builder().Equal("status", "active").Limit(10).Build() + assertQueriesEqual(t, Normalize([]any{built}), built) +} + +func TestNormalizeMixedAndPage(t *testing.T) { + normalized := Normalize([]any{ + Equal("status", "active"), + Page(2, 10), + }) + assertQueriesEqual(t, normalized, []string{ + Equal("status", "active"), + Limit(10), + Offset(10), + }) +} {% endautoescape %} diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 7bdd89ff20..2bdd92df01 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -238,7 +238,12 @@ class Client @JvmOverloads constructor( headers: Map = mapOf(), params: Map = mapOf(), ): Request { - val filteredParams = params.filterValues { it != null } + val requestParams = if (params.containsKey("queries")) { + params + ("queries" to Query.normalize(params["queries"])) + } else { + params + } + val filteredParams = requestParams.filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) 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 312584e346..cc982d08cf 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -220,6 +220,95 @@ class Query( fun notTouches(attribute: String, values: List) = Query("notTouches", attribute, listOf(values)).toJson() + fun count(attribute: String = "*", alias: String = "") = Query("count", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun countDistinct(attribute: String, alias: String = "") = Query("countDistinct", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun sum(attribute: String, alias: String = "") = Query("sum", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun avg(attribute: String, alias: String = "") = Query("avg", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun min(attribute: String, alias: String = "") = Query("min", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun max(attribute: String, alias: String = "") = Query("max", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun stddev(attribute: String, alias: String = "") = Query("stddev", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun stddevPop(attribute: String, alias: String = "") = Query("stddevPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun stddevSamp(attribute: String, alias: String = "") = Query("stddevSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun variance(attribute: String, alias: String = "") = Query("variance", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun varPop(attribute: String, alias: String = "") = Query("varPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun varSamp(attribute: String, alias: String = "") = Query("varSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun bitAnd(attribute: String, alias: String = "") = Query("bitAnd", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun bitOr(attribute: String, alias: String = "") = Query("bitOr", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun bitXor(attribute: String, alias: String = "") = Query("bitXor", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun groupBy(attributes: List) = Query("groupBy", null, attributes).toJson() + + fun having(queries: List) = Query("having", null, queries.map { it.fromJson() }).toJson() + + fun distinct() = Query("distinct").toJson() + + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + + fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + + fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() + + fun notCovers(attribute: String, values: List) = Query("notCovers", attribute, listOf(values)).toJson() + + fun spatialEquals(attribute: String, values: List) = Query("spatialEquals", attribute, listOf(values)).toJson() + + fun notSpatialEquals(attribute: String, values: List) = Query("notSpatialEquals", attribute, listOf(values)).toJson() + + fun page(page: Int, perPage: Int = 25): List { + require(page >= 1) { "Page must be >= 1, got $page" } + require(perPage >= 1) { "Per page must be >= 1, got $perPage" } + return listOf(limit(perPage), offset((page - 1) * perPage)) + } + + fun builder() = QueryBuilder() + + fun normalize(queries: Any?): List { + if (queries == null) { + return emptyList() + } + if (queries is QueryBuilder) { + return queries.build() + } + if (queries is Query) { + return listOf(queries.toString()) + } + if (queries is String) { + return listOf(queries) + } + if (queries is Array<*>) { + return queries.flatMap { normalize(it) } + } + if (queries is Collection<*>) { + return queries.flatMap { normalize(it) } + } + return emptyList() + } + + private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { + val values = if (alias.isNotEmpty()) listOf(left, operator, right, alias) else listOf(left, operator, right) + return Query(method, table, values).toJson() + } + private fun parseValue(value: Any): List { return when (value) { is List<*> -> value as List @@ -228,3 +317,179 @@ class Query( } } } + +class QueryBuilder { + private val queries = mutableListOf() + + fun build(): List = queries.toList() + + fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) + + fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) + + fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) + + fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) + + fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) + + fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) + + fun search(attribute: String, value: String) = push(Query.search(attribute, value)) + + fun isNull(attribute: String) = push(Query.isNull(attribute)) + + fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) + + fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) + + fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) + + fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) + + fun select(attributes: List) = push(Query.select(attributes)) + + fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) + + fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) + + fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) + + fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) + + fun orderRandom() = push(Query.orderRandom()) + + fun limit(limit: Int) = push(Query.limit(limit)) + + fun offset(offset: Int) = push(Query.offset(offset)) + + fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) + + fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) + + fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) + + fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) + + fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) + + fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) + + fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) + + fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) + + fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) + + fun exists(attributes: List) = push(Query.exists(attributes)) + + fun notExists(attributes: List) = push(Query.notExists(attributes)) + + fun createdBefore(value: String) = push(Query.createdBefore(value)) + + fun createdAfter(value: String) = push(Query.createdAfter(value)) + + fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) + + fun updatedBefore(value: String) = push(Query.updatedBefore(value)) + + fun updatedAfter(value: String) = push(Query.updatedAfter(value)) + + fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) + + fun or(queries: List) = push(Query.or(queries)) + + fun and(queries: List) = push(Query.and(queries)) + + fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) + + fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) + + fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) + + fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) + + fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) + + fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) + + fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) + + fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) + + fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) + + fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) + + fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) + + fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) + + fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) + + fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) + + fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) + + fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) + + fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) + + fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) + + fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) + + fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) + + fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) + + fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) + + fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) + + fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) + + fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) + + fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) + + fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) + + fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) + + fun groupBy(attributes: List) = push(Query.groupBy(attributes)) + + fun having(queries: List) = push(Query.having(queries)) + + fun distinct() = push(Query.distinct()) + + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) + + fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) + + fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) + + fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) + + fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) + + fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) + + fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) + + fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) + + fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) + + private fun push(query: String) = apply { queries.add(query) } + + private fun push(query: List) = apply { queries.addAll(query) } +} diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 0022eca53d..66c4040e8a 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -1,6 +1,7 @@ import { fetch, FormData, File, Agent, type RequestInit } from 'undici'; import { Models } from './models'; import { InputFile } from './inputFile'; +import { Query } from './query'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true }); @@ -222,6 +223,10 @@ class Client { prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { method = method.toUpperCase(); + if (Object.prototype.hasOwnProperty.call(params, 'queries')) { + params = { ...params, queries: Query.normalize(params.queries) }; + } + headers = Object.assign({}, this.headers, headers); let options: RequestInit = { @@ -611,6 +616,6 @@ class Client { } export { Client, {{spec.info.title | caseUcfirst}}Exception }; -export { Query } from './query'; +export { Query, QueryBuilder } from './query'; export type { Models, Payload, UploadProgress }; -export type { QueryTypes, QueryTypesList } from './query'; +export type { QueryTypes, QueryTypesList, QueryInput } from './query'; diff --git a/templates/node/src/index.ts.twig b/templates/node/src/index.ts.twig index bf92f9194a..a546201a19 100644 --- a/templates/node/src/index.ts.twig +++ b/templates/node/src/index.ts.twig @@ -1,9 +1,9 @@ -export { Client, Query, {{spec.info.title | caseUcfirst}}Exception } from './client'; +export { Client, Query, QueryBuilder, {{spec.info.title | caseUcfirst}}Exception } from './client'; {% for service in services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} export type { Models, Payload, UploadProgress } from './client'; -export type { QueryTypes, QueryTypesList } from './query'; +export type { QueryTypes, QueryTypesList, QueryInput } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index e539a1f606..6c7feca3db 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -152,4 +152,93 @@ describe('Query', () => { expect(Query.offset(1)) .toEqual('{"method":"offset","values":[1]}') ); + + test('count with alias', () => + expect(Query.count('*', 'total')) + .toEqual('{"method":"count","attribute":"*","values":["total"]}') + ); + + test('count without alias', () => + expect(Query.count()) + .toEqual('{"method":"count","attribute":"*"}') + ); + + test('groupBy', () => + expect(Query.groupBy(['status', 'year'])) + .toEqual('{"method":"groupBy","values":["status","year"]}') + ); + + test('having nests parsed queries', () => { + const having = JSON.parse(Query.having([Query.greaterThan('total', 1)])); + expect(having.method).toEqual('having'); + expect(having.values[0].method).toEqual('greaterThan'); + expect(having.values[0].attribute).toEqual('total'); + expect(having.values[0].values).toEqual([1]); + }); + + test('distinct', () => + expect(Query.distinct()) + .toEqual('{"method":"distinct"}') + ); + + test('join', () => + expect(Query.join('orders', '$id', 'customerId')) + .toEqual('{"method":"join","attribute":"orders","values":["$id","=","customerId"]}') + ); + + test('leftJoin with alias', () => + expect(Query.leftJoin('orders', '$id', 'customerId', '=', 'ord')) + .toEqual('{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}') + ); + + test('crossJoin', () => + expect(Query.crossJoin('orders', 'ord')) + .toEqual('{"method":"crossJoin","attribute":"orders","values":["ord"]}') + ); + + test('vectorDot', () => + expect(Query.vectorDot('embedding', [0.1, 0.2])) + .toEqual('{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}') + ); + + test('page', () => { + const queries = Query.page(2, 10); + expect(queries).toEqual([ + '{"method":"limit","values":[10]}', + '{"method":"offset","values":[10]}', + ]); + }); + + test('builder build', () => { + const built = Query.builder() + .equal('status', 'published') + .leftJoin('orders', '$id', 'customerId', '=', 'ord') + .count('*', 'total') + .groupBy(['status']) + .limit(25) + .build(); + + expect(built).toEqual([ + Query.equal('status', 'published'), + Query.leftJoin('orders', '$id', 'customerId', '=', 'ord'), + Query.count('*', 'total'), + Query.groupBy(['status']), + Query.limit(25), + ]); + }); + + test('normalize builder wrapped in array', () => { + const builder = Query.builder().equal('status', 'active').limit(10); + expect(Query.normalize([builder])).toEqual(builder.build()); + expect(Query.normalize([builder.build()])).toEqual(builder.build()); + expect(Query.normalize(builder)).toEqual(builder.build()); + }); + + test('normalize page wrapped in array', () => { + expect(Query.normalize([Query.equal('status', 'active'), Query.page(2, 10)])).toEqual([ + Query.equal('status', 'active'), + Query.limit(10), + Query.offset(10), + ]); + }); }) diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 3f7ed30e6e..459c387d60 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -281,6 +281,9 @@ class Client } {% endif %} $headers = array_merge($this->headers, $headers); + if (\array_key_exists('queries', $params)) { + $params['queries'] = Query::normalize($params['queries']); + } $querySeparator = str_contains($path, '?') ? '&' : '?'; $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? $querySeparator . http_build_query($params) : '')); $responseHeaders = []; diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 866945105a..41c2483e9f 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -704,4 +704,233 @@ class Query implements \JsonSerializable { return (new Query('notTouches', $attribute, [$values]))->__toString(); } + + public static function count(string $attribute = '*', string $alias = ''): string + { + return (new Query('count', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function countDistinct(string $attribute, string $alias = ''): string + { + return (new Query('countDistinct', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function sum(string $attribute, string $alias = ''): string + { + return (new Query('sum', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function avg(string $attribute, string $alias = ''): string + { + return (new Query('avg', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function min(string $attribute, string $alias = ''): string + { + return (new Query('min', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function max(string $attribute, string $alias = ''): string + { + return (new Query('max', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function stddev(string $attribute, string $alias = ''): string + { + return (new Query('stddev', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function stddevPop(string $attribute, string $alias = ''): string + { + return (new Query('stddevPop', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function stddevSamp(string $attribute, string $alias = ''): string + { + return (new Query('stddevSamp', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function variance(string $attribute, string $alias = ''): string + { + return (new Query('variance', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function varPop(string $attribute, string $alias = ''): string + { + return (new Query('varPop', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function varSamp(string $attribute, string $alias = ''): string + { + return (new Query('varSamp', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function bitAnd(string $attribute, string $alias = ''): string + { + return (new Query('bitAnd', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function bitOr(string $attribute, string $alias = ''): string + { + return (new Query('bitOr', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + public static function bitXor(string $attribute, string $alias = ''): string + { + return (new Query('bitXor', $attribute, $alias !== '' ? $alias : null))->__toString(); + } + + /** + * @param array $attributes + */ + public static function groupBy(array $attributes): string + { + return (new Query('groupBy', null, $attributes))->__toString(); + } + + /** + * @param array $queries + */ + public static function having(array $queries): string + { + foreach ($queries as &$query) { + if ($query instanceof self) { + $query = $query->__toString(); + } + $query = \json_decode($query, true); + } + unset($query); + + return (new Query('having', null, $queries))->__toString(); + } + + public static function distinct(): string + { + return (new Query('distinct', null, null))->__toString(); + } + + public static function join(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + { + return self::joinQuery('join', $table, $left, $right, $operator, $alias); + } + + public static function leftJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + { + return self::joinQuery('leftJoin', $table, $left, $right, $operator, $alias); + } + + public static function rightJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + { + return self::joinQuery('rightJoin', $table, $left, $right, $operator, $alias); + } + + public static function fullOuterJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + { + return self::joinQuery('fullOuterJoin', $table, $left, $right, $operator, $alias); + } + + public static function crossJoin(string $table, string $alias = ''): string + { + return (new Query('crossJoin', $table, $alias !== '' ? $alias : null))->__toString(); + } + + /** + * @param array $values + */ + public static function covers(string $attribute, array $values): string + { + return (new Query('covers', $attribute, [$values]))->__toString(); + } + + /** + * @param array $values + */ + public static function notCovers(string $attribute, array $values): string + { + return (new Query('notCovers', $attribute, [$values]))->__toString(); + } + + /** + * @param array $values + */ + public static function spatialEquals(string $attribute, array $values): string + { + return (new Query('spatialEquals', $attribute, [$values]))->__toString(); + } + + /** + * @param array $values + */ + public static function notSpatialEquals(string $attribute, array $values): string + { + return (new Query('notSpatialEquals', $attribute, [$values]))->__toString(); + } + + /** + * @return array + */ + public static function page(int $page, int $perPage = 25): array + { + if ($page < 1) { + throw new \InvalidArgumentException('Page must be >= 1, got ' . $page); + } + if ($perPage < 1) { + throw new \InvalidArgumentException('Per page must be >= 1, got ' . $perPage); + } + + return [ + self::limit($perPage), + self::offset(($page - 1) * $perPage), + ]; + } + + public static function builder(): QueryBuilder + { + return new QueryBuilder(); + } + + /** + * Flatten a queries argument into a list of query strings. + * + * Accepts a QueryBuilder, a list of query strings, builders, or nested + * lists (the common `[$builder]` / `[$builder->build()]` mistakes). + * + * @param mixed $queries + * @return array + */ + public static function normalize(mixed $queries): array + { + if ($queries instanceof QueryBuilder) { + return $queries->build(); + } + + if (!\is_array($queries)) { + return []; + } + + $out = []; + foreach ($queries as $item) { + if ($item instanceof QueryBuilder) { + array_push($out, ...$item->build()); + } elseif ($item instanceof self) { + $out[] = $item->__toString(); + } elseif (\is_array($item)) { + array_push($out, ...self::normalize($item)); + } else { + $out[] = $item; + } + } + + return $out; + } + + private static function joinQuery(string $method, string $table, string $left, string $right, string $operator, string $alias): string + { + $values = [$left, $operator, $right]; + if ($alias !== '') { + $values[] = $alias; + } + + return (new Query($method, $table, $values))->__toString(); + } } diff --git a/templates/php/src/QueryBuilder.php.twig b/templates/php/src/QueryBuilder.php.twig new file mode 100644 index 0000000000..412315355f --- /dev/null +++ b/templates/php/src/QueryBuilder.php.twig @@ -0,0 +1,535 @@ + + */ + private array $queries = []; + + /** + * @return list + */ + public function build(): array + { + return $this->queries; + } + + public function equal(string $attribute, mixed $value): self + { + return $this->push(Query::equal($attribute, $value)); + } + + public function notEqual(string $attribute, mixed $value): self + { + return $this->push(Query::notEqual($attribute, $value)); + } + + public function lessThan(string $attribute, mixed $value): self + { + return $this->push(Query::lessThan($attribute, $value)); + } + + public function lessThanEqual(string $attribute, mixed $value): self + { + return $this->push(Query::lessThanEqual($attribute, $value)); + } + + public function greaterThan(string $attribute, mixed $value): self + { + return $this->push(Query::greaterThan($attribute, $value)); + } + + public function greaterThanEqual(string $attribute, mixed $value): self + { + return $this->push(Query::greaterThanEqual($attribute, $value)); + } + + public function search(string $attribute, string $value): self + { + return $this->push(Query::search($attribute, $value)); + } + + public function isNull(string $attribute): self + { + return $this->push(Query::isNull($attribute)); + } + + public function isNotNull(string $attribute): self + { + return $this->push(Query::isNotNull($attribute)); + } + + public function between(string $attribute, mixed $start, mixed $end): self + { + return $this->push(Query::between($attribute, $start, $end)); + } + + public function startsWith(string $attribute, string $value): self + { + return $this->push(Query::startsWith($attribute, $value)); + } + + public function endsWith(string $attribute, string $value): self + { + return $this->push(Query::endsWith($attribute, $value)); + } + + /** + * @param array $attributes + */ + public function select(array $attributes): self + { + return $this->push(Query::select($attributes)); + } + + public function cursorAfter(string $documentId): self + { + return $this->push(Query::cursorAfter($documentId)); + } + + public function cursorBefore(string $documentId): self + { + return $this->push(Query::cursorBefore($documentId)); + } + + public function orderAsc(string $attribute): self + { + return $this->push(Query::orderAsc($attribute)); + } + + public function orderDesc(string $attribute): self + { + return $this->push(Query::orderDesc($attribute)); + } + + public function orderRandom(): self + { + return $this->push(Query::orderRandom()); + } + + public function limit(int $limit): self + { + return $this->push(Query::limit($limit)); + } + + public function offset(int $offset): self + { + return $this->push(Query::offset($offset)); + } + + public function contains(string $attribute, mixed $value): self + { + return $this->push(Query::contains($attribute, $value)); + } + + /** + * @param array $value + */ + public function containsAny(string $attribute, array $value): self + { + return $this->push(Query::containsAny($attribute, $value)); + } + + /** + * @param array $value + */ + public function containsAll(string $attribute, array $value): self + { + return $this->push(Query::containsAll($attribute, $value)); + } + + public function notContains(string $attribute, mixed $value): self + { + return $this->push(Query::notContains($attribute, $value)); + } + + public function notSearch(string $attribute, string $value): self + { + return $this->push(Query::notSearch($attribute, $value)); + } + + public function notBetween(string $attribute, mixed $start, mixed $end): self + { + return $this->push(Query::notBetween($attribute, $start, $end)); + } + + public function notStartsWith(string $attribute, string $value): self + { + return $this->push(Query::notStartsWith($attribute, $value)); + } + + public function notEndsWith(string $attribute, string $value): self + { + return $this->push(Query::notEndsWith($attribute, $value)); + } + + public function regex(string $attribute, string $pattern): self + { + return $this->push(Query::regex($attribute, $pattern)); + } + + /** + * @param array $attributes + */ + public function exists(array $attributes): self + { + return $this->push(Query::exists($attributes)); + } + + /** + * @param array $attributes + */ + public function notExists(array $attributes): self + { + return $this->push(Query::notExists($attributes)); + } + + public function createdBefore(string $value): self + { + return $this->push(Query::createdBefore($value)); + } + + public function createdAfter(string $value): self + { + return $this->push(Query::createdAfter($value)); + } + + public function createdBetween(string $start, string $end): self + { + return $this->push(Query::createdBetween($start, $end)); + } + + public function updatedBefore(string $value): self + { + return $this->push(Query::updatedBefore($value)); + } + + public function updatedAfter(string $value): self + { + return $this->push(Query::updatedAfter($value)); + } + + public function updatedBetween(string $start, string $end): self + { + return $this->push(Query::updatedBetween($start, $end)); + } + + /** + * @param array $queries + */ + public function or(array $queries): self + { + return $this->push(Query::or($queries)); + } + + /** + * @param array $queries + */ + public function and(array $queries): self + { + return $this->push(Query::and($queries)); + } + + /** + * @param array $queries + */ + public function elemMatch(string $attribute, array $queries): self + { + return $this->push(Query::elemMatch($attribute, $queries)); + } + + /** + * @param array $values + */ + public function distanceEqual(string $attribute, array $values, int|float $distance, bool $meters = true): self + { + return $this->push(Query::distanceEqual($attribute, $values, $distance, $meters)); + } + + /** + * @param array $values + */ + public function distanceNotEqual(string $attribute, array $values, int|float $distance, bool $meters = true): self + { + return $this->push(Query::distanceNotEqual($attribute, $values, $distance, $meters)); + } + + /** + * @param array $values + */ + public function distanceGreaterThan(string $attribute, array $values, int|float $distance, bool $meters = true): self + { + return $this->push(Query::distanceGreaterThan($attribute, $values, $distance, $meters)); + } + + /** + * @param array $values + */ + public function distanceLessThan(string $attribute, array $values, int|float $distance, bool $meters = true): self + { + return $this->push(Query::distanceLessThan($attribute, $values, $distance, $meters)); + } + + /** + * @param array $values + */ + public function intersects(string $attribute, array $values): self + { + return $this->push(Query::intersects($attribute, $values)); + } + + /** + * @param array $values + */ + public function notIntersects(string $attribute, array $values): self + { + return $this->push(Query::notIntersects($attribute, $values)); + } + + /** + * @param array $values + */ + public function crosses(string $attribute, array $values): self + { + return $this->push(Query::crosses($attribute, $values)); + } + + /** + * @param array $values + */ + public function notCrosses(string $attribute, array $values): self + { + return $this->push(Query::notCrosses($attribute, $values)); + } + + /** + * @param array $values + */ + public function overlaps(string $attribute, array $values): self + { + return $this->push(Query::overlaps($attribute, $values)); + } + + /** + * @param array $values + */ + public function notOverlaps(string $attribute, array $values): self + { + return $this->push(Query::notOverlaps($attribute, $values)); + } + + /** + * @param array $values + */ + public function touches(string $attribute, array $values): self + { + return $this->push(Query::touches($attribute, $values)); + } + + /** + * @param array $values + */ + public function notTouches(string $attribute, array $values): self + { + return $this->push(Query::notTouches($attribute, $values)); + } + + public function count(string $attribute = '*', string $alias = ''): self + { + return $this->push(Query::count($attribute, $alias)); + } + + public function countDistinct(string $attribute, string $alias = ''): self + { + return $this->push(Query::countDistinct($attribute, $alias)); + } + + public function sum(string $attribute, string $alias = ''): self + { + return $this->push(Query::sum($attribute, $alias)); + } + + public function avg(string $attribute, string $alias = ''): self + { + return $this->push(Query::avg($attribute, $alias)); + } + + public function min(string $attribute, string $alias = ''): self + { + return $this->push(Query::min($attribute, $alias)); + } + + public function max(string $attribute, string $alias = ''): self + { + return $this->push(Query::max($attribute, $alias)); + } + + public function stddev(string $attribute, string $alias = ''): self + { + return $this->push(Query::stddev($attribute, $alias)); + } + + public function stddevPop(string $attribute, string $alias = ''): self + { + return $this->push(Query::stddevPop($attribute, $alias)); + } + + public function stddevSamp(string $attribute, string $alias = ''): self + { + return $this->push(Query::stddevSamp($attribute, $alias)); + } + + public function variance(string $attribute, string $alias = ''): self + { + return $this->push(Query::variance($attribute, $alias)); + } + + public function varPop(string $attribute, string $alias = ''): self + { + return $this->push(Query::varPop($attribute, $alias)); + } + + public function varSamp(string $attribute, string $alias = ''): self + { + return $this->push(Query::varSamp($attribute, $alias)); + } + + public function bitAnd(string $attribute, string $alias = ''): self + { + return $this->push(Query::bitAnd($attribute, $alias)); + } + + public function bitOr(string $attribute, string $alias = ''): self + { + return $this->push(Query::bitOr($attribute, $alias)); + } + + public function bitXor(string $attribute, string $alias = ''): self + { + return $this->push(Query::bitXor($attribute, $alias)); + } + + /** + * @param array $attributes + */ + public function groupBy(array $attributes): self + { + return $this->push(Query::groupBy($attributes)); + } + + /** + * @param array $queries + */ + public function having(array $queries): self + { + return $this->push(Query::having($queries)); + } + + public function distinct(): self + { + return $this->push(Query::distinct()); + } + + public function join(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + { + return $this->push(Query::join($table, $left, $right, $operator, $alias)); + } + + public function leftJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + { + return $this->push(Query::leftJoin($table, $left, $right, $operator, $alias)); + } + + public function rightJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + { + return $this->push(Query::rightJoin($table, $left, $right, $operator, $alias)); + } + + public function fullOuterJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + { + return $this->push(Query::fullOuterJoin($table, $left, $right, $operator, $alias)); + } + + public function crossJoin(string $table, string $alias = ''): self + { + return $this->push(Query::crossJoin($table, $alias)); + } + + /** + * @param array $vector + */ + public function vectorDot(string $attribute, array $vector): self + { + return $this->push(Query::vectorDot($attribute, $vector)); + } + + /** + * @param array $vector + */ + public function vectorCosine(string $attribute, array $vector): self + { + return $this->push(Query::vectorCosine($attribute, $vector)); + } + + /** + * @param array $vector + */ + public function vectorEuclidean(string $attribute, array $vector): self + { + return $this->push(Query::vectorEuclidean($attribute, $vector)); + } + + /** + * @param array $values + */ + public function covers(string $attribute, array $values): self + { + return $this->push(Query::covers($attribute, $values)); + } + + /** + * @param array $values + */ + public function notCovers(string $attribute, array $values): self + { + return $this->push(Query::notCovers($attribute, $values)); + } + + /** + * @param array $values + */ + public function spatialEquals(string $attribute, array $values): self + { + return $this->push(Query::spatialEquals($attribute, $values)); + } + + /** + * @param array $values + */ + public function notSpatialEquals(string $attribute, array $values): self + { + return $this->push(Query::notSpatialEquals($attribute, $values)); + } + + public function page(int $page, int $perPage = 25): self + { + return $this->push(Query::page($page, $perPage)); + } + + /** + * @param string|list $query + */ + private function push(string|array $query): self + { + if (\is_array($query)) { + array_push($this->queries, ...$query); + } else { + $this->queries[] = $query; + } + + return $this; + } +} diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index b10bab7ce0..ec400ce00a 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -323,4 +323,152 @@ final class QueryTest extends TestCase { $this->assertSame(['2023-01-01', '2023-12-31'], $query['values']); $this->assertSame('between', $query['method']); } + + public function testCountWithAlias(): void { + $query = json_decode(Query::count('*', 'total'), true); + $this->assertSame('count', $query['method']); + $this->assertSame('*', $query['attribute']); + $this->assertSame(['total'], $query['values']); + } + + public function testCountWithoutAlias(): void { + $query = json_decode(Query::count(), true); + $this->assertSame('count', $query['method']); + $this->assertSame('*', $query['attribute']); + $this->assertArrayNotHasKey('values', $query); + } + + public function testCountDistinct(): void { + $query = json_decode(Query::countDistinct('year', 'uniqueYears'), true); + $this->assertSame('countDistinct', $query['method']); + $this->assertSame('year', $query['attribute']); + $this->assertSame(['uniqueYears'], $query['values']); + } + + public function testSumAvgMinMax(): void { + $this->assertSame('sum', json_decode(Query::sum('price', 'total'), true)['method']); + $this->assertSame('avg', json_decode(Query::avg('price', 'avgPrice'), true)['method']); + $this->assertSame('min', json_decode(Query::min('price', 'lowest'), true)['method']); + $this->assertSame('max', json_decode(Query::max('price', 'highest'), true)['method']); + } + + public function testGroupBy(): void { + $query = json_decode(Query::groupBy(['status', 'year']), true); + $this->assertSame('groupBy', $query['method']); + $this->assertSame(['status', 'year'], $query['values']); + $this->assertArrayNotHasKey('attribute', $query); + } + + public function testHaving(): void { + $query = json_decode(Query::having([Query::greaterThan('total', 1)]), true); + $this->assertSame('having', $query['method']); + $this->assertSame('greaterThan', $query['values'][0]['method']); + $this->assertSame('total', $query['values'][0]['attribute']); + $this->assertSame([1], $query['values'][0]['values']); + } + + public function testDistinct(): void { + $query = json_decode(Query::distinct(), true); + $this->assertSame('distinct', $query['method']); + $this->assertArrayNotHasKey('attribute', $query); + $this->assertArrayNotHasKey('values', $query); + } + + public function testJoin(): void { + $query = json_decode(Query::join('orders', '$id', 'customerId'), true); + $this->assertSame('join', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + + public function testJoinWithAlias(): void { + $query = json_decode(Query::leftJoin('orders', '$id', 'customerId', '=', 'ord'), true); + $this->assertSame('leftJoin', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame(['$id', '=', 'customerId', 'ord'], $query['values']); + } + + public function testCrossJoin(): void { + $query = json_decode(Query::crossJoin('orders', 'ord'), true); + $this->assertSame('crossJoin', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame(['ord'], $query['values']); + } + + public function testVectorDot(): void { + $query = json_decode(Query::vectorDot('embedding', [0.1, 0.2]), true); + $this->assertSame('vectorDot', $query['method']); + $this->assertSame('embedding', $query['attribute']); + $this->assertSame([[0.1, 0.2]], $query['values']); + } + + public function testCovers(): void { + $query = json_decode(Query::covers('location', [0, 0]), true); + $this->assertSame('covers', $query['method']); + $this->assertSame([[0, 0]], $query['values']); + } + + public function testPage(): void { + $queries = Query::page(2, 10); + $this->assertCount(2, $queries); + $limit = json_decode($queries[0], true); + $offset = json_decode($queries[1], true); + $this->assertSame('limit', $limit['method']); + $this->assertSame([10], $limit['values']); + $this->assertSame('offset', $offset['method']); + $this->assertSame([10], $offset['values']); + } + + public function testPageRejectsInvalid(): void { + $this->expectException(\InvalidArgumentException::class); + Query::page(0, 10); + } + + public function testBuilderBuild(): void { + $expected = [ + Query::equal('status', 'published'), + Query::leftJoin('orders', '$id', 'customerId', '=', 'ord'), + Query::count('*', 'total'), + Query::groupBy(['status']), + Query::limit(25), + ]; + + $built = Query::builder() + ->equal('status', 'published') + ->leftJoin('orders', '$id', 'customerId', '=', 'ord') + ->count('*', 'total') + ->groupBy(['status']) + ->limit(25) + ->build(); + + $this->assertSame($expected, $built); + } + + public function testNormalizeBuilderDirect(): void { + $builder = Query::builder()->equal('status', 'active')->limit(10); + $this->assertSame($builder->build(), Query::normalize($builder)); + } + + public function testNormalizeBuilderWrappedInArray(): void { + $builder = Query::builder()->equal('status', 'active')->limit(10); + $this->assertSame($builder->build(), Query::normalize([$builder])); + } + + public function testNormalizeBuildResultWrappedInArray(): void { + $built = Query::builder()->equal('status', 'active')->limit(10)->build(); + $this->assertSame($built, Query::normalize([$built])); + } + + public function testNormalizeMixedAndPage(): void { + $normalized = Query::normalize([ + Query::equal('status', 'active'), + Query::page(2, 10), + ]); + + $this->assertSame([ + Query::equal('status', 'active'), + Query::limit(10), + Query::offset(10), + ], $normalized); + } } diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index 4d7229f12e..82e3807568 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -9,6 +9,7 @@ from threading import Lock from .input_file import InputFile from .exception import {{spec.info.title | caseUcfirst}}Exception from .encoders.value_class_encoder import ValueClassEncoder +from .query import Query class Client: def __init__(self): @@ -69,6 +70,9 @@ class Client: if params is None: params = {} + if 'queries' in params: + params['queries'] = Query.normalize(params['queries']) + data = {} files = {} stringify = False diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index 3dafd4aa0b..c8e24db89c 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -291,3 +291,419 @@ class Query(): @staticmethod def not_touches(attribute, values): return str(Query("notTouches", attribute, [values])) + + @staticmethod + def count(attribute='*', alias=''): + return str(Query("count", attribute, alias if alias != '' else None)) + + @staticmethod + def count_distinct(attribute, alias=''): + return str(Query("countDistinct", attribute, alias if alias != '' else None)) + + @staticmethod + def sum(attribute, alias=''): + return str(Query("sum", attribute, alias if alias != '' else None)) + + @staticmethod + def avg(attribute, alias=''): + return str(Query("avg", attribute, alias if alias != '' else None)) + + @staticmethod + def min(attribute, alias=''): + return str(Query("min", attribute, alias if alias != '' else None)) + + @staticmethod + def max(attribute, alias=''): + return str(Query("max", attribute, alias if alias != '' else None)) + + @staticmethod + def stddev(attribute, alias=''): + return str(Query("stddev", attribute, alias if alias != '' else None)) + + @staticmethod + def stddev_pop(attribute, alias=''): + return str(Query("stddevPop", attribute, alias if alias != '' else None)) + + @staticmethod + def stddev_samp(attribute, alias=''): + return str(Query("stddevSamp", attribute, alias if alias != '' else None)) + + @staticmethod + def variance(attribute, alias=''): + return str(Query("variance", attribute, alias if alias != '' else None)) + + @staticmethod + def var_pop(attribute, alias=''): + return str(Query("varPop", attribute, alias if alias != '' else None)) + + @staticmethod + def var_samp(attribute, alias=''): + return str(Query("varSamp", attribute, alias if alias != '' else None)) + + @staticmethod + def bit_and(attribute, alias=''): + return str(Query("bitAnd", attribute, alias if alias != '' else None)) + + @staticmethod + def bit_or(attribute, alias=''): + return str(Query("bitOr", attribute, alias if alias != '' else None)) + + @staticmethod + def bit_xor(attribute, alias=''): + return str(Query("bitXor", attribute, alias if alias != '' else None)) + + @staticmethod + def group_by(attributes): + return str(Query("groupBy", None, attributes)) + + @staticmethod + def having(queries): + return str(Query("having", None, [json.loads(query) for query in queries])) + + @staticmethod + def distinct(): + return str(Query("distinct", None, None)) + + @staticmethod + def join(table, left, right, operator='=', alias=''): + return Query._join("join", table, left, right, operator, alias) + + @staticmethod + def left_join(table, left, right, operator='=', alias=''): + return Query._join("leftJoin", table, left, right, operator, alias) + + @staticmethod + def right_join(table, left, right, operator='=', alias=''): + return Query._join("rightJoin", table, left, right, operator, alias) + + @staticmethod + def full_outer_join(table, left, right, operator='=', alias=''): + return Query._join("fullOuterJoin", table, left, right, operator, alias) + + @staticmethod + def cross_join(table, alias=''): + return str(Query("crossJoin", table, alias if alias != '' else None)) + + @staticmethod + def covers(attribute, values): + return str(Query("covers", attribute, [values])) + + @staticmethod + def not_covers(attribute, values): + return str(Query("notCovers", attribute, [values])) + + @staticmethod + def spatial_equals(attribute, values): + return str(Query("spatialEquals", attribute, [values])) + + @staticmethod + def not_spatial_equals(attribute, values): + return str(Query("notSpatialEquals", attribute, [values])) + + @staticmethod + def page(page, per_page=25): + if page < 1: + raise ValueError(f'Page must be >= 1, got {page}') + if per_page < 1: + raise ValueError(f'Per page must be >= 1, got {per_page}') + + return [ + Query.limit(per_page), + Query.offset((page - 1) * per_page), + ] + + @staticmethod + def builder(): + return QueryBuilder() + + @staticmethod + def normalize(queries): + if isinstance(queries, QueryBuilder): + return queries.build() + + if not isinstance(queries, list): + return [] + + out = [] + for item in queries: + if isinstance(item, QueryBuilder): + out.extend(item.build()) + elif isinstance(item, Query): + out.append(str(item)) + elif isinstance(item, list): + out.extend(Query.normalize(item)) + else: + out.append(item) + + return out + + @staticmethod + def _join(method, table, left, right, operator, alias): + values = [left, operator, right] + if alias != '': + values.append(alias) + return str(Query(method, table, values)) + +class QueryBuilder: + def __init__(self): + self._queries = [] + + def build(self): + return list(self._queries) + + def equal(self, attribute, value): + return self._push(Query.equal(attribute, value)) + + def not_equal(self, attribute, value): + return self._push(Query.not_equal(attribute, value)) + + def less_than(self, attribute, value): + return self._push(Query.less_than(attribute, value)) + + def less_than_equal(self, attribute, value): + return self._push(Query.less_than_equal(attribute, value)) + + def greater_than(self, attribute, value): + return self._push(Query.greater_than(attribute, value)) + + def greater_than_equal(self, attribute, value): + return self._push(Query.greater_than_equal(attribute, value)) + + def search(self, attribute, value): + return self._push(Query.search(attribute, value)) + + def is_null(self, attribute): + return self._push(Query.is_null(attribute)) + + def is_not_null(self, attribute): + return self._push(Query.is_not_null(attribute)) + + def between(self, attribute, start, end): + return self._push(Query.between(attribute, start, end)) + + def starts_with(self, attribute, value): + return self._push(Query.starts_with(attribute, value)) + + def ends_with(self, attribute, value): + return self._push(Query.ends_with(attribute, value)) + + def select(self, attributes): + return self._push(Query.select(attributes)) + + def cursor_after(self, id): + return self._push(Query.cursor_after(id)) + + def cursor_before(self, id): + return self._push(Query.cursor_before(id)) + + def order_asc(self, attribute): + return self._push(Query.order_asc(attribute)) + + def order_desc(self, attribute): + return self._push(Query.order_desc(attribute)) + + def order_random(self): + return self._push(Query.order_random()) + + def limit(self, limit): + return self._push(Query.limit(limit)) + + def offset(self, offset): + return self._push(Query.offset(offset)) + + def contains(self, attribute, value): + return self._push(Query.contains(attribute, value)) + + def contains_any(self, attribute, value): + return self._push(Query.contains_any(attribute, value)) + + def contains_all(self, attribute, value): + return self._push(Query.contains_all(attribute, value)) + + def not_contains(self, attribute, value): + return self._push(Query.not_contains(attribute, value)) + + def not_search(self, attribute, value): + return self._push(Query.not_search(attribute, value)) + + def not_between(self, attribute, start, end): + return self._push(Query.not_between(attribute, start, end)) + + def not_starts_with(self, attribute, value): + return self._push(Query.not_starts_with(attribute, value)) + + def not_ends_with(self, attribute, value): + return self._push(Query.not_ends_with(attribute, value)) + + def regex(self, attribute, pattern): + return self._push(Query.regex(attribute, pattern)) + + def exists(self, attributes): + return self._push(Query.exists(attributes)) + + def not_exists(self, attributes): + return self._push(Query.not_exists(attributes)) + + def created_before(self, value): + return self._push(Query.created_before(value)) + + def created_after(self, value): + return self._push(Query.created_after(value)) + + def created_between(self, start, end): + return self._push(Query.created_between(start, end)) + + def updated_before(self, value): + return self._push(Query.updated_before(value)) + + def updated_after(self, value): + return self._push(Query.updated_after(value)) + + def updated_between(self, start, end): + return self._push(Query.updated_between(start, end)) + + def or_queries(self, queries): + return self._push(Query.or_queries(queries)) + + def and_queries(self, queries): + return self._push(Query.and_queries(queries)) + + def elem_match(self, attribute, queries): + return self._push(Query.elem_match(attribute, queries)) + + def distance_equal(self, attribute, values, distance, meters=True): + return self._push(Query.distance_equal(attribute, values, distance, meters)) + + def distance_not_equal(self, attribute, values, distance, meters=True): + return self._push(Query.distance_not_equal(attribute, values, distance, meters)) + + def distance_greater_than(self, attribute, values, distance, meters=True): + return self._push(Query.distance_greater_than(attribute, values, distance, meters)) + + def distance_less_than(self, attribute, values, distance, meters=True): + return self._push(Query.distance_less_than(attribute, values, distance, meters)) + + def intersects(self, attribute, values): + return self._push(Query.intersects(attribute, values)) + + def not_intersects(self, attribute, values): + return self._push(Query.not_intersects(attribute, values)) + + def crosses(self, attribute, values): + return self._push(Query.crosses(attribute, values)) + + def not_crosses(self, attribute, values): + return self._push(Query.not_crosses(attribute, values)) + + def overlaps(self, attribute, values): + return self._push(Query.overlaps(attribute, values)) + + def not_overlaps(self, attribute, values): + return self._push(Query.not_overlaps(attribute, values)) + + def touches(self, attribute, values): + return self._push(Query.touches(attribute, values)) + + def not_touches(self, attribute, values): + return self._push(Query.not_touches(attribute, values)) + + def count(self, attribute='*', alias=''): + return self._push(Query.count(attribute, alias)) + + def count_distinct(self, attribute, alias=''): + return self._push(Query.count_distinct(attribute, alias)) + + def sum(self, attribute, alias=''): + return self._push(Query.sum(attribute, alias)) + + def avg(self, attribute, alias=''): + return self._push(Query.avg(attribute, alias)) + + def min(self, attribute, alias=''): + return self._push(Query.min(attribute, alias)) + + def max(self, attribute, alias=''): + return self._push(Query.max(attribute, alias)) + + def stddev(self, attribute, alias=''): + return self._push(Query.stddev(attribute, alias)) + + def stddev_pop(self, attribute, alias=''): + return self._push(Query.stddev_pop(attribute, alias)) + + def stddev_samp(self, attribute, alias=''): + return self._push(Query.stddev_samp(attribute, alias)) + + def variance(self, attribute, alias=''): + return self._push(Query.variance(attribute, alias)) + + def var_pop(self, attribute, alias=''): + return self._push(Query.var_pop(attribute, alias)) + + def var_samp(self, attribute, alias=''): + return self._push(Query.var_samp(attribute, alias)) + + def bit_and(self, attribute, alias=''): + return self._push(Query.bit_and(attribute, alias)) + + def bit_or(self, attribute, alias=''): + return self._push(Query.bit_or(attribute, alias)) + + def bit_xor(self, attribute, alias=''): + return self._push(Query.bit_xor(attribute, alias)) + + def group_by(self, attributes): + return self._push(Query.group_by(attributes)) + + def having(self, queries): + return self._push(Query.having(queries)) + + def distinct(self): + return self._push(Query.distinct()) + + def join(self, table, left, right, operator='=', alias=''): + return self._push(Query.join(table, left, right, operator, alias)) + + def left_join(self, table, left, right, operator='=', alias=''): + return self._push(Query.left_join(table, left, right, operator, alias)) + + def right_join(self, table, left, right, operator='=', alias=''): + return self._push(Query.right_join(table, left, right, operator, alias)) + + def full_outer_join(self, table, left, right, operator='=', alias=''): + return self._push(Query.full_outer_join(table, left, right, operator, alias)) + + def cross_join(self, table, alias=''): + return self._push(Query.cross_join(table, alias)) + + def vector_dot(self, attribute, vector): + return self._push(Query.vector_dot(attribute, vector)) + + def vector_cosine(self, attribute, vector): + return self._push(Query.vector_cosine(attribute, vector)) + + def vector_euclidean(self, attribute, vector): + return self._push(Query.vector_euclidean(attribute, vector)) + + def covers(self, attribute, values): + return self._push(Query.covers(attribute, values)) + + def not_covers(self, attribute, values): + return self._push(Query.not_covers(attribute, values)) + + def spatial_equals(self, attribute, values): + return self._push(Query.spatial_equals(attribute, values)) + + def not_spatial_equals(self, attribute, values): + return self._push(Query.not_spatial_equals(attribute, values)) + + def page(self, page, per_page=25): + return self._push(Query.page(page, per_page)) + + def _push(self, query): + if isinstance(query, list): + self._queries.extend(query) + else: + self._queries.append(query) + + return self diff --git a/templates/python/test/test_query.py.twig b/templates/python/test/test_query.py.twig index 9f5d299388..5e56cfbab5 100644 --- a/templates/python/test/test_query.py.twig +++ b/templates/python/test/test_query.py.twig @@ -1,6 +1,6 @@ import unittest -from {{ namespace | caseSnake }}.query import Query +from {{ namespace | caseSnake }}.query import Query class BasicFilterQueryTest: def __init__(self, description: str, value, expected_values: str): @@ -105,3 +105,35 @@ class TestQueryMethods(unittest.TestCase): def test_offset(self): self.assertEqual(Query.offset(1), '{"method":"offset","values":[1]}') + + def test_count(self): + self.assertEqual(Query.count('*', 'total'), '{"method":"count","attribute":"*","values":["total"]}') + + def test_join(self): + self.assertEqual(Query.join('orders', '$id', 'customerId'), '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}') + + def test_group_by(self): + self.assertEqual(Query.group_by(['status']), '{"method":"groupBy","values":["status"]}') + + def test_distinct(self): + self.assertEqual(Query.distinct(), '{"method":"distinct"}') + + def test_covers(self): + self.assertEqual(Query.covers('location', [1, 2]), '{"method":"covers","attribute":"location","values":[[1,2]]}') + + def test_builder_build(self): + expected = [ + Query.equal('status', 'published'), + Query.left_join('orders', '$id', 'customerId', '=', 'ord'), + Query.count('*', 'total'), + Query.group_by(['status']), + Query.limit(25), + ] + built = Query.builder().equal('status', 'published').left_join('orders', '$id', 'customerId', '=', 'ord').count('*', 'total').group_by(['status']).limit(25).build() + self.assertEqual(expected, built) + + def test_normalize_builder(self): + builder = Query.builder().equal('status', 'active').limit(10) + self.assertEqual(builder.build(), Query.normalize(builder)) + self.assertEqual(builder.build(), Query.normalize([builder])) + self.assertEqual(builder.build(), Query.normalize([builder.build()])) diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 1ea649aad0..c2cc48e976 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -569,6 +569,10 @@ class Client { async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}, responseType = 'json'): Promise { method = method.toUpperCase(); + if (Object.prototype.hasOwnProperty.call(params, 'queries')) { + params = { ...params, queries: Query.normalize(params.queries) }; + } + headers = Object.assign({}, this.headers, headers); headers.Origin = `{{ spec.info.title | caseLower }}-${Platform.OS}://${this.config.platform}` diff --git a/templates/react-native/src/index.ts.twig b/templates/react-native/src/index.ts.twig index b43c5d6548..f7b42d5549 100644 --- a/templates/react-native/src/index.ts.twig +++ b/templates/react-native/src/index.ts.twig @@ -5,8 +5,8 @@ export { {{service.name | caseUcfirst}} } from './services/{{service.name | case export { Realtime } from './services/realtime'; export type { Models, Payload, RealtimeResponseEvent, UploadProgress } from './client'; export type { RealtimeSubscription } from './services/realtime'; -export type { QueryTypes, QueryTypesList } from './query'; -export { Query } from './query'; +export type { QueryTypes, QueryTypesList, QueryInput } from './query'; +export { Query, QueryBuilder } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index a77df1becd..60cc99def7 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -445,4 +445,548 @@ export class Query { */ static notTouches = (attribute: string, values: any[]): string => new Query("notTouches", attribute, [values]).toString(); + + /** + * Count documents, optionally aliased. + */ + static count = (attribute: string = "*", alias: string = ""): string => + new Query("count", attribute, alias !== "" ? alias : undefined).toString(); + + /** + * Count distinct values of attribute, optionally aliased. + */ + static countDistinct = (attribute: string, alias: string = ""): string => + new Query("countDistinct", attribute, alias !== "" ? alias : undefined).toString(); + + static sum = (attribute: string, alias: string = ""): string => + new Query("sum", attribute, alias !== "" ? alias : undefined).toString(); + + static avg = (attribute: string, alias: string = ""): string => + new Query("avg", attribute, alias !== "" ? alias : undefined).toString(); + + static min = (attribute: string, alias: string = ""): string => + new Query("min", attribute, alias !== "" ? alias : undefined).toString(); + + static max = (attribute: string, alias: string = ""): string => + new Query("max", attribute, alias !== "" ? alias : undefined).toString(); + + static stddev = (attribute: string, alias: string = ""): string => + new Query("stddev", attribute, alias !== "" ? alias : undefined).toString(); + + static stddevPop = (attribute: string, alias: string = ""): string => + new Query("stddevPop", attribute, alias !== "" ? alias : undefined).toString(); + + static stddevSamp = (attribute: string, alias: string = ""): string => + new Query("stddevSamp", attribute, alias !== "" ? alias : undefined).toString(); + + static variance = (attribute: string, alias: string = ""): string => + new Query("variance", attribute, alias !== "" ? alias : undefined).toString(); + + static varPop = (attribute: string, alias: string = ""): string => + new Query("varPop", attribute, alias !== "" ? alias : undefined).toString(); + + static varSamp = (attribute: string, alias: string = ""): string => + new Query("varSamp", attribute, alias !== "" ? alias : undefined).toString(); + + static bitAnd = (attribute: string, alias: string = ""): string => + new Query("bitAnd", attribute, alias !== "" ? alias : undefined).toString(); + + static bitOr = (attribute: string, alias: string = ""): string => + new Query("bitOr", attribute, alias !== "" ? alias : undefined).toString(); + + static bitXor = (attribute: string, alias: string = ""): string => + new Query("bitXor", attribute, alias !== "" ? alias : undefined).toString(); + + /** + * Group results by attributes. + */ + static groupBy = (attributes: string[]): string => + new Query("groupBy", undefined, attributes).toString(); + + /** + * Filter grouped results. `queries` are nested like `and`/`or`. + */ + static having = (queries: string[]): string => + new Query( + "having", + undefined, + queries.map((query) => JSONbig.parse(query)) + ).toString(); + + static distinct = (): string => + new Query("distinct").toString(); + + /** + * Inner join. `table` is the joined collection/table ID. + */ + static join = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "join", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static leftJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "leftJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static rightJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "rightJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static fullOuterJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "fullOuterJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static crossJoin = (table: string, alias: string = ""): string => + new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); + + static covers = (attribute: string, values: any[]): string => + new Query("covers", attribute, [values]).toString(); + + static notCovers = (attribute: string, values: any[]): string => + new Query("notCovers", attribute, [values]).toString(); + + static spatialEquals = (attribute: string, values: any[]): string => + new Query("spatialEquals", attribute, [values]).toString(); + + static notSpatialEquals = (attribute: string, values: any[]): string => + new Query("notSpatialEquals", attribute, [values]).toString(); + + /** + * Return limit and offset queries for 1-based page pagination. + */ + static page = (page: number, perPage: number = 25): string[] => { + if (page < 1) { + throw new Error("Page must be >= 1, got " + page); + } + if (perPage < 1) { + throw new Error("Per page must be >= 1, got " + perPage); + } + return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; + }; + + static builder = (): QueryBuilder => new QueryBuilder(); + + /** + * Flatten a queries argument into query strings. + * + * Accepts a QueryBuilder, a list of query strings, builders, or nested + * lists (the common `[builder]` / `[builder.build()]` mistakes). + */ + static normalize = (queries?: QueryInput | null): string[] => { + if (queries == null) { + return []; + } + if (queries instanceof QueryBuilder) { + return queries.build(); + } + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === "string") { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...Query.normalize(item)); + } + return out; + }; +} + +export type QueryInput = + | string + | Query + | QueryBuilder + | QueryInput[]; + +/** + * Fluent collector of Query fragments. Call {@link QueryBuilder.build} to + * produce the `string[]` expected by list/createQuery methods. + */ +export class QueryBuilder { + private readonly queries: string[] = []; + + build(): string[] { + return [...this.queries]; + } + + equal(attribute: string, value: QueryTypes): this { + return this.push(Query.equal(attribute, value)); + } + + notEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.notEqual(attribute, value)); + } + + lessThan(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThan(attribute, value)); + } + + lessThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThanEqual(attribute, value)); + } + + greaterThan(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThan(attribute, value)); + } + + greaterThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThanEqual(attribute, value)); + } + + search(attribute: string, value: string): this { + return this.push(Query.search(attribute, value)); + } + + isNull(attribute: string): this { + return this.push(Query.isNull(attribute)); + } + + isNotNull(attribute: string): this { + return this.push(Query.isNotNull(attribute)); + } + + between(attribute: string, start: string | number | bigint, end: string | number | bigint): this { + return this.push(Query.between(attribute, start, end)); + } + + startsWith(attribute: string, value: string): this { + return this.push(Query.startsWith(attribute, value)); + } + + endsWith(attribute: string, value: string): this { + return this.push(Query.endsWith(attribute, value)); + } + + select(attributes: string[]): this { + return this.push(Query.select(attributes)); + } + + cursorAfter(documentId: string): this { + return this.push(Query.cursorAfter(documentId)); + } + + cursorBefore(documentId: string): this { + return this.push(Query.cursorBefore(documentId)); + } + + orderAsc(attribute: string): this { + return this.push(Query.orderAsc(attribute)); + } + + orderDesc(attribute: string): this { + return this.push(Query.orderDesc(attribute)); + } + + orderRandom(): this { + return this.push(Query.orderRandom()); + } + + limit(limit: number): this { + return this.push(Query.limit(limit)); + } + + offset(offset: number): this { + return this.push(Query.offset(offset)); + } + + contains(attribute: string, value: string | any[]): this { + return this.push(Query.contains(attribute, value)); + } + + containsAny(attribute: string, value: any[]): this { + return this.push(Query.containsAny(attribute, value)); + } + + containsAll(attribute: string, value: any[]): this { + return this.push(Query.containsAll(attribute, value)); + } + + notContains(attribute: string, value: string | any[]): this { + return this.push(Query.notContains(attribute, value)); + } + + notSearch(attribute: string, value: string): this { + return this.push(Query.notSearch(attribute, value)); + } + + notBetween(attribute: string, start: string | number | bigint, end: string | number | bigint): this { + return this.push(Query.notBetween(attribute, start, end)); + } + + notStartsWith(attribute: string, value: string): this { + return this.push(Query.notStartsWith(attribute, value)); + } + + notEndsWith(attribute: string, value: string): this { + return this.push(Query.notEndsWith(attribute, value)); + } + + regex(attribute: string, pattern: string): this { + return this.push(Query.regex(attribute, pattern)); + } + + exists(attributes: string[]): this { + return this.push(Query.exists(attributes)); + } + + notExists(attributes: string[]): this { + return this.push(Query.notExists(attributes)); + } + + createdBefore(value: string): this { + return this.push(Query.createdBefore(value)); + } + + createdAfter(value: string): this { + return this.push(Query.createdAfter(value)); + } + + createdBetween(start: string, end: string): this { + return this.push(Query.createdBetween(start, end)); + } + + updatedBefore(value: string): this { + return this.push(Query.updatedBefore(value)); + } + + updatedAfter(value: string): this { + return this.push(Query.updatedAfter(value)); + } + + updatedBetween(start: string, end: string): this { + return this.push(Query.updatedBetween(start, end)); + } + + or(queries: string[]): this { + return this.push(Query.or(queries)); + } + + and(queries: string[]): this { + return this.push(Query.and(queries)); + } + + elemMatch(attribute: string, queries: string[]): this { + return this.push(Query.elemMatch(attribute, queries)); + } + + distanceEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceEqual(attribute, values, distance, meters)); + } + + distanceNotEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceNotEqual(attribute, values, distance, meters)); + } + + distanceGreaterThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceGreaterThan(attribute, values, distance, meters)); + } + + distanceLessThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceLessThan(attribute, values, distance, meters)); + } + + intersects(attribute: string, values: any[]): this { + return this.push(Query.intersects(attribute, values)); + } + + notIntersects(attribute: string, values: any[]): this { + return this.push(Query.notIntersects(attribute, values)); + } + + crosses(attribute: string, values: any[]): this { + return this.push(Query.crosses(attribute, values)); + } + + notCrosses(attribute: string, values: any[]): this { + return this.push(Query.notCrosses(attribute, values)); + } + + overlaps(attribute: string, values: any[]): this { + return this.push(Query.overlaps(attribute, values)); + } + + notOverlaps(attribute: string, values: any[]): this { + return this.push(Query.notOverlaps(attribute, values)); + } + + touches(attribute: string, values: any[]): this { + return this.push(Query.touches(attribute, values)); + } + + notTouches(attribute: string, values: any[]): this { + return this.push(Query.notTouches(attribute, values)); + } + + count(attribute: string = "*", alias: string = ""): this { + return this.push(Query.count(attribute, alias)); + } + + countDistinct(attribute: string, alias: string = ""): this { + return this.push(Query.countDistinct(attribute, alias)); + } + + sum(attribute: string, alias: string = ""): this { + return this.push(Query.sum(attribute, alias)); + } + + avg(attribute: string, alias: string = ""): this { + return this.push(Query.avg(attribute, alias)); + } + + min(attribute: string, alias: string = ""): this { + return this.push(Query.min(attribute, alias)); + } + + max(attribute: string, alias: string = ""): this { + return this.push(Query.max(attribute, alias)); + } + + stddev(attribute: string, alias: string = ""): this { + return this.push(Query.stddev(attribute, alias)); + } + + stddevPop(attribute: string, alias: string = ""): this { + return this.push(Query.stddevPop(attribute, alias)); + } + + stddevSamp(attribute: string, alias: string = ""): this { + return this.push(Query.stddevSamp(attribute, alias)); + } + + variance(attribute: string, alias: string = ""): this { + return this.push(Query.variance(attribute, alias)); + } + + varPop(attribute: string, alias: string = ""): this { + return this.push(Query.varPop(attribute, alias)); + } + + varSamp(attribute: string, alias: string = ""): this { + return this.push(Query.varSamp(attribute, alias)); + } + + bitAnd(attribute: string, alias: string = ""): this { + return this.push(Query.bitAnd(attribute, alias)); + } + + bitOr(attribute: string, alias: string = ""): this { + return this.push(Query.bitOr(attribute, alias)); + } + + bitXor(attribute: string, alias: string = ""): this { + return this.push(Query.bitXor(attribute, alias)); + } + + groupBy(attributes: string[]): this { + return this.push(Query.groupBy(attributes)); + } + + having(queries: string[]): this { + return this.push(Query.having(queries)); + } + + distinct(): this { + return this.push(Query.distinct()); + } + + join(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.join(table, left, right, operator, alias)); + } + + leftJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.leftJoin(table, left, right, operator, alias)); + } + + rightJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.rightJoin(table, left, right, operator, alias)); + } + + fullOuterJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.fullOuterJoin(table, left, right, operator, alias)); + } + + crossJoin(table: string, alias: string = ""): this { + return this.push(Query.crossJoin(table, alias)); + } + + vectorDot(attribute: string, vector: number[]): this { + return this.push(Query.vectorDot(attribute, vector)); + } + + vectorCosine(attribute: string, vector: number[]): this { + return this.push(Query.vectorCosine(attribute, vector)); + } + + vectorEuclidean(attribute: string, vector: number[]): this { + return this.push(Query.vectorEuclidean(attribute, vector)); + } + + covers(attribute: string, values: any[]): this { + return this.push(Query.covers(attribute, values)); + } + + notCovers(attribute: string, values: any[]): this { + return this.push(Query.notCovers(attribute, values)); + } + + spatialEquals(attribute: string, values: any[]): this { + return this.push(Query.spatialEquals(attribute, values)); + } + + notSpatialEquals(attribute: string, values: any[]): this { + return this.push(Query.notSpatialEquals(attribute, values)); + } + + page(page: number, perPage: number = 25): this { + return this.push(Query.page(page, perPage)); + } + + private push(query: string | string[]): this { + if (Array.isArray(query)) { + this.queries.push(...query); + } else { + this.queries.push(query); + } + return this; + } } diff --git a/templates/ruby/lib/container/client.rb.twig b/templates/ruby/lib/container/client.rb.twig index 532d5036c1..66ff8f39d2 100644 --- a/templates/ruby/lib/container/client.rb.twig +++ b/templates/ruby/lib/container/client.rb.twig @@ -111,6 +111,12 @@ module {{ spec.info.title | caseUcfirst }} params: {}, response_type: nil ) + if params.key?(:queries) && !params[:queries].nil? + params[:queries] = Query.normalize(params[:queries]) + elsif params.key?('queries') && !params['queries'].nil? + params['queries'] = Query.normalize(params['queries']) + end + separator = path.include?('?') ? '&' : '?' uri = URI.parse(@endpoint + path + ((method == "GET" && params.length) ? separator + encode(params) : '')) diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index 667dbca4e9..65d81d83f6 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -275,6 +275,520 @@ module {{spec.info.title | caseUcfirst}} def not_touches(attribute, values) return Query.new("notTouches", attribute, [values]).to_s end + + def count(attribute = '*', alias_name = '') + return Query.new("count", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def count_distinct(attribute, alias_name = '') + return Query.new("countDistinct", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def sum(attribute, alias_name = '') + return Query.new("sum", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def avg(attribute, alias_name = '') + return Query.new("avg", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def min(attribute, alias_name = '') + return Query.new("min", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def max(attribute, alias_name = '') + return Query.new("max", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def stddev(attribute, alias_name = '') + return Query.new("stddev", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def stddev_pop(attribute, alias_name = '') + return Query.new("stddevPop", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def stddev_samp(attribute, alias_name = '') + return Query.new("stddevSamp", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def variance(attribute, alias_name = '') + return Query.new("variance", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def var_pop(attribute, alias_name = '') + return Query.new("varPop", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def var_samp(attribute, alias_name = '') + return Query.new("varSamp", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def bit_and(attribute, alias_name = '') + return Query.new("bitAnd", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def bit_or(attribute, alias_name = '') + return Query.new("bitOr", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def bit_xor(attribute, alias_name = '') + return Query.new("bitXor", attribute, alias_name != '' ? alias_name : nil).to_s + end + + def group_by(attributes) + return Query.new("groupBy", nil, attributes).to_s + end + + def having(queries) + return Query.new("having", nil, queries.map { |query| JSON.parse(query) }).to_s + end + + def distinct() + return Query.new("distinct", nil, nil).to_s + end + + def join(table, left, right, operator = '=', alias_name = '') + return join_query("join", table, left, right, operator, alias_name) + end + + def left_join(table, left, right, operator = '=', alias_name = '') + return join_query("leftJoin", table, left, right, operator, alias_name) + end + + def right_join(table, left, right, operator = '=', alias_name = '') + return join_query("rightJoin", table, left, right, operator, alias_name) + end + + def full_outer_join(table, left, right, operator = '=', alias_name = '') + return join_query("fullOuterJoin", table, left, right, operator, alias_name) + end + + def cross_join(table, alias_name = '') + return Query.new("crossJoin", table, alias_name != '' ? alias_name : nil).to_s + end + + def covers(attribute, values) + return Query.new("covers", attribute, [values]).to_s + end + + def not_covers(attribute, values) + return Query.new("notCovers", attribute, [values]).to_s + end + + def spatial_equals(attribute, values) + return Query.new("spatialEquals", attribute, [values]).to_s + end + + def not_spatial_equals(attribute, values) + return Query.new("notSpatialEquals", attribute, [values]).to_s + end + + def page(page, per_page = 25) + if page < 1 + raise ArgumentError, "Page must be >= 1, got #{page}" + end + if per_page < 1 + raise ArgumentError, "Per page must be >= 1, got #{per_page}" + end + + return [ + limit(per_page), + offset((page - 1) * per_page), + ] + end + + def builder + return QueryBuilder.new + end + + def normalize(queries) + if queries.is_a?(QueryBuilder) + return queries.build + end + + unless queries.is_a?(Array) + return [] + end + + out = [] + queries.each do |item| + if item.is_a?(QueryBuilder) + out.concat(item.build) + elsif item.is_a?(Query) + out << item.to_s + elsif item.is_a?(Array) + out.concat(normalize(item)) + else + out << item + end + end + + return out + end + + private + + def join_query(method, table, left, right, operator, alias_name) + values = [left, operator, right] + values << alias_name if alias_name != '' + + return Query.new(method, table, values).to_s + end + end + end + + class QueryBuilder + def initialize + @queries = [] + end + + def build + return @queries.dup + end + + def equal(attribute, value) + return push(Query.equal(attribute, value)) + end + + def not_equal(attribute, value) + return push(Query.not_equal(attribute, value)) + end + + def less_than(attribute, value) + return push(Query.less_than(attribute, value)) + end + + def less_than_equal(attribute, value) + return push(Query.less_than_equal(attribute, value)) + end + + def greater_than(attribute, value) + return push(Query.greater_than(attribute, value)) + end + + def greater_than_equal(attribute, value) + return push(Query.greater_than_equal(attribute, value)) + end + + def search(attribute, value) + return push(Query.search(attribute, value)) + end + + def is_null(attribute) + return push(Query.is_null(attribute)) + end + + def is_not_null(attribute) + return push(Query.is_not_null(attribute)) + end + + def between(attribute, start, ending) + return push(Query.between(attribute, start, ending)) + end + + def starts_with(attribute, value) + return push(Query.starts_with(attribute, value)) + end + + def ends_with(attribute, value) + return push(Query.ends_with(attribute, value)) + end + + def select(attributes) + return push(Query.select(attributes)) + end + + def cursor_after(id) + return push(Query.cursor_after(id)) + end + + def cursor_before(id) + return push(Query.cursor_before(id)) + end + + def order_asc(attribute) + return push(Query.order_asc(attribute)) + end + + def order_desc(attribute) + return push(Query.order_desc(attribute)) + end + + def order_random() + return push(Query.order_random()) + end + + def limit(limit) + return push(Query.limit(limit)) + end + + def offset(offset) + return push(Query.offset(offset)) + end + + def contains(attribute, value) + return push(Query.contains(attribute, value)) + end + + def contains_any(attribute, value) + return push(Query.contains_any(attribute, value)) + end + + def contains_all(attribute, value) + return push(Query.contains_all(attribute, value)) + end + + def not_contains(attribute, value) + return push(Query.not_contains(attribute, value)) + end + + def not_search(attribute, value) + return push(Query.not_search(attribute, value)) + end + + def not_between(attribute, start, ending) + return push(Query.not_between(attribute, start, ending)) + end + + def not_starts_with(attribute, value) + return push(Query.not_starts_with(attribute, value)) + end + + def not_ends_with(attribute, value) + return push(Query.not_ends_with(attribute, value)) + end + + def regex(attribute, pattern) + return push(Query.regex(attribute, pattern)) + end + + def exists(attributes) + return push(Query.exists(attributes)) + end + + def not_exists(attributes) + return push(Query.not_exists(attributes)) + end + + def created_before(value) + return push(Query.created_before(value)) + end + + def created_after(value) + return push(Query.created_after(value)) + end + + def created_between(start, ending) + return push(Query.created_between(start, ending)) + end + + def updated_before(value) + return push(Query.updated_before(value)) + end + + def updated_after(value) + return push(Query.updated_after(value)) + end + + def updated_between(start, ending) + return push(Query.updated_between(start, ending)) + end + + def or(queries) + return push(Query.or(queries)) + end + + def and(queries) + return push(Query.and(queries)) + end + + def elem_match(attribute, queries) + return push(Query.elem_match(attribute, queries)) + end + + def distance_equal(attribute, values, distance, meters = true) + return push(Query.distance_equal(attribute, values, distance, meters)) + end + + def distance_not_equal(attribute, values, distance, meters = true) + return push(Query.distance_not_equal(attribute, values, distance, meters)) + end + + def distance_greater_than(attribute, values, distance, meters = true) + return push(Query.distance_greater_than(attribute, values, distance, meters)) + end + + def distance_less_than(attribute, values, distance, meters = true) + return push(Query.distance_less_than(attribute, values, distance, meters)) + end + + def intersects(attribute, values) + return push(Query.intersects(attribute, values)) + end + + def not_intersects(attribute, values) + return push(Query.not_intersects(attribute, values)) + end + + def crosses(attribute, values) + return push(Query.crosses(attribute, values)) + end + + def not_crosses(attribute, values) + return push(Query.not_crosses(attribute, values)) + end + + def overlaps(attribute, values) + return push(Query.overlaps(attribute, values)) + end + + def not_overlaps(attribute, values) + return push(Query.not_overlaps(attribute, values)) + end + + def touches(attribute, values) + return push(Query.touches(attribute, values)) + end + + def not_touches(attribute, values) + return push(Query.not_touches(attribute, values)) + end + + def count(attribute = '*', alias_name = '') + return push(Query.count(attribute, alias_name)) + end + + def count_distinct(attribute, alias_name = '') + return push(Query.count_distinct(attribute, alias_name)) + end + + def sum(attribute, alias_name = '') + return push(Query.sum(attribute, alias_name)) + end + + def avg(attribute, alias_name = '') + return push(Query.avg(attribute, alias_name)) + end + + def min(attribute, alias_name = '') + return push(Query.min(attribute, alias_name)) + end + + def max(attribute, alias_name = '') + return push(Query.max(attribute, alias_name)) + end + + def stddev(attribute, alias_name = '') + return push(Query.stddev(attribute, alias_name)) + end + + def stddev_pop(attribute, alias_name = '') + return push(Query.stddev_pop(attribute, alias_name)) + end + + def stddev_samp(attribute, alias_name = '') + return push(Query.stddev_samp(attribute, alias_name)) + end + + def variance(attribute, alias_name = '') + return push(Query.variance(attribute, alias_name)) + end + + def var_pop(attribute, alias_name = '') + return push(Query.var_pop(attribute, alias_name)) + end + + def var_samp(attribute, alias_name = '') + return push(Query.var_samp(attribute, alias_name)) + end + + def bit_and(attribute, alias_name = '') + return push(Query.bit_and(attribute, alias_name)) + end + + def bit_or(attribute, alias_name = '') + return push(Query.bit_or(attribute, alias_name)) + end + + def bit_xor(attribute, alias_name = '') + return push(Query.bit_xor(attribute, alias_name)) + end + + def group_by(attributes) + return push(Query.group_by(attributes)) + end + + def having(queries) + return push(Query.having(queries)) + end + + def distinct() + return push(Query.distinct()) + end + + def join(table, left, right, operator = '=', alias_name = '') + return push(Query.join(table, left, right, operator, alias_name)) + end + + def left_join(table, left, right, operator = '=', alias_name = '') + return push(Query.left_join(table, left, right, operator, alias_name)) + end + + def right_join(table, left, right, operator = '=', alias_name = '') + return push(Query.right_join(table, left, right, operator, alias_name)) + end + + def full_outer_join(table, left, right, operator = '=', alias_name = '') + return push(Query.full_outer_join(table, left, right, operator, alias_name)) + end + + def cross_join(table, alias_name = '') + return push(Query.cross_join(table, alias_name)) + end + + def vector_dot(attribute, vector) + return push(Query.vector_dot(attribute, vector)) + end + + def vector_cosine(attribute, vector) + return push(Query.vector_cosine(attribute, vector)) + end + + def vector_euclidean(attribute, vector) + return push(Query.vector_euclidean(attribute, vector)) + end + + def covers(attribute, values) + return push(Query.covers(attribute, values)) + end + + def not_covers(attribute, values) + return push(Query.not_covers(attribute, values)) + end + + def spatial_equals(attribute, values) + return push(Query.spatial_equals(attribute, values)) + end + + def not_spatial_equals(attribute, values) + return push(Query.not_spatial_equals(attribute, values)) + end + + def page(page, per_page = 25) + return push(Query.page(page, per_page)) + end + + private + + def push(query) + if query.is_a?(Array) + @queries.concat(query) + else + @queries << query + end + + return self end end end diff --git a/templates/rust/src/client.rs.twig b/templates/rust/src/client.rs.twig index 75827da04e..9cd2f7e94e 100644 --- a/templates/rust/src/client.rs.twig +++ b/templates/rust/src/client.rs.twig @@ -3,6 +3,7 @@ use crate::error::{{ spec.info.title | caseUcfirst }}Error; use crate::error::Result; use crate::input_file::InputFile; +use crate::query::Query; use arc_swap::ArcSwap; use reqwest::{header::HeaderMap, multipart, Client as HttpClient, Method, Response}; use serde::de::DeserializeOwned; @@ -344,6 +345,17 @@ impl Client { result } + fn normalize_request_params(mut params: HashMap) -> HashMap { + if let Some(queries) = params.get("queries").cloned() { + let normalized = Query::normalize_value(&queries) + .into_iter() + .map(Value::String) + .collect::>(); + params.insert("queries".to_string(), Value::Array(normalized)); + } + params + } + pub async fn call( &self, method: Method, @@ -355,7 +367,7 @@ impl Client { let url = format!("{}{}", state.config.endpoint, path); let mut request_builder; - if let Some(params) = params { + if let Some(params) = params.map(Self::normalize_request_params) { if method == Method::GET { let mut url_with_params = Url::parse(&url).map_err(|e| {{ spec.info.title | caseUcfirst }}Error::new(0, format!("Invalid URL: {}", e), None, String::new()))?; { @@ -406,7 +418,7 @@ impl Client { } } - if let Some(params) = params { + if let Some(params) = params.map(Self::normalize_request_params) { if method == Method::GET { let mut url_with_params = Url::parse(&url).map_err(|e| {{ spec.info.title | caseUcfirst }}Error::new(0, format!("Invalid URL: {}", e), None, String::new()))?; { @@ -493,7 +505,7 @@ impl Client { let url = format!("{}{}", state.config.endpoint, path); let mut request_builder; - if let Some(params) = params { + if let Some(params) = params.map(Self::normalize_request_params) { if method == Method::GET { let mut url_with_params = Url::parse(&url).map_err(|e| {{ spec.info.title | caseUcfirst }}Error::new(0, format!("Invalid URL: {}", e), None, String::new()))?; { diff --git a/templates/rust/src/query.rs.twig b/templates/rust/src/query.rs.twig index cb4dbabd49..b99687aac6 100644 --- a/templates/rust/src/query.rs.twig +++ b/templates/rust/src/query.rs.twig @@ -279,6 +279,212 @@ impl Query { Self::new("elemMatch".to_string(), Some(attribute.into()), values) } + pub fn count(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("count", attribute, alias) + } + + pub fn count_distinct(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("countDistinct", attribute, alias) + } + + pub fn sum(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("sum", attribute, alias) + } + + pub fn avg(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("avg", attribute, alias) + } + + pub fn min(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("min", attribute, alias) + } + + pub fn max(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("max", attribute, alias) + } + + pub fn stddev(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("stddev", attribute, alias) + } + + pub fn stddev_pop(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("stddevPop", attribute, alias) + } + + pub fn stddev_samp(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("stddevSamp", attribute, alias) + } + + pub fn variance(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("variance", attribute, alias) + } + + pub fn var_pop(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("varPop", attribute, alias) + } + + pub fn var_samp(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("varSamp", attribute, alias) + } + + pub fn bit_and(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("bitAnd", attribute, alias) + } + + pub fn bit_or(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("bitOr", attribute, alias) + } + + pub fn bit_xor(attribute: impl Into, alias: impl Into) -> Self { + Self::aliased("bitXor", attribute, alias) + } + + pub fn group_by(attributes: I) -> Self + where + I: IntoIterator, + S: Into, + { + let values: Vec = attributes.into_iter().map(|s| Value::String(s.into())).collect(); + Self::new("groupBy".to_string(), None, values) + } + + pub fn having(queries: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let values: Vec = queries + .into_iter() + .filter_map(|query| serde_json::from_str(query.as_ref()).ok()) + .collect(); + Self::new("having".to_string(), None, values) + } + + pub fn distinct() -> Self { + Self::new("distinct".to_string(), None, vec![]) + } + + pub fn join( + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + Self::join_query("join", table, left, right, operator, alias) + } + + pub fn left_join( + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + Self::join_query("leftJoin", table, left, right, operator, alias) + } + + pub fn right_join( + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + Self::join_query("rightJoin", table, left, right, operator, alias) + } + + pub fn full_outer_join( + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + Self::join_query("fullOuterJoin", table, left, right, operator, alias) + } + + pub fn cross_join(table: impl Into, alias: impl Into) -> Self { + Self::aliased("crossJoin", table, alias) + } + + pub fn covers, V: Into>(attribute: S, values: V) -> Self { + Self::new("covers".to_string(), Some(attribute.into()), vec![values.into()]) + } + + pub fn not_covers, V: Into>(attribute: S, values: V) -> Self { + Self::new("notCovers".to_string(), Some(attribute.into()), vec![values.into()]) + } + + pub fn spatial_equals, V: Into>(attribute: S, values: V) -> Self { + Self::new("spatialEquals".to_string(), Some(attribute.into()), vec![values.into()]) + } + + pub fn not_spatial_equals, V: Into>(attribute: S, values: V) -> Self { + Self::new("notSpatialEquals".to_string(), Some(attribute.into()), vec![values.into()]) + } + + pub fn page(page: u32, per_page: u32) -> Vec { + if page < 1 { + panic!("Page must be >= 1, got {}", page); + } + if per_page < 1 { + panic!("Per page must be >= 1, got {}", per_page); + } + vec![Self::limit(per_page), Self::offset((page - 1) * per_page)] + } + + pub fn builder() -> QueryBuilder { + QueryBuilder::new() + } + + pub fn normalize(value: impl Serialize) -> Vec { + match serde_json::to_value(value) { + Ok(v) => Self::normalize_value(&v), + Err(_) => vec![], + } + } + + pub fn normalize_value(value: &Value) -> Vec { + match value { + Value::Null => vec![], + Value::String(s) => vec![s.clone()], + Value::Array(items) => items.iter().flat_map(Self::normalize_value).collect(), + Value::Object(_) => vec![value.to_string()], + _ => vec![], + } + } + + fn aliased(method: &str, attribute: impl Into, alias: impl Into) -> Self { + let alias = alias.into(); + let values = if alias.is_empty() { + vec![] + } else { + vec![Value::String(alias)] + }; + Self::new(method.to_string(), Some(attribute.into()), values) + } + + fn join_query( + method: &str, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + let mut values = vec![ + Value::String(left.into()), + Value::String(operator.into()), + Value::String(right.into()), + ]; + let alias = alias.into(); + if !alias.is_empty() { + values.push(Value::String(alias)); + } + Self::new(method.to_string(), Some(table.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(); @@ -304,6 +510,494 @@ impl std::fmt::Display for Query { } } +#[derive(Debug, Clone, Default)] +pub struct QueryBuilder { + queries: Vec, +} + +impl QueryBuilder { + pub fn new() -> Self { + Self { queries: Vec::new() } + } + + pub fn build(&self) -> Vec { + self.queries.iter().map(|query| query.to_string()).collect() + } + + pub fn equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::equal(attribute, value)); + self + } + + pub fn not_equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_equal(attribute, value)); + self + } + + pub fn less_than, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::less_than(attribute, value)); + self + } + + pub fn less_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::less_than_equal(attribute, value)); + self + } + + pub fn greater_than, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::greater_than(attribute, value)); + self + } + + pub fn greater_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::greater_than_equal(attribute, value)); + self + } + + pub fn is_null>(mut self, attribute: S) -> Self { + self.queries.push(Query::is_null(attribute)); + self + } + + pub fn is_not_null>(mut self, attribute: S) -> Self { + self.queries.push(Query::is_not_null(attribute)); + self + } + + pub fn between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { + self.queries.push(Query::between(attribute, start, end)); + self + } + + pub fn starts_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::starts_with(attribute, value)); + self + } + + pub fn ends_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::ends_with(attribute, value)); + self + } + + pub fn select, S: Into>(mut self, attributes: I) -> Self { + self.queries.push(Query::select(attributes)); + self + } + + pub fn search, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::search(attribute, value)); + self + } + + pub fn not_search, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_search(attribute, value)); + self + } + + pub fn order_asc>(mut self, attribute: S) -> Self { + self.queries.push(Query::order_asc(attribute)); + self + } + + pub fn order_desc>(mut self, attribute: S) -> Self { + self.queries.push(Query::order_desc(attribute)); + self + } + + pub fn order_random(mut self) -> Self { + self.queries.push(Query::order_random()); + self + } + + pub fn cursor_after>(mut self, document_id: S) -> Self { + self.queries.push(Query::cursor_after(document_id)); + self + } + + pub fn cursor_before>(mut self, document_id: S) -> Self { + self.queries.push(Query::cursor_before(document_id)); + self + } + + pub fn limit(mut self, value: u32) -> Self { + self.queries.push(Query::limit(value)); + self + } + + pub fn offset(mut self, value: u32) -> Self { + self.queries.push(Query::offset(value)); + self + } + + pub fn contains, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::contains(attribute, value)); + self + } + + pub fn contains_any, V: Into>(mut self, attribute: S, values: Vec) -> Self { + self.queries.push(Query::contains_any(attribute, values)); + self + } + + pub fn contains_all, V: Into>(mut self, attribute: S, values: Vec) -> Self { + self.queries.push(Query::contains_all(attribute, values)); + self + } + + pub fn not_contains, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_contains(attribute, value)); + self + } + + pub fn not_between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { + self.queries.push(Query::not_between(attribute, start, end)); + self + } + + pub fn not_starts_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_starts_with(attribute, value)); + self + } + + pub fn not_ends_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_ends_with(attribute, value)); + self + } + + pub fn created_before>(mut self, value: V) -> Self { + self.queries.push(Query::created_before(value)); + self + } + + pub fn created_after>(mut self, value: V) -> Self { + self.queries.push(Query::created_after(value)); + self + } + + pub fn created_between>(mut self, start: V, end: V) -> Self { + self.queries.push(Query::created_between(start, end)); + self + } + + pub fn updated_before>(mut self, value: V) -> Self { + self.queries.push(Query::updated_before(value)); + self + } + + pub fn updated_after>(mut self, value: V) -> Self { + self.queries.push(Query::updated_after(value)); + self + } + + pub fn updated_between>(mut self, start: V, end: V) -> Self { + self.queries.push(Query::updated_between(start, end)); + self + } + + pub fn distance_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_equal(attribute, values, distance, meters)); + self + } + + pub fn distance_not_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_not_equal(attribute, values, distance, meters)); + self + } + + pub fn distance_greater_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_greater_than(attribute, values, distance, meters)); + self + } + + pub fn distance_less_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_less_than(attribute, values, distance, meters)); + self + } + + pub fn intersects, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::intersects(attribute, values)); + self + } + + pub fn not_intersects, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_intersects(attribute, values)); + self + } + + pub fn crosses, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::crosses(attribute, values)); + self + } + + pub fn not_crosses, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_crosses(attribute, values)); + self + } + + pub fn overlaps, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::overlaps(attribute, values)); + self + } + + pub fn not_overlaps, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_overlaps(attribute, values)); + self + } + + pub fn touches, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::touches(attribute, values)); + self + } + + pub fn not_touches, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_touches(attribute, values)); + self + } + + pub fn or(mut self, queries: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::or(queries)); + self + } + + pub fn and(mut self, queries: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::and(queries)); + self + } + + pub fn regex, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::regex(attribute, value)); + self + } + + pub fn exists>(mut self, attributes: Vec) -> Self { + self.queries.push(Query::exists(attributes)); + self + } + + pub fn not_exists>(mut self, attributes: Vec) -> Self { + self.queries.push(Query::not_exists(attributes)); + self + } + + pub fn elem_match(mut self, attribute: S, queries: I) -> Self + where + S: Into, + I: IntoIterator, + Q: AsRef, + { + self.queries.push(Query::elem_match(attribute, queries)); + self + } + + pub fn count(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::count(attribute, alias)); + self + } + + pub fn count_distinct(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::count_distinct(attribute, alias)); + self + } + + pub fn sum(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::sum(attribute, alias)); + self + } + + pub fn avg(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::avg(attribute, alias)); + self + } + + pub fn min(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::min(attribute, alias)); + self + } + + pub fn max(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::max(attribute, alias)); + self + } + + pub fn stddev(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::stddev(attribute, alias)); + self + } + + pub fn stddev_pop(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::stddev_pop(attribute, alias)); + self + } + + pub fn stddev_samp(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::stddev_samp(attribute, alias)); + self + } + + pub fn variance(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::variance(attribute, alias)); + self + } + + pub fn var_pop(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::var_pop(attribute, alias)); + self + } + + pub fn var_samp(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::var_samp(attribute, alias)); + self + } + + pub fn bit_and(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::bit_and(attribute, alias)); + self + } + + pub fn bit_or(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::bit_or(attribute, alias)); + self + } + + pub fn bit_xor(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::bit_xor(attribute, alias)); + self + } + + pub fn group_by(mut self, attributes: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.queries.push(Query::group_by(attributes)); + self + } + + pub fn having(mut self, queries: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::having(queries)); + self + } + + pub fn distinct(mut self) -> Self { + self.queries.push(Query::distinct()); + self + } + + pub fn join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::join(table, left, right, operator, alias)); + self + } + + pub fn left_join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::left_join(table, left, right, operator, alias)); + self + } + + pub fn right_join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::right_join(table, left, right, operator, alias)); + self + } + + pub fn full_outer_join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::full_outer_join(table, left, right, operator, alias)); + self + } + + pub fn cross_join(mut self, table: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::cross_join(table, alias)); + self + } + + pub fn vector_dot, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); + self.queries.push(Query::vector_dot(attribute, values)); + self + } + + pub fn vector_cosine, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); + self.queries.push(Query::vector_cosine(attribute, values)); + self + } + + pub fn vector_euclidean, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); + self.queries.push(Query::vector_euclidean(attribute, values)); + self + } + + pub fn covers, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::covers(attribute, values)); + self + } + + pub fn not_covers, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_covers(attribute, values)); + self + } + + pub fn spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::spatial_equals(attribute, values)); + self + } + + pub fn not_spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_spatial_equals(attribute, values)); + self + } + + pub fn page(mut self, page: u32, per_page: u32) -> Self { + self.queries.extend(Query::page(page, per_page)); + self + } +} + +impl Serialize for QueryBuilder { + fn serialize(&self, serializer: S) -> Result { + self.build().serialize(serializer) + } +} + #[cfg(test)] mod tests { use super::*; @@ -882,4 +1576,142 @@ mod tests { assert_eq!(value["attribute"], "content"); assert_eq!(value["values"][0], "spam keyword"); } + + #[test] + fn test_count_with_alias() { + let query = Query::count("*", "total"); + let value = query.to_value(); + assert_eq!(value["method"], "count"); + assert_eq!(value["attribute"], "*"); + assert_eq!(value["values"][0], "total"); + } + + #[test] + fn test_count_without_alias() { + let query = Query::count("*", ""); + let value = query.to_value(); + assert_eq!(value["method"], "count"); + assert_eq!(value["attribute"], "*"); + assert!(value.get("values").is_none()); + } + + #[test] + fn test_group_by() { + let query = Query::group_by(vec!["status", "year"]); + let value = query.to_value(); + assert_eq!(value["method"], "groupBy"); + assert_eq!(value["values"][0], "status"); + assert_eq!(value["values"][1], "year"); + } + + #[test] + fn test_having() { + let query = Query::having(vec![Query::greater_than("total", 1).to_string()]); + let value = query.to_value(); + assert_eq!(value["method"], "having"); + assert_eq!(value["values"][0]["method"], "greaterThan"); + assert_eq!(value["values"][0]["attribute"], "total"); + assert_eq!(value["values"][0]["values"][0], 1); + } + + #[test] + fn test_distinct() { + let query = Query::distinct(); + let value = query.to_value(); + assert_eq!(value["method"], "distinct"); + } + + #[test] + fn test_join() { + let query = Query::join("orders", "$id", "customerId", "=", ""); + let value = query.to_value(); + assert_eq!(value["method"], "join"); + assert_eq!(value["attribute"], "orders"); + assert_eq!(value["values"][0], "$id"); + assert_eq!(value["values"][1], "="); + assert_eq!(value["values"][2], "customerId"); + assert_eq!(value["values"].as_array().unwrap().len(), 3); + } + + #[test] + fn test_left_join_with_alias() { + let query = Query::left_join("orders", "$id", "customerId", "=", "ord"); + let value = query.to_value(); + assert_eq!(value["method"], "leftJoin"); + assert_eq!(value["attribute"], "orders"); + assert_eq!(value["values"][3], "ord"); + } + + #[test] + fn test_cross_join() { + let query = Query::cross_join("orders", "ord"); + let value = query.to_value(); + assert_eq!(value["method"], "crossJoin"); + assert_eq!(value["attribute"], "orders"); + assert_eq!(value["values"][0], "ord"); + } + + #[test] + fn test_page() { + let queries = Query::page(2, 10); + assert_eq!(queries.len(), 2); + let limit = queries[0].clone().to_value(); + let offset = queries[1].clone().to_value(); + assert_eq!(limit["method"], "limit"); + assert_eq!(limit["values"][0], 10); + assert_eq!(offset["method"], "offset"); + assert_eq!(offset["values"][0], 10); + } + + #[test] + fn test_builder_build() { + let built = Query::builder() + .equal("status", "published") + .left_join("orders", "$id", "customerId", "=", "ord") + .count("*", "total") + .group_by(vec!["status"]) + .limit(25) + .build(); + + assert_eq!( + built, + vec![ + Query::equal("status", "published").to_string(), + Query::left_join("orders", "$id", "customerId", "=", "ord").to_string(), + Query::count("*", "total").to_string(), + Query::group_by(vec!["status"]).to_string(), + Query::limit(25).to_string(), + ] + ); + } + + #[test] + fn test_normalize_builder() { + let builder = Query::builder().equal("status", "active").limit(10); + let expected = builder.build(); + assert_eq!(Query::normalize(builder.clone()), expected); + assert_eq!(Query::normalize(vec![builder.clone()]), expected); + assert_eq!(Query::normalize(vec![builder.build()]), expected); + } + + #[test] + fn test_normalize_page_wrapped_in_array() { + let normalized = Query::normalize(vec![ + Value::String(Query::equal("status", "active").to_string()), + Value::Array( + Query::page(2, 10) + .into_iter() + .map(|query| Value::String(query.to_string())) + .collect(), + ), + ]); + assert_eq!( + normalized, + vec![ + Query::equal("status", "active").to_string(), + Query::limit(10).to_string(), + Query::offset(10).to_string(), + ] + ); + } } diff --git a/templates/rust/tests/tests.rs b/templates/rust/tests/tests.rs index 2821bc301d..1614d52a15 100644 --- a/templates/rust/tests/tests.rs +++ b/templates/rust/tests/tests.rs @@ -293,6 +293,12 @@ fn test_queries() { Query::equal("name", "Alice").to_string(), Query::greater_than("age", 18).to_string(), ])); + println!("{}", Query::count("*", "total")); + println!("{}", Query::join("orders", "$id", "customerId", "=", "")); + println!("{}", Query::group_by(vec!["status"])); + println!("{}", Query::distinct()); + println!("{}", Query::covers("location", json!([1, 2]))); + println!("{}", Query::builder().limit(1).build()[0]); } fn test_permission_helpers() { diff --git a/templates/skills/dart.md.twig b/templates/skills/dart.md.twig index 751d63ce0d..c70bdd23cb 100644 --- a/templates/skills/dart.md.twig +++ b/templates/skills/dart.md.twig @@ -189,6 +189,44 @@ 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) + +// Aggregations (optional alias) +Query.count('*', 'total') +Query.sum('amount', 'revenue') +Query.avg('score', 'avgScore') +Query.min('price', 'minPrice') +Query.max('price', 'maxPrice') + +// Grouping +Query.groupBy(['status', 'year']) +Query.having([Query.greaterThan('total', 1)]) +Query.distinct() + +// Joins (table id, left key, right key, operator, alias) +Query.join('orders', r'$id', 'customerId') +Query.leftJoin('orders', r'$id', 'customerId', '=', 'ord') + +// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +final queries = Query.builder() + .equal('status', 'published') + .leftJoin('orders', r'$id', 'customerId', '=', 'ord') + .count('*', 'total') + .groupBy(['status']) + .limit(25) + .build(); + +await tablesDB.listRows( + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + queries: queries, // or [Query.builder().equal('status', 'active')] +); + +// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. +await tablesDB.createQuery( + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + queries: queries, +); ``` ### File Storage diff --git a/templates/skills/dotnet.md.twig b/templates/skills/dotnet.md.twig index 2cd7350986..0f9ca1c669 100644 --- a/templates/skills/dotnet.md.twig +++ b/templates/skills/dotnet.md.twig @@ -132,6 +132,37 @@ 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) + +// Aggregations (optional alias) +Query.Count("*", "total") +Query.Sum("amount", "revenue") +Query.Avg("score", "avgScore") +Query.Min("price", "minPrice") +Query.Max("price", "maxPrice") + +// Grouping +Query.GroupBy(new List { "status", "year" }) +Query.Having(new List { Query.GreaterThan("total", 1) }) +Query.Distinct() + +// Joins (table id, left key, right key, operator, alias) +Query.Join("orders", "$id", "customerId") +Query.LeftJoin("orders", "$id", "customerId", "=", "ord") + +// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +var queries = Query.Builder() + .Equal("status", "published") + .LeftJoin("orders", "$id", "customerId", "=", "ord") + .Count("*", "total") + .GroupBy(new List { "status" }) + .Limit(25) + .Build(); + +await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", queries); +// await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", new[] { Query.Builder().Equal("status", "active") }); + +// POST CreateQuery is the large-payload path when it exists; ListRows still accepts the same query strings. +await tablesDB.CreateQuery("[DATABASE_ID]", "[TABLE_ID]", queries); ``` ### File Storage diff --git a/templates/skills/go.md.twig b/templates/skills/go.md.twig index 3e88f95d23..3f48cfb7dd 100644 --- a/templates/skills/go.md.twig +++ b/templates/skills/go.md.twig @@ -157,6 +157,37 @@ 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) + +// Aggregations (optional alias) +query.Count("*", "total") +query.Sum("amount", "revenue") +query.Avg("score", "avgScore") +query.Min("price", "minPrice") +query.Max("price", "maxPrice") + +// Grouping +query.GroupBy([]string{"status", "year"}) +query.Having([]string{query.GreaterThan("total", 1)}) +query.Distinct() + +// Joins (table id, left key, right key, operator, alias) +query.Join("orders", "$id", "customerId") +query.LeftJoin("orders", "$id", "customerId", "=", "ord") + +// Fluent builder. Wrapping the builder in an extra slice is OK — the SDK flattens it. +queries := query.Builder(). + Equal("status", "published"). + LeftJoin("orders", "$id", "customerId", "=", "ord"). + Count("*", "total"). + GroupBy([]string{"status"}). + Limit(25). + Build() + +_, _ = service.ListRows("[DATABASE_ID]", "[TABLE_ID]", service.WithListRowsQueries(queries)) +// Wrapping the builder in an extra slice is OK — the SDK flattens it. + +// POST CreateQuery is the large-payload path when it exists; ListRows still accepts the same query strings. +_, _ = service.CreateQuery("[DATABASE_ID]", "[TABLE_ID]", queries) ``` ### File Storage diff --git a/templates/skills/kotlin.md.twig b/templates/skills/kotlin.md.twig index b7c49276c8..c3d1aa56bd 100644 --- a/templates/skills/kotlin.md.twig +++ b/templates/skills/kotlin.md.twig @@ -213,6 +213,44 @@ 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) + +// Aggregations (optional alias) +Query.count("*", "total") +Query.sum("amount", "revenue") +Query.avg("score", "avgScore") +Query.min("price", "minPrice") +Query.max("price", "maxPrice") + +// Grouping +Query.groupBy(listOf("status", "year")) +Query.having(listOf(Query.greaterThan("total", 1))) +Query.distinct() + +// Joins (table id, left key, right key, operator, alias) +Query.join("orders", "\$id", "customerId") +Query.leftJoin("orders", "\$id", "customerId", "=", "ord") + +// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +val queries = Query.builder() + .equal("status", "published") + .leftJoin("orders", "\$id", "customerId", "=", "ord") + .count("*", "total") + .groupBy(listOf("status")) + .limit(25) + .build() + +tablesDB.listRows( + databaseId = "[DATABASE_ID]", + tableId = "[TABLE_ID]", + queries = queries, // or listOf(Query.builder().equal("status", "active")) +) + +// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. +tablesDB.createQuery( + databaseId = "[DATABASE_ID]", + tableId = "[TABLE_ID]", + queries = queries, +) ``` ### File Storage diff --git a/templates/skills/php.md.twig b/templates/skills/php.md.twig index 705f8f262f..26a35b292a 100644 --- a/templates/skills/php.md.twig +++ b/templates/skills/php.md.twig @@ -140,6 +140,37 @@ 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) + +// Aggregations (optional alias) +Query::count('*', 'total') +Query::sum('amount', 'revenue') +Query::avg('score', 'avgScore') +Query::min('price', 'minPrice') +Query::max('price', 'maxPrice') + +// Grouping +Query::groupBy(['status', 'year']) +Query::having([Query::greaterThan('total', 1)]) +Query::distinct() + +// Joins (table id, left key, right key, operator, alias) +Query::join('orders', '$id', 'customerId') +Query::leftJoin('orders', '$id', 'customerId', '=', 'ord') + +// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +$queries = Query::builder() + ->equal('status', 'published') + ->leftJoin('orders', '$id', 'customerId', '=', 'ord') + ->count('*', 'total') + ->groupBy(['status']) + ->limit(25) + ->build(); + +$tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', $queries); +// $tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', [Query::builder()->equal('status', 'active')]); + +// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. +$tablesDB->createQuery('[DATABASE_ID]', '[TABLE_ID]', $queries); ``` ### File Storage diff --git a/templates/skills/python.md.twig b/templates/skills/python.md.twig index 658eed487c..4ae5d84566 100644 --- a/templates/skills/python.md.twig +++ b/templates/skills/python.md.twig @@ -147,6 +147,39 @@ 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) + +# Aggregations (optional alias) +Query.count('*', 'total') +Query.sum('amount', 'revenue') +Query.avg('score', 'avg_score') +Query.min('price', 'min_price') +Query.max('price', 'max_price') + +# Grouping +Query.group_by(['status', 'year']) +Query.having([Query.greater_than('total', 1)]) +Query.distinct() + +# Joins (table id, left key, right key, operator, alias) +Query.join('orders', '$id', 'customerId') +Query.left_join('orders', '$id', 'customerId', '=', 'ord') + +# Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +queries = ( + Query.builder() + .equal('status', 'published') + .left_join('orders', '$id', 'customerId', '=', 'ord') + .count('*', 'total') + .group_by(['status']) + .limit(25) + .build() +) + +tables_db.list_rows('[DATABASE_ID]', '[TABLE_ID]', queries) +# tables_db.list_rows('[DATABASE_ID]', '[TABLE_ID]', [Query.builder().equal('status', 'active')]) + +# POST create_query is the large-payload path when it exists; list_rows still accepts the same query strings. +tables_db.create_query('[DATABASE_ID]', '[TABLE_ID]', queries) ``` ### File Storage diff --git a/templates/skills/ruby.md.twig b/templates/skills/ruby.md.twig index 485de2a07f..0cb2282622 100644 --- a/templates/skills/ruby.md.twig +++ b/templates/skills/ruby.md.twig @@ -147,6 +147,37 @@ 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) + +# Aggregations (optional alias) +Query.count('*', 'total') +Query.sum('amount', 'revenue') +Query.avg('score', 'avg_score') +Query.min('price', 'min_price') +Query.max('price', 'max_price') + +# Grouping +Query.group_by(['status', 'year']) +Query.having([Query.greater_than('total', 1)]) +Query.distinct + +# Joins (table id, left key, right key, operator, alias) +Query.join('orders', '$id', 'customerId') +Query.left_join('orders', '$id', 'customerId', '=', 'ord') + +# Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +queries = Query.builder + .equal('status', 'published') + .left_join('orders', '$id', 'customerId', '=', 'ord') + .count('*', 'total') + .group_by(['status']) + .limit(25) + .build + +tables_db.list_rows(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', queries: queries) +# tables_db.list_rows(..., queries: [Query.builder.equal('status', 'active')]) + +# POST create_query is the large-payload path when it exists; list_rows still accepts the same query strings. +tables_db.create_query(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', queries: queries) ``` ### File Storage diff --git a/templates/skills/swift.md.twig b/templates/skills/swift.md.twig index 10d229e719..c99d6f1c8a 100644 --- a/templates/skills/swift.md.twig +++ b/templates/skills/swift.md.twig @@ -171,6 +171,37 @@ 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) + +// Aggregations (optional alias) +Query.count("*", alias: "total") +Query.sum("amount", alias: "revenue") +Query.avg("score", alias: "avgScore") +Query.min("price", alias: "minPrice") +Query.max("price", alias: "maxPrice") + +// Grouping +Query.groupBy(["status", "year"]) +Query.having([Query.greaterThan("total", value: 1)]) +Query.distinct() + +// Joins (table id, left key, right key, operator, alias) +Query.join("orders", left: "$id", right: "customerId") +Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord") + +// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +let queries = Query.builder() + .equal("status", value: "published") + .leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord") + .count("*", alias: "total") + .groupBy(["status"]) + .limit(25) + .build() + +_ = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: queries) +// listRows(..., queries: [Query.builder().equal("status", value: "active")]) is also OK + +// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. +_ = try await tablesDB.createQuery(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: queries) ``` ### File Storage diff --git a/templates/skills/typescript.md.twig b/templates/skills/typescript.md.twig index d7afc9e2c7..4e6c8a2b79 100644 --- a/templates/skills/typescript.md.twig +++ b/templates/skills/typescript.md.twig @@ -330,6 +330,44 @@ 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) + +// Aggregations (optional alias) +Query.count('*', 'total') +Query.sum('amount', 'revenue') +Query.avg('score', 'avgScore') +Query.min('price', 'minPrice') +Query.max('price', 'maxPrice') + +// Grouping +Query.groupBy(['status', 'year']) +Query.having([Query.greaterThan('total', 1)]) +Query.distinct() + +// Joins (table id, left key, right key, operator, alias) +Query.join('orders', '$id', 'customerId') +Query.leftJoin('orders', '$id', 'customerId', '=', 'ord') + +// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +const queries = Query.builder() + .equal('status', 'published') + .leftJoin('orders', '$id', 'customerId', '=', 'ord') + .count('*', 'total') + .groupBy(['status']) + .limit(25) + .build(); + +await tablesDB.listRows({ + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + queries, // or [Query.builder().equal('status', 'active')] +}); + +// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. +await tablesDB.createQuery({ + databaseId: '[DATABASE_ID]', + tableId: '[TABLE_ID]', + queries, +}); ``` ### File Storage diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 5489516be9..ead0aa50ad 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -324,6 +324,10 @@ open class Client { headers: [String: String] = [:], params: [String: Any?] = [:] ) throws -> HTTPClientRequest { + var params = params + if params.keys.contains("queries") { + params["queries"] = Query.normalize(params["queries"] as Any?) + } let validParams = params.filter { $0.value != nil } let queryParameters = method == "GET" && !validParams.isEmpty diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index 274546030b..d4ce04ee56 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -106,6 +106,10 @@ public struct Query : Codable, CustomStringConvertible { return .bool(boolValue) } else if let queryValue = item as? Query { return .query(queryValue) + } else if let doubleArray = item as? [Double] { + return .array(doubleArray.map { .double($0) }) + } else if let intArray = item as? [Int] { + return .array(intArray.map { .int($0) }) } else if let nestedArray = item as? [Any] { // Convert nested array to QueryValue.array if let converted = convertToQueryValueArray(nestedArray) { @@ -636,6 +640,268 @@ public struct Query : Codable, CustomStringConvertible { ).description } + public static func count(_ attribute: String = "*", alias: String = "") -> String { + return Query( + method: "count", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func countDistinct(_ attribute: String, alias: String = "") -> String { + return Query( + method: "countDistinct", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func sum(_ attribute: String, alias: String = "") -> String { + return Query( + method: "sum", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func avg(_ attribute: String, alias: String = "") -> String { + return Query( + method: "avg", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func min(_ attribute: String, alias: String = "") -> String { + return Query( + method: "min", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func max(_ attribute: String, alias: String = "") -> String { + return Query( + method: "max", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func stddev(_ attribute: String, alias: String = "") -> String { + return Query( + method: "stddev", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func stddevPop(_ attribute: String, alias: String = "") -> String { + return Query( + method: "stddevPop", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func stddevSamp(_ attribute: String, alias: String = "") -> String { + return Query( + method: "stddevSamp", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func variance(_ attribute: String, alias: String = "") -> String { + return Query( + method: "variance", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func varPop(_ attribute: String, alias: String = "") -> String { + return Query( + method: "varPop", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func varSamp(_ attribute: String, alias: String = "") -> String { + return Query( + method: "varSamp", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func bitAnd(_ attribute: String, alias: String = "") -> String { + return Query( + method: "bitAnd", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func bitOr(_ attribute: String, alias: String = "") -> String { + return Query( + method: "bitOr", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func bitXor(_ attribute: String, alias: String = "") -> String { + return Query( + method: "bitXor", + attribute: attribute, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func groupBy(_ attributes: [String]) -> String { + return Query( + method: "groupBy", + values: attributes + ).description + } + + public static func having(_ queries: [String]) -> String { + return Query( + method: "having", + values: decodeQueries(queries) + ).description + } + + public static func distinct() -> String { + return Query( + method: "distinct" + ).description + } + + public static func join(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { + return joinQuery("join", table: table, left: left, right: right, operator: op, alias: alias) + } + + public static func leftJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { + return joinQuery("leftJoin", table: table, left: left, right: right, operator: op, alias: alias) + } + + public static func rightJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { + return joinQuery("rightJoin", table: table, left: left, right: right, operator: op, alias: alias) + } + + public static func fullOuterJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { + return joinQuery("fullOuterJoin", table: table, left: left, right: right, operator: op, alias: alias) + } + + public static func crossJoin(_ table: String, alias: String = "") -> String { + return Query( + method: "crossJoin", + attribute: table, + values: alias.isEmpty ? nil : alias + ).description + } + + public static func covers(_ attribute: String, values: [Any]) -> String { + return Query( + method: "covers", + attribute: attribute, + values: [values] + ).description + } + + public static func notCovers(_ attribute: String, values: [Any]) -> String { + return Query( + method: "notCovers", + attribute: attribute, + values: [values] + ).description + } + + public static func spatialEquals(_ attribute: String, values: [Any]) -> String { + return Query( + method: "spatialEquals", + attribute: attribute, + values: [values] + ).description + } + + public static func notSpatialEquals(_ attribute: String, values: [Any]) -> String { + return Query( + method: "notSpatialEquals", + attribute: attribute, + values: [values] + ).description + } + + public static func page(_ page: Int, perPage: Int = 25) -> [String] { + precondition(page >= 1, "Page must be >= 1, got \(page)") + precondition(perPage >= 1, "Per page must be >= 1, got \(perPage)") + return [limit(perPage), offset((page - 1) * perPage)] + } + + public static func builder() -> QueryBuilder { + return QueryBuilder() + } + + public static func normalize(_ queries: Any?) -> [String] { + guard let queries else { + return [] + } + if queries is NSNull { + return [] + } + if let builder = queries as? QueryBuilder { + return builder.build() + } + if let query = queries as? Query { + return [query.description] + } + if let string = queries as? String { + return [string] + } + if let strings = queries as? [String] { + return strings + } + if let builders = queries as? [QueryBuilder] { + return builders.flatMap { $0.build() } + } + if let nested = queries as? [[String]] { + return nested.flatMap { $0 } + } + if let nestedAny = queries as? [Any] { + return nestedAny.flatMap { normalize($0) } + } + if let nestedOptional = queries as? [Any?] { + return nestedOptional.flatMap { normalize($0) } + } + return [] + } + + private static func joinQuery(_ method: String, table: String, left: String, right: String, operator op: String, alias: String) -> String { + var values: [String] = [left, op, right] + if !alias.isEmpty { + values.append(alias) + } + return Query( + method: method, + attribute: table, + values: values + ).description + } + + private static func decodeQueries(_ queries: [String]) -> [Query] { + let decoder = JSONDecoder() + return queries.compactMap { queryStr -> Query? in + guard let data = queryStr.data(using: .utf8) else { + return nil + } + return try? decoder.decode(Query.self, from: data) + } + } + private static func parseValue(_ value: Any) -> [Any] { if let value = value as? [Any] { return value @@ -644,3 +910,460 @@ public struct Query : Codable, CustomStringConvertible { } } } + +public class QueryBuilder { + private var queries: [String] = [] + + public init() {} + + public func build() -> [String] { + return queries + } + + @discardableResult + public func equal(_ attribute: String, value: Any) -> QueryBuilder { + return push(Query.equal(attribute, value: value)) + } + + @discardableResult + public func notEqual(_ attribute: String, value: Any) -> QueryBuilder { + return push(Query.notEqual(attribute, value: value)) + } + + @discardableResult + public func lessThan(_ attribute: String, value: Any) -> QueryBuilder { + return push(Query.lessThan(attribute, value: value)) + } + + @discardableResult + public func lessThanEqual(attribute: String, value: Any) -> QueryBuilder { + return push(Query.lessThanEqual(attribute: attribute, value: value)) + } + + @discardableResult + public func greaterThan(_ attribute: String, value: Any) -> QueryBuilder { + return push(Query.greaterThan(attribute, value: value)) + } + + @discardableResult + public func greaterThanEqual(_ attribute: String, value: Any) -> QueryBuilder { + return push(Query.greaterThanEqual(attribute, value: value)) + } + + @discardableResult + public func search(_ attribute: String, value: String) -> QueryBuilder { + return push(Query.search(attribute, value: value)) + } + + @discardableResult + public func isNull(_ attribute: String) -> QueryBuilder { + return push(Query.isNull(attribute)) + } + + @discardableResult + public func isNotNull(_ attribute: String) -> QueryBuilder { + return push(Query.isNotNull(attribute)) + } + + @discardableResult + public func between(_ attribute: String, start: Int, end: Int) -> QueryBuilder { + return push(Query.between(attribute, start: start, end: end)) + } + + @discardableResult + public func between(_ attribute: String, start: Double, end: Double) -> QueryBuilder { + return push(Query.between(attribute, start: start, end: end)) + } + + @discardableResult + public func between(_ attribute: String, start: String, end: String) -> QueryBuilder { + return push(Query.between(attribute, start: start, end: end)) + } + + @discardableResult + public func startsWith(_ attribute: String, value: String) -> QueryBuilder { + return push(Query.startsWith(attribute, value: value)) + } + + @discardableResult + public func endsWith(_ attribute: String, value: String) -> QueryBuilder { + return push(Query.endsWith(attribute, value: value)) + } + + @discardableResult + public func select(_ attributes: [String]) -> QueryBuilder { + return push(Query.select(attributes)) + } + + @discardableResult + public func cursorAfter(_ id: String) -> QueryBuilder { + return push(Query.cursorAfter(id)) + } + + @discardableResult + public func cursorBefore(_ id: String) -> QueryBuilder { + return push(Query.cursorBefore(id)) + } + + @discardableResult + public func orderAsc(_ attribute: String) -> QueryBuilder { + return push(Query.orderAsc(attribute)) + } + + @discardableResult + public func orderDesc(_ attribute: String) -> QueryBuilder { + return push(Query.orderDesc(attribute)) + } + + @discardableResult + public func orderRandom() -> QueryBuilder { + return push(Query.orderRandom()) + } + + @discardableResult + public func limit(_ limit: Int) -> QueryBuilder { + return push(Query.limit(limit)) + } + + @discardableResult + public func offset(_ offset: Int) -> QueryBuilder { + return push(Query.offset(offset)) + } + + @discardableResult + public func contains(_ attribute: String, value: Any) -> QueryBuilder { + return push(Query.contains(attribute, value: value)) + } + + @discardableResult + public func containsAny(_ attribute: String, value: [Any]) -> QueryBuilder { + return push(Query.containsAny(attribute, value: value)) + } + + @discardableResult + public func containsAll(_ attribute: String, value: [Any]) -> QueryBuilder { + return push(Query.containsAll(attribute, value: value)) + } + + @discardableResult + public func notContains(_ attribute: String, value: Any) -> QueryBuilder { + return push(Query.notContains(attribute, value: value)) + } + + @discardableResult + public func notSearch(_ attribute: String, value: String) -> QueryBuilder { + return push(Query.notSearch(attribute, value: value)) + } + + @discardableResult + public func notBetween(_ attribute: String, start: Int, end: Int) -> QueryBuilder { + return push(Query.notBetween(attribute, start: start, end: end)) + } + + @discardableResult + public func notBetween(_ attribute: String, start: Double, end: Double) -> QueryBuilder { + return push(Query.notBetween(attribute, start: start, end: end)) + } + + @discardableResult + public func notBetween(_ attribute: String, start: String, end: String) -> QueryBuilder { + return push(Query.notBetween(attribute, start: start, end: end)) + } + + @discardableResult + public func notStartsWith(_ attribute: String, value: String) -> QueryBuilder { + return push(Query.notStartsWith(attribute, value: value)) + } + + @discardableResult + public func notEndsWith(_ attribute: String, value: String) -> QueryBuilder { + return push(Query.notEndsWith(attribute, value: value)) + } + + @discardableResult + public func regex(_ attribute: String, pattern: String) -> QueryBuilder { + return push(Query.regex(attribute, pattern: pattern)) + } + + @discardableResult + public func exists(_ attributes: [String]) -> QueryBuilder { + return push(Query.exists(attributes)) + } + + @discardableResult + public func notExists(_ attributes: [String]) -> QueryBuilder { + return push(Query.notExists(attributes)) + } + + @discardableResult + public func createdBefore(_ value: String) -> QueryBuilder { + return push(Query.createdBefore(value)) + } + + @discardableResult + public func createdAfter(_ value: String) -> QueryBuilder { + return push(Query.createdAfter(value)) + } + + @discardableResult + public func createdBetween(_ start: String, _ end: String) -> QueryBuilder { + return push(Query.createdBetween(start, end)) + } + + @discardableResult + public func updatedBefore(_ value: String) -> QueryBuilder { + return push(Query.updatedBefore(value)) + } + + @discardableResult + public func updatedAfter(_ value: String) -> QueryBuilder { + return push(Query.updatedAfter(value)) + } + + @discardableResult + public func updatedBetween(_ start: String, _ end: String) -> QueryBuilder { + return push(Query.updatedBetween(start, end)) + } + + @discardableResult + public func or(_ queries: [String]) -> QueryBuilder { + return push(Query.or(queries)) + } + + @discardableResult + public func and(_ queries: [String]) -> QueryBuilder { + return push(Query.and(queries)) + } + + @discardableResult + public func elemMatch(_ attribute: String, queries: [String]) -> QueryBuilder { + return push(Query.elemMatch(attribute, queries: queries)) + } + + @discardableResult + public func distanceEqual(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { + return push(Query.distanceEqual(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func distanceNotEqual(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { + return push(Query.distanceNotEqual(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func distanceGreaterThan(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { + return push(Query.distanceGreaterThan(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func distanceLessThan(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { + return push(Query.distanceLessThan(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func intersects(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.intersects(attribute, values: values)) + } + + @discardableResult + public func notIntersects(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.notIntersects(attribute, values: values)) + } + + @discardableResult + public func crosses(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.crosses(attribute, values: values)) + } + + @discardableResult + public func notCrosses(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.notCrosses(attribute, values: values)) + } + + @discardableResult + public func overlaps(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.overlaps(attribute, values: values)) + } + + @discardableResult + public func notOverlaps(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.notOverlaps(attribute, values: values)) + } + + @discardableResult + public func touches(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.touches(attribute, values: values)) + } + + @discardableResult + public func notTouches(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.notTouches(attribute, values: values)) + } + + @discardableResult + public func count(_ attribute: String = "*", alias: String = "") -> QueryBuilder { + return push(Query.count(attribute, alias: alias)) + } + + @discardableResult + public func countDistinct(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.countDistinct(attribute, alias: alias)) + } + + @discardableResult + public func sum(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.sum(attribute, alias: alias)) + } + + @discardableResult + public func avg(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.avg(attribute, alias: alias)) + } + + @discardableResult + public func min(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.min(attribute, alias: alias)) + } + + @discardableResult + public func max(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.max(attribute, alias: alias)) + } + + @discardableResult + public func stddev(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.stddev(attribute, alias: alias)) + } + + @discardableResult + public func stddevPop(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.stddevPop(attribute, alias: alias)) + } + + @discardableResult + public func stddevSamp(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.stddevSamp(attribute, alias: alias)) + } + + @discardableResult + public func variance(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.variance(attribute, alias: alias)) + } + + @discardableResult + public func varPop(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.varPop(attribute, alias: alias)) + } + + @discardableResult + public func varSamp(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.varSamp(attribute, alias: alias)) + } + + @discardableResult + public func bitAnd(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.bitAnd(attribute, alias: alias)) + } + + @discardableResult + public func bitOr(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.bitOr(attribute, alias: alias)) + } + + @discardableResult + public func bitXor(_ attribute: String, alias: String = "") -> QueryBuilder { + return push(Query.bitXor(attribute, alias: alias)) + } + + @discardableResult + public func groupBy(_ attributes: [String]) -> QueryBuilder { + return push(Query.groupBy(attributes)) + } + + @discardableResult + public func having(_ queries: [String]) -> QueryBuilder { + return push(Query.having(queries)) + } + + @discardableResult + public func distinct() -> QueryBuilder { + return push(Query.distinct()) + } + + @discardableResult + public func join(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { + return push(Query.join(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func leftJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { + return push(Query.leftJoin(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func rightJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { + return push(Query.rightJoin(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func fullOuterJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { + return push(Query.fullOuterJoin(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func crossJoin(_ table: String, alias: String = "") -> QueryBuilder { + return push(Query.crossJoin(table, alias: alias)) + } + + @discardableResult + public func vectorDot(_ attribute: String, vector: [Double]) -> QueryBuilder { + return push(Query.vectorDot(attribute, vector: vector)) + } + + @discardableResult + public func vectorCosine(_ attribute: String, vector: [Double]) -> QueryBuilder { + return push(Query.vectorCosine(attribute, vector: vector)) + } + + @discardableResult + public func vectorEuclidean(_ attribute: String, vector: [Double]) -> QueryBuilder { + return push(Query.vectorEuclidean(attribute, vector: vector)) + } + + @discardableResult + public func covers(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.covers(attribute, values: values)) + } + + @discardableResult + public func notCovers(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.notCovers(attribute, values: values)) + } + + @discardableResult + public func spatialEquals(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.spatialEquals(attribute, values: values)) + } + + @discardableResult + public func notSpatialEquals(_ attribute: String, values: [Any]) -> QueryBuilder { + return push(Query.notSpatialEquals(attribute, values: values)) + } + + @discardableResult + public func page(_ page: Int, perPage: Int = 25) -> QueryBuilder { + return push(Query.page(page, perPage: perPage)) + } + + @discardableResult + private func push(_ query: String) -> QueryBuilder { + queries.append(query) + return self + } + + @discardableResult + private func push(_ query: [String]) -> QueryBuilder { + queries.append(contentsOf: query) + return self + } +} diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index ce0232c5e0..bc063d22f9 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1,6 +1,6 @@ import { Models } from './models'; import { Channel, ActionableChannel, ResolvedChannel } from './channel'; -import { Query } from './query'; +import { Query, type QueryInput } from './query'; import { ID } from './id'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); @@ -762,7 +762,7 @@ class Client { subscribe( channels: string | string[] | Channel | ActionableChannel | ResolvedChannel | (Channel | ActionableChannel | ResolvedChannel)[], callback: (payload: RealtimeResponseEvent) => void, - queries: (string | Query)[] = [] + queries: QueryInput = [] ): () => void { const channelArray = Array.isArray(channels) ? channels : [channels]; // Convert Channel instances to strings @@ -779,7 +779,8 @@ class Client { }); channelStrings.forEach(channel => this.realtime.channels.add(channel)); - const queryStrings = (queries ?? []).map(q => typeof q === 'string' ? q : q.toString()); + const queryStrings = Query.normalize(queries); + queryStrings.forEach(query => this.realtime.queries.add(query)); let subscriptionId = ''; const attempts = this.realtime.subscriptions.size + 1; @@ -825,6 +826,10 @@ class Client { prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { method = method.toUpperCase(); + if (Object.prototype.hasOwnProperty.call(params, 'queries')) { + params = { ...params, queries: Query.normalize(params.queries) }; + } + headers = Object.assign({}, this.headers, headers); if (typeof window !== 'undefined' && window.localStorage{% if sdk.platform == 'console' %} && this.shouldUseFallbackCredentials(url){% endif %}) { @@ -1109,7 +1114,7 @@ class Client { } export { Client, {{spec.info.title | caseUcfirst}}Exception }; -export { Query } from './query'; +export { Query, QueryBuilder } from './query'; export type { Models, Payload, UploadProgress }; export type { RealtimeResponseEvent }; -export type { QueryTypes, QueryTypesList } from './query'; +export type { QueryTypes, QueryTypesList, QueryInput } from './query'; diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index 5ae6684abb..94a2c08eea 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -5,14 +5,14 @@ * For older versions, please check * [previous releases](https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/releases). */ -export { Client, Query, {{spec.info.title | caseUcfirst}}Exception } from './client'; +export { Client, Query, QueryBuilder, {{spec.info.title | caseUcfirst}}Exception } from './client'; {% for service in services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} export { Realtime } from './services/realtime'; export type { Models, Payload, RealtimeResponseEvent, UploadProgress } from './client'; export type { RealtimeSubscription } from './services/realtime'; -export type { QueryTypes, QueryTypesList } from './query'; +export type { QueryTypes, QueryTypesList, QueryInput } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index e0e9725761..ac10f9e828 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -603,4 +603,548 @@ export class Query { */ static notTouches = (attribute: string, values: any[]): string => new Query("notTouches", attribute, [values]).toString(); + + /** + * Count documents, optionally aliased. + */ + static count = (attribute: string = "*", alias: string = ""): string => + new Query("count", attribute, alias !== "" ? alias : undefined).toString(); + + /** + * Count distinct values of attribute, optionally aliased. + */ + static countDistinct = (attribute: string, alias: string = ""): string => + new Query("countDistinct", attribute, alias !== "" ? alias : undefined).toString(); + + static sum = (attribute: string, alias: string = ""): string => + new Query("sum", attribute, alias !== "" ? alias : undefined).toString(); + + static avg = (attribute: string, alias: string = ""): string => + new Query("avg", attribute, alias !== "" ? alias : undefined).toString(); + + static min = (attribute: string, alias: string = ""): string => + new Query("min", attribute, alias !== "" ? alias : undefined).toString(); + + static max = (attribute: string, alias: string = ""): string => + new Query("max", attribute, alias !== "" ? alias : undefined).toString(); + + static stddev = (attribute: string, alias: string = ""): string => + new Query("stddev", attribute, alias !== "" ? alias : undefined).toString(); + + static stddevPop = (attribute: string, alias: string = ""): string => + new Query("stddevPop", attribute, alias !== "" ? alias : undefined).toString(); + + static stddevSamp = (attribute: string, alias: string = ""): string => + new Query("stddevSamp", attribute, alias !== "" ? alias : undefined).toString(); + + static variance = (attribute: string, alias: string = ""): string => + new Query("variance", attribute, alias !== "" ? alias : undefined).toString(); + + static varPop = (attribute: string, alias: string = ""): string => + new Query("varPop", attribute, alias !== "" ? alias : undefined).toString(); + + static varSamp = (attribute: string, alias: string = ""): string => + new Query("varSamp", attribute, alias !== "" ? alias : undefined).toString(); + + static bitAnd = (attribute: string, alias: string = ""): string => + new Query("bitAnd", attribute, alias !== "" ? alias : undefined).toString(); + + static bitOr = (attribute: string, alias: string = ""): string => + new Query("bitOr", attribute, alias !== "" ? alias : undefined).toString(); + + static bitXor = (attribute: string, alias: string = ""): string => + new Query("bitXor", attribute, alias !== "" ? alias : undefined).toString(); + + /** + * Group results by attributes. + */ + static groupBy = (attributes: string[]): string => + new Query("groupBy", undefined, attributes).toString(); + + /** + * Filter grouped results. `queries` are nested like `and`/`or`. + */ + static having = (queries: string[]): string => + new Query( + "having", + undefined, + queries.map((query) => JSONbig.parse(query)) + ).toString(); + + static distinct = (): string => + new Query("distinct").toString(); + + /** + * Inner join. `table` is the joined collection/table ID. + */ + static join = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "join", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static leftJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "leftJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static rightJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "rightJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static fullOuterJoin = ( + table: string, + left: string, + right: string, + operator: string = "=", + alias: string = "" + ): string => + new Query( + "fullOuterJoin", + table, + alias !== "" ? [left, operator, right, alias] : [left, operator, right] + ).toString(); + + static crossJoin = (table: string, alias: string = ""): string => + new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); + + static covers = (attribute: string, values: any[]): string => + new Query("covers", attribute, [values]).toString(); + + static notCovers = (attribute: string, values: any[]): string => + new Query("notCovers", attribute, [values]).toString(); + + static spatialEquals = (attribute: string, values: any[]): string => + new Query("spatialEquals", attribute, [values]).toString(); + + static notSpatialEquals = (attribute: string, values: any[]): string => + new Query("notSpatialEquals", attribute, [values]).toString(); + + /** + * Return limit and offset queries for 1-based page pagination. + */ + static page = (page: number, perPage: number = 25): string[] => { + if (page < 1) { + throw new Error("Page must be >= 1, got " + page); + } + if (perPage < 1) { + throw new Error("Per page must be >= 1, got " + perPage); + } + return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; + }; + + static builder = (): QueryBuilder => new QueryBuilder(); + + /** + * Flatten a queries argument into query strings. + * + * Accepts a QueryBuilder, a list of query strings, builders, or nested + * lists (the common `[builder]` / `[builder.build()]` mistakes). + */ + static normalize = (queries?: QueryInput | null): string[] => { + if (queries == null) { + return []; + } + if (queries instanceof QueryBuilder) { + return queries.build(); + } + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === "string") { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...Query.normalize(item)); + } + return out; + }; +} + +export type QueryInput = + | string + | Query + | QueryBuilder + | QueryInput[]; + +/** + * Fluent collector of Query fragments. Call {@link QueryBuilder.build} to + * produce the `string[]` expected by list/createQuery methods. + */ +export class QueryBuilder { + private readonly queries: string[] = []; + + build(): string[] { + return [...this.queries]; + } + + equal(attribute: string, value: QueryTypes): this { + return this.push(Query.equal(attribute, value)); + } + + notEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.notEqual(attribute, value)); + } + + lessThan(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThan(attribute, value)); + } + + lessThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThanEqual(attribute, value)); + } + + greaterThan(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThan(attribute, value)); + } + + greaterThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThanEqual(attribute, value)); + } + + search(attribute: string, value: string): this { + return this.push(Query.search(attribute, value)); + } + + isNull(attribute: string): this { + return this.push(Query.isNull(attribute)); + } + + isNotNull(attribute: string): this { + return this.push(Query.isNotNull(attribute)); + } + + between(attribute: string, start: string | number | bigint, end: string | number | bigint): this { + return this.push(Query.between(attribute, start, end)); + } + + startsWith(attribute: string, value: string): this { + return this.push(Query.startsWith(attribute, value)); + } + + endsWith(attribute: string, value: string): this { + return this.push(Query.endsWith(attribute, value)); + } + + select(attributes: string[]): this { + return this.push(Query.select(attributes)); + } + + cursorAfter(documentId: string): this { + return this.push(Query.cursorAfter(documentId)); + } + + cursorBefore(documentId: string): this { + return this.push(Query.cursorBefore(documentId)); + } + + orderAsc(attribute: string): this { + return this.push(Query.orderAsc(attribute)); + } + + orderDesc(attribute: string): this { + return this.push(Query.orderDesc(attribute)); + } + + orderRandom(): this { + return this.push(Query.orderRandom()); + } + + limit(limit: number): this { + return this.push(Query.limit(limit)); + } + + offset(offset: number): this { + return this.push(Query.offset(offset)); + } + + contains(attribute: string, value: string | any[]): this { + return this.push(Query.contains(attribute, value)); + } + + containsAny(attribute: string, value: any[]): this { + return this.push(Query.containsAny(attribute, value)); + } + + containsAll(attribute: string, value: any[]): this { + return this.push(Query.containsAll(attribute, value)); + } + + notContains(attribute: string, value: string | any[]): this { + return this.push(Query.notContains(attribute, value)); + } + + notSearch(attribute: string, value: string): this { + return this.push(Query.notSearch(attribute, value)); + } + + notBetween(attribute: string, start: string | number | bigint, end: string | number | bigint): this { + return this.push(Query.notBetween(attribute, start, end)); + } + + notStartsWith(attribute: string, value: string): this { + return this.push(Query.notStartsWith(attribute, value)); + } + + notEndsWith(attribute: string, value: string): this { + return this.push(Query.notEndsWith(attribute, value)); + } + + regex(attribute: string, pattern: string): this { + return this.push(Query.regex(attribute, pattern)); + } + + exists(attributes: string[]): this { + return this.push(Query.exists(attributes)); + } + + notExists(attributes: string[]): this { + return this.push(Query.notExists(attributes)); + } + + createdBefore(value: string): this { + return this.push(Query.createdBefore(value)); + } + + createdAfter(value: string): this { + return this.push(Query.createdAfter(value)); + } + + createdBetween(start: string, end: string): this { + return this.push(Query.createdBetween(start, end)); + } + + updatedBefore(value: string): this { + return this.push(Query.updatedBefore(value)); + } + + updatedAfter(value: string): this { + return this.push(Query.updatedAfter(value)); + } + + updatedBetween(start: string, end: string): this { + return this.push(Query.updatedBetween(start, end)); + } + + or(queries: string[]): this { + return this.push(Query.or(queries)); + } + + and(queries: string[]): this { + return this.push(Query.and(queries)); + } + + elemMatch(attribute: string, queries: string[]): this { + return this.push(Query.elemMatch(attribute, queries)); + } + + distanceEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceEqual(attribute, values, distance, meters)); + } + + distanceNotEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceNotEqual(attribute, values, distance, meters)); + } + + distanceGreaterThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceGreaterThan(attribute, values, distance, meters)); + } + + distanceLessThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { + return this.push(Query.distanceLessThan(attribute, values, distance, meters)); + } + + intersects(attribute: string, values: any[]): this { + return this.push(Query.intersects(attribute, values)); + } + + notIntersects(attribute: string, values: any[]): this { + return this.push(Query.notIntersects(attribute, values)); + } + + crosses(attribute: string, values: any[]): this { + return this.push(Query.crosses(attribute, values)); + } + + notCrosses(attribute: string, values: any[]): this { + return this.push(Query.notCrosses(attribute, values)); + } + + overlaps(attribute: string, values: any[]): this { + return this.push(Query.overlaps(attribute, values)); + } + + notOverlaps(attribute: string, values: any[]): this { + return this.push(Query.notOverlaps(attribute, values)); + } + + touches(attribute: string, values: any[]): this { + return this.push(Query.touches(attribute, values)); + } + + notTouches(attribute: string, values: any[]): this { + return this.push(Query.notTouches(attribute, values)); + } + + count(attribute: string = "*", alias: string = ""): this { + return this.push(Query.count(attribute, alias)); + } + + countDistinct(attribute: string, alias: string = ""): this { + return this.push(Query.countDistinct(attribute, alias)); + } + + sum(attribute: string, alias: string = ""): this { + return this.push(Query.sum(attribute, alias)); + } + + avg(attribute: string, alias: string = ""): this { + return this.push(Query.avg(attribute, alias)); + } + + min(attribute: string, alias: string = ""): this { + return this.push(Query.min(attribute, alias)); + } + + max(attribute: string, alias: string = ""): this { + return this.push(Query.max(attribute, alias)); + } + + stddev(attribute: string, alias: string = ""): this { + return this.push(Query.stddev(attribute, alias)); + } + + stddevPop(attribute: string, alias: string = ""): this { + return this.push(Query.stddevPop(attribute, alias)); + } + + stddevSamp(attribute: string, alias: string = ""): this { + return this.push(Query.stddevSamp(attribute, alias)); + } + + variance(attribute: string, alias: string = ""): this { + return this.push(Query.variance(attribute, alias)); + } + + varPop(attribute: string, alias: string = ""): this { + return this.push(Query.varPop(attribute, alias)); + } + + varSamp(attribute: string, alias: string = ""): this { + return this.push(Query.varSamp(attribute, alias)); + } + + bitAnd(attribute: string, alias: string = ""): this { + return this.push(Query.bitAnd(attribute, alias)); + } + + bitOr(attribute: string, alias: string = ""): this { + return this.push(Query.bitOr(attribute, alias)); + } + + bitXor(attribute: string, alias: string = ""): this { + return this.push(Query.bitXor(attribute, alias)); + } + + groupBy(attributes: string[]): this { + return this.push(Query.groupBy(attributes)); + } + + having(queries: string[]): this { + return this.push(Query.having(queries)); + } + + distinct(): this { + return this.push(Query.distinct()); + } + + join(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.join(table, left, right, operator, alias)); + } + + leftJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.leftJoin(table, left, right, operator, alias)); + } + + rightJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.rightJoin(table, left, right, operator, alias)); + } + + fullOuterJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { + return this.push(Query.fullOuterJoin(table, left, right, operator, alias)); + } + + crossJoin(table: string, alias: string = ""): this { + return this.push(Query.crossJoin(table, alias)); + } + + vectorDot(attribute: string, vector: number[]): this { + return this.push(Query.vectorDot(attribute, vector)); + } + + vectorCosine(attribute: string, vector: number[]): this { + return this.push(Query.vectorCosine(attribute, vector)); + } + + vectorEuclidean(attribute: string, vector: number[]): this { + return this.push(Query.vectorEuclidean(attribute, vector)); + } + + covers(attribute: string, values: any[]): this { + return this.push(Query.covers(attribute, values)); + } + + notCovers(attribute: string, values: any[]): this { + return this.push(Query.notCovers(attribute, values)); + } + + spatialEquals(attribute: string, values: any[]): this { + return this.push(Query.spatialEquals(attribute, values)); + } + + notSpatialEquals(attribute: string, values: any[]): this { + return this.push(Query.notSpatialEquals(attribute, values)); + } + + page(page: number, perPage: number = 25): this { + return this.push(Query.page(page, perPage)); + } + + private push(query: string | string[]): this { + if (Array.isArray(query)) { + this.queries.push(...query); + } else { + this.queries.push(query); + } + return this; + } } diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index 72c2509a18..ea340457b4 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,6 +1,6 @@ import { {{ spec.info.title | caseUcfirst}}Exception, Client, JSONbig } from '../client'; import { Channel, ActionableChannel, ResolvedChannel } from '../channel'; -import { Query } from '../query'; +import { Query, type QueryInput } from '../query'; import { ID } from '../id'; {% if language.name == 'ReactNative' %} import { Platform } from 'react-native'; @@ -8,7 +8,7 @@ import { Platform } from 'react-native'; export type RealtimeSubscriptionUpdate = { channels?: (string | Channel | ActionableChannel | ResolvedChannel)[]; - queries?: (string | Query)[]; + queries?: QueryInput; } export type RealtimeSubscription = { @@ -482,7 +482,7 @@ export class Realtime { public async subscribe( channel: string | Channel | ActionableChannel | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void, - queries?: (string | Query)[] + queries?: QueryInput ): Promise; /** @@ -495,7 +495,7 @@ export class Realtime { public async subscribe( channels: (string | Channel | ActionableChannel | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void, - queries?: (string | Query)[] + queries?: QueryInput ): Promise; /** @@ -508,7 +508,7 @@ export class Realtime { public async subscribe( channel: string | Channel | ActionableChannel | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void, - queries?: (string | Query)[] + queries?: QueryInput ): Promise; /** @@ -521,13 +521,13 @@ export class Realtime { public async subscribe( channels: (string | Channel | ActionableChannel | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void, - queries?: (string | Query)[] + queries?: QueryInput ): Promise; public async subscribe( channelsOrChannel: string | Channel | ActionableChannel | ResolvedChannel | (string | Channel | ActionableChannel | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void, - queries: (string | Query)[] = [] + queries: QueryInput = [] ): Promise { const channelArray = Array.isArray(channelsOrChannel) ? channelsOrChannel @@ -537,19 +537,7 @@ export class Realtime { const channelStrings = channelArray.map(ch => this.channelToString(ch)); const channels = new Set(channelStrings); - // Convert queries to array of strings - // Ensure each query is a separate string in the array - const queryStrings: string[] = []; - for (const q of (queries ?? [])) { - if (Array.isArray(q)) { - // Handle nested arrays: [[q1, q2]] -> [q1, q2] - for (const inner of q) { - queryStrings.push(typeof inner === 'string' ? inner : inner.toString()); - } - } else { - queryStrings.push(typeof q === 'string' ? q : q.toString()); - } - } + const queryStrings = Query.normalize(queries); const subscriptionId = this.generateUniqueSubscriptionId(); diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 575035f63f..97330a47b9 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -198,6 +198,12 @@ 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":"count","attribute":"*","values":["total"]}', + '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}', + '{"method":"groupBy","values":["status"]}', + '{"method":"distinct"}', + '{"method":"covers","attribute":"location","values":[[1,2]]}', + '{"method":"limit","values":[1]}', ]; protected const PERMISSION_HELPER_RESPONSES = [ diff --git a/tests/e2e/languages/android/Tests.kt b/tests/e2e/languages/android/Tests.kt index 5cbcb9b5a2..dbc9d91e97 100644 --- a/tests/e2e/languages/android/Tests.kt +++ b/tests/e2e/languages/android/Tests.kt @@ -388,6 +388,12 @@ class ServiceTest { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ))) + writeToFile(Query.count("*", "total")) + writeToFile(Query.join("orders", "\$id", "customerId")) + writeToFile(Query.groupBy(listOf("status"))) + writeToFile(Query.distinct()) + writeToFile(Query.covers("location", listOf(1, 2))) + writeToFile(Query.builder().limit(1).build()[0]) // 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 7eee4b74ad..b72793e775 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -357,6 +357,12 @@ class Tests: XCTestCase { Query.equal("name", value: "Alice"), Query.greaterThan("age", value: 18) ])) + print(Query.count("*", alias: "total")) + print(Query.join("orders", left: "$id", right: "customerId")) + print(Query.groupBy(["status"])) + print(Query.distinct()) + print(Query.covers("location", values: [1, 2])) + print(Query.builder().limit(1).build()[0]) // 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 be5ae1f817..9a65d5839d 100644 --- a/tests/e2e/languages/dart/tests.dart +++ b/tests/e2e/languages/dart/tests.dart @@ -225,6 +225,12 @@ void main() async { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + print(Query.count('*', 'total')); + print(Query.join('orders', '\$id', 'customerId')); + print(Query.groupBy(['status'])); + print(Query.distinct()); + print(Query.covers('location', [1, 2])); + print(Query.builder().limit(1).build()[0]); // 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 6934f9c9c7..ab932a43c5 100644 --- a/tests/e2e/languages/deno/tests.ts +++ b/tests/e2e/languages/deno/tests.ts @@ -256,6 +256,12 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.count("*", "total")); + console.log(Query.join("orders", "$id", "customerId")); + console.log(Query.groupBy(["status"])); + console.log(Query.distinct()); + console.log(Query.covers("location", [1, 2])); + console.log(Query.builder().limit(1).build()[0]); // 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 5f89b50df2..f8b7e8e2c0 100644 --- a/tests/e2e/languages/dotnet/Tests.cs +++ b/tests/e2e/languages/dotnet/Tests.cs @@ -237,6 +237,12 @@ public async Task Test1() Query.Equal("name", "Alice"), Query.GreaterThan("age", 18) })); + TestContext.WriteLine(Query.Count("*", "total")); + TestContext.WriteLine(Query.Join("orders", "$id", "customerId")); + TestContext.WriteLine(Query.GroupBy(new List { "status" })); + TestContext.WriteLine(Query.Distinct()); + TestContext.WriteLine(Query.Covers("location", new List { 1, 2 })); + TestContext.WriteLine(Query.Builder().Limit(1).Build()[0]); // 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 1f275f3795..5a5465535a 100644 --- a/tests/e2e/languages/flutter/tests.dart +++ b/tests/e2e/languages/flutter/tests.dart @@ -354,6 +354,12 @@ void main() async { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + print(Query.count('*', 'total')); + print(Query.join('orders', '\$id', 'customerId')); + print(Query.groupBy(['status'])); + print(Query.distinct()); + print(Query.covers('location', [1, 2])); + print(Query.builder().limit(1).build()[0]); // 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 57866bcc3f..afe0be5abe 100644 --- a/tests/e2e/languages/go-v2/tests.go +++ b/tests/e2e/languages/go-v2/tests.go @@ -290,6 +290,12 @@ func testQueries() { query.Equal("name", "Alice"), query.GreaterThan("age", 18), })) + fmt.Println(query.Count("*", "total")) + fmt.Println(query.Join("orders", "$id", "customerId", "=", "")) + fmt.Println(query.GroupBy([]interface{}{"status"})) + fmt.Println(query.Distinct()) + fmt.Println(query.Covers("location", []interface{}{1, 2})) + fmt.Println(query.Builder().Limit(1).Build()[0]) } func testPermissionHelpers() { diff --git a/tests/e2e/languages/go/tests.go b/tests/e2e/languages/go/tests.go index fab5d6203b..e2b28f86ae 100644 --- a/tests/e2e/languages/go/tests.go +++ b/tests/e2e/languages/go/tests.go @@ -290,6 +290,12 @@ func testQueries() { query.Equal("name", "Alice"), query.GreaterThan("age", 18), })) + fmt.Println(query.Count("*", "total")) + fmt.Println(query.Join("orders", "$id", "customerId", "=", "")) + fmt.Println(query.GroupBy([]interface{}{"status"})) + fmt.Println(query.Distinct()) + fmt.Println(query.Covers("location", []interface{}{1, 2})) + fmt.Println(query.Builder().Limit(1).Build()[0]) } func testPermissionHelpers() { diff --git a/tests/e2e/languages/kotlin/Tests.kt b/tests/e2e/languages/kotlin/Tests.kt index 9c2714fdf3..a50a4aca5b 100644 --- a/tests/e2e/languages/kotlin/Tests.kt +++ b/tests/e2e/languages/kotlin/Tests.kt @@ -256,6 +256,12 @@ class ServiceTest { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ))) + writeToFile(Query.count("*", "total")) + writeToFile(Query.join("orders", "\$id", "customerId")) + writeToFile(Query.groupBy(listOf("status"))) + writeToFile(Query.distinct()) + writeToFile(Query.covers("location", listOf(1, 2))) + writeToFile(Query.builder().limit(1).build()[0]) // 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 98383225c4..e76fa5d35c 100644 --- a/tests/e2e/languages/node/test.js +++ b/tests/e2e/languages/node/test.js @@ -341,6 +341,12 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.count("*", "total")); + console.log(Query.join("orders", "$id", "customerId")); + console.log(Query.groupBy(["status"])); + console.log(Query.distinct()); + console.log(Query.covers("location", [1, 2])); + console.log(Query.builder().limit(1).build()[0]); // 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 441198046c..85d3dc0047 100644 --- a/tests/e2e/languages/php/test.php +++ b/tests/e2e/languages/php/test.php @@ -6,6 +6,7 @@ include __DIR__ . '/../../sdks/php/src/Appwrite/Service.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/InputFile.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Query.php'; +include __DIR__ . '/../../sdks/php/src/Appwrite/QueryBuilder.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Permission.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Role.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/ID.php'; @@ -347,6 +348,12 @@ public function toArray(): array Query::equal('name', ['Alice']), Query::greaterThan('age', 18) ]) . "\n"; +echo Query::count('*', 'total') . "\n"; +echo Query::join('orders', '$id', 'customerId') . "\n"; +echo Query::groupBy(['status']) . "\n"; +echo Query::distinct() . "\n"; +echo Query::covers('location', [1, 2]) . "\n"; +echo Query::builder()->limit(1)->build()[0] . "\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 ccbd56e64f..399bd20376 100644 --- a/tests/e2e/languages/python/tests.py +++ b/tests/e2e/languages/python/tests.py @@ -211,6 +211,12 @@ Query.equal("name", "Alice"), Query.greater_than("age", 18) ])) +print(Query.count("*", "total")) +print(Query.join("orders", "$id", "customerId")) +print(Query.group_by(["status"])) +print(Query.distinct()) +print(Query.covers("location", [1, 2])) +print(Query.builder().limit(1).build()[0]) # 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 d6c4ea350e..dbcfa2d2e2 100644 --- a/tests/e2e/languages/react-native/browser.js +++ b/tests/e2e/languages/react-native/browser.js @@ -290,6 +290,12 @@ import { Query.equal('name', 'Alice'), Query.greaterThan('age', 18), ])); + console.log(Query.count('*', 'total')); + console.log(Query.join('orders', '$id', 'customerId')); + console.log(Query.groupBy(['status'])); + console.log(Query.distinct()); + console.log(Query.covers('location', [1, 2])); + console.log(Query.builder().limit(1).build()[0]); // 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 7126309a21..c1c2f90921 100644 --- a/tests/e2e/languages/ruby/tests.rb +++ b/tests/e2e/languages/ruby/tests.rb @@ -217,6 +217,12 @@ Query.equal("name", "Alice"), Query.greater_than("age", 18) ]) +puts Query.count('*', 'total') +puts Query.join('orders', '$id', 'customerId') +puts Query.group_by(['status']) +puts Query.distinct() +puts Query.covers('location', [1, 2]) +puts Query.builder.limit(1).build[0] # 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 9170ecb014..43258a9db6 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -247,6 +247,12 @@ class Tests: XCTestCase { Query.equal("name", value: "Alice"), Query.greaterThan("age", value: 18) ])) + print(Query.count("*", alias: "total")) + print(Query.join("orders", left: "$id", right: "customerId")) + print(Query.groupBy(["status"])) + print(Query.distinct()) + print(Query.covers("location", values: [1, 2])) + print(Query.builder().limit(1).build()[0]) // 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 ba8bc97d46..40c113ad76 100644 --- a/tests/e2e/languages/unity/Tests.cs +++ b/tests/e2e/languages/unity/Tests.cs @@ -365,6 +365,12 @@ private async Task RunAsyncTest() Query.Equal("name", "Alice"), Query.GreaterThan("age", 18) })); + LogResult(Query.Count("*", "total")); + LogResult(Query.Join("orders", "$id", "customerId")); + LogResult(Query.GroupBy(new List { "status" })); + LogResult(Query.Distinct()); + LogResult(Query.Covers("location", new List { 1, 2 })); + LogResult(Query.Builder().Limit(1).Build()[0]); // 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 522ad3acc4..a5f71f21ab 100644 --- a/tests/e2e/languages/web/index.html +++ b/tests/e2e/languages/web/index.html @@ -432,6 +432,12 @@ Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.count("*", "total")); + console.log(Query.join("orders", "$id", "customerId")); + console.log(Query.groupBy(["status"])); + console.log(Query.distinct()); + console.log(Query.covers("location", [1, 2])); + console.log(Query.builder().limit(1).build()[0]); // 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 7f3db05b0c..d92b75751f 100644 --- a/tests/e2e/languages/web/node.js +++ b/tests/e2e/languages/web/node.js @@ -282,6 +282,12 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); + console.log(Query.count("*", "total")); + console.log(Query.join("orders", "$id", "customerId")); + console.log(Query.groupBy(["status"])); + console.log(Query.distinct()); + console.log(Query.covers("location", [1, 2])); + console.log(Query.builder().limit(1).build()[0]); // Permission & Role helper tests console.log(Permission.read(Role.any())); From 4f009cf0e355c56e6e8db3db242a18682dec2777 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 16:12:31 +1200 Subject: [PATCH 02/42] fix: keep Query.normalize compatible with Go 1.13 and web realtime types `any` does not exist on Go 1.13, which still compiles the SDK in CI. Realtime subscription updates now go through Query.normalize instead of iterating QueryInput, and Client.subscribe no longer writes a queries set that is not on the Realtime connection type. --- templates/go/query.go.twig | 2 +- templates/go/query_test.go.twig | 6 +++--- templates/web/src/client.ts.twig | 1 - templates/web/src/services/realtime.ts.twig | 12 +----------- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/templates/go/query.go.twig b/templates/go/query.go.twig index 7f11289ed8..f3e39b85ca 100644 --- a/templates/go/query.go.twig +++ b/templates/go/query.go.twig @@ -1051,7 +1051,7 @@ func (b *QueryBuilder) Page(page, perPage int) *QueryBuilder { return b.push(Page(page, perPage)...) } -func Normalize(queries any) []string { +func Normalize(queries interface{}) []string { if queries == nil { return []string{} } diff --git a/templates/go/query_test.go.twig b/templates/go/query_test.go.twig index e4dc4cfde7..83a11bbaf2 100644 --- a/templates/go/query_test.go.twig +++ b/templates/go/query_test.go.twig @@ -406,16 +406,16 @@ func TestNormalizeBuilderDirect(t *testing.T) { func TestNormalizeBuilderWrappedInArray(t *testing.T) { builder := Builder().Equal("status", "active").Limit(10) - assertQueriesEqual(t, Normalize([]any{builder}), builder.Build()) + assertQueriesEqual(t, Normalize([]interface{}{builder}), builder.Build()) } func TestNormalizeBuildResultWrappedInArray(t *testing.T) { built := Builder().Equal("status", "active").Limit(10).Build() - assertQueriesEqual(t, Normalize([]any{built}), built) + assertQueriesEqual(t, Normalize([]interface{}{built}), built) } func TestNormalizeMixedAndPage(t *testing.T) { - normalized := Normalize([]any{ + normalized := Normalize([]interface{}{ Equal("status", "active"), Page(2, 10), }) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index bc063d22f9..7f43efe3e3 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -780,7 +780,6 @@ class Client { channelStrings.forEach(channel => this.realtime.channels.add(channel)); const queryStrings = Query.normalize(queries); - queryStrings.forEach(query => this.realtime.queries.add(query)); let subscriptionId = ''; const attempts = this.realtime.subscriptions.size + 1; diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index ea340457b4..eafd58eb76 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -584,17 +584,7 @@ export class Realtime { } if (changes.queries !== undefined) { - const nextQueries: string[] = []; - for (const q of changes.queries) { - if (Array.isArray(q)) { - for (const inner of q) { - nextQueries.push(typeof inner === 'string' ? inner : (inner as Query).toString()); - } - } else { - nextQueries.push(typeof q === 'string' ? q : q.toString()); - } - } - subscription.queries = nextQueries; + subscription.queries = Query.normalize(changes.queries); } this.enqueuePendingSubscribe(subscriptionId); From 8a9dc952d8a41c967e7d7ceda4cc46b5bc5bf8cf Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 16:35:49 +1200 Subject: [PATCH 03/42] refactor: keep query flattening internal to the client Query.normalize was only flattening a builder or nested build()/page() array before send. Call sites already do that on the queries param, so it does not belong on the public Query helper surface. --- .../src/main/java/io/package/Query.kt.twig | 2 +- templates/dart/lib/query.dart.twig | 25 --------- templates/dart/test/query_test.dart.twig | 28 ---------- templates/deno/src/client.ts.twig | 4 +- templates/deno/src/query.ts.twig | 54 +++++++++---------- templates/deno/test/query.test.ts.twig | 10 ++-- templates/dotnet/Package/Query.cs.twig | 2 +- templates/go/client.go.twig | 32 ++++++++++- templates/go/query.go.twig | 4 +- templates/go/query_test.go.twig | 8 +-- .../src/main/kotlin/io/appwrite/Query.kt.twig | 2 +- templates/node/src/client.ts.twig | 4 +- templates/node/test/query.test.js.twig | 10 ++-- templates/php/src/Client.php.twig | 28 +++++++++- templates/php/src/Query.php.twig | 35 ------------ templates/php/tests/QueryTest.php.twig | 36 ++----------- templates/python/package/client.py.twig | 2 +- templates/python/package/query.py.twig | 4 +- templates/python/test/test_query.py.twig | 6 +-- templates/react-native/src/client.ts.twig | 4 +- templates/react-native/src/query.ts.twig | 54 +++++++++---------- templates/ruby/lib/container/client.rb.twig | 4 +- templates/ruby/lib/container/query.rb.twig | 4 +- templates/rust/src/query.rs.twig | 4 +- templates/swift/Sources/Query.swift.twig | 2 +- templates/web/src/client.ts.twig | 6 +-- templates/web/src/query.ts.twig | 54 +++++++++---------- templates/web/src/services/realtime.ts.twig | 6 +-- 28 files changed, 178 insertions(+), 256 deletions(-) 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 e5d58d3062..2b360014c3 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 @@ -574,7 +574,7 @@ class Query( fun builder() = QueryBuilder() - fun normalize(queries: Any?): List { + internal fun normalize(queries: Any?): List { if (queries == null) { return emptyList() } diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index e852e72fb9..964b99e3f2 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -441,31 +441,6 @@ class Query { static QueryBuilder builder() => QueryBuilder(); - /// Flatten a queries argument into query strings. - /// - /// Accepts a [QueryBuilder], a list of query strings, builders, or nested - /// lists (the common `[builder]` / `[builder.build()]` mistakes). - static List normalize(dynamic queries) { - if (queries is QueryBuilder) { - return queries.build(); - } - if (queries is Query) { - return [queries.toString()]; - } - if (queries is String) { - return [queries]; - } - if (queries is! List) { - return []; - } - - final out = []; - for (final item in queries) { - out.addAll(Query.normalize(item)); - } - return out; - } - static String _joinQuery( String method, String table, diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index 7905f16255..96bbb7215a 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -483,32 +483,4 @@ void main() { expect(built, expected); }); - - test('normalize builder direct', () { - final builder = Query.builder().equal('status', 'active').limit(10); - expect(Query.normalize(builder), builder.build()); - }); - - test('normalize builder wrapped in array', () { - final builder = Query.builder().equal('status', 'active').limit(10); - expect(Query.normalize([builder]), builder.build()); - }); - - test('normalize build result wrapped in array', () { - final built = Query.builder().equal('status', 'active').limit(10).build(); - expect(Query.normalize([built]), built); - }); - - test('normalize mixed and page', () { - final normalized = Query.normalize([ - Query.equal('status', 'active'), - Query.page(2, 10), - ]); - - expect(normalized, [ - Query.equal('status', 'active'), - Query.limit(10), - Query.offset(10), - ]); - }); } diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 0631484fda..76ec55f080 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -1,5 +1,5 @@ import { {{ spec.info.title | caseUcfirst}}Exception } from './exception.ts'; -import { Query } from './query.ts'; +import { normalizeQueries } from './query.ts'; export interface Payload { [key: string]: any; @@ -83,7 +83,7 @@ export class Client { async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}, responseType: string = "json") { if (params.queries) { - params.queries = Query.normalize(params.queries); + params.queries = normalizeQueries(params.queries); } headers = {...this.headers, ...headers}; diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index 8925695e30..c8826541a3 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -600,36 +600,6 @@ export class Query { }; static builder = (): QueryBuilder => new QueryBuilder(); - - /** - * Flatten a queries argument into query strings. - * - * Accepts a QueryBuilder, a list of query strings, builders, or nested - * lists (the common `[builder]` / `[builder.build()]` mistakes). - */ - static normalize = (queries?: QueryInput | null): string[] => { - if (queries == null) { - return []; - } - if (queries instanceof QueryBuilder) { - return queries.build(); - } - if (!Array.isArray(queries)) { - if (queries instanceof Query) { - return [queries.toString()]; - } - if (typeof queries === "string") { - return [queries]; - } - return []; - } - - const out: string[] = []; - for (const item of queries) { - out.push(...Query.normalize(item)); - } - return out; - }; } export type QueryInput = @@ -990,3 +960,27 @@ export class QueryBuilder { return this; } } + +export function normalizeQueries(queries?: QueryInput | null): string[] { + if (queries == null) { + return []; + } + if (queries instanceof QueryBuilder) { + return queries.build(); + } + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === "string") { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...normalizeQueries(item)); + } + return out; +} diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index c32151519c..a5784bac86 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -1,6 +1,6 @@ import {describe, it as test} from "https://deno.land/std@0.149.0/testing/bdd.ts"; import {assertEquals} from "https://deno.land/std@0.204.0/assert/assert_equals.ts"; -import {Query, QueryTypes} from "../src/query.ts"; +import {Query, QueryTypes, normalizeQueries} from "../src/query.ts"; type BasicFilterQueryTest = { description: string; @@ -351,13 +351,13 @@ describe('Query', () => { test('normalize builder wrapped in array', () => { const builder = Query.builder().equal('status', 'active').limit(10); - assertEquals(Query.normalize([builder]), builder.build()); - assertEquals(Query.normalize([builder.build()]), builder.build()); - assertEquals(Query.normalize(builder), builder.build()); + assertEquals(normalizeQueries([builder]), builder.build()); + assertEquals(normalizeQueries([builder.build()]), builder.build()); + assertEquals(normalizeQueries(builder), builder.build()); }); test('normalize page wrapped in array', () => { - assertEquals(Query.normalize([Query.equal('status', 'active'), Query.page(2, 10)]), [ + assertEquals(normalizeQueries([Query.equal('status', 'active'), Query.page(2, 10)]), [ Query.equal('status', 'active'), Query.limit(10), Query.offset(10), diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index a97ca995a8..07a1c4ea8c 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -510,7 +510,7 @@ namespace {{ spec.info.title | caseUcfirst }} return new QueryBuilder(); } - public static List Normalize(object? queries) + internal static List Normalize(object? queries) { if (queries == null) { diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index 062093c8b8..658618061a 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -438,7 +438,7 @@ func (client *Client) FileUpload(url string, headers map[string]interface{}, par func (client *Client) Call(method string, path string, headers map[string]interface{}, params map[string]interface{}) (*ClientResponse, error) { if params != nil { if _, ok := params["queries"]; ok { - params["queries"] = query.Normalize(params["queries"]) + params["queries"] = flattenQueries(params["queries"]) } } @@ -761,3 +761,33 @@ func flatten(params interface{}, prefix string, result *map[string]string) error } return nil } + +func flattenQueries(queries interface{}) []string { + if queries == nil { + return []string{} + } + + switch v := queries.(type) { + case *query.QueryBuilder: + if v == nil { + return []string{} + } + return v.Build() + case query.QueryBuilder: + return v.Build() + case string: + return []string{v} + } + + value := reflect.ValueOf(queries) + switch value.Kind() { + case reflect.Slice, reflect.Array: + out := make([]string, 0, value.Len()) + for i := 0; i < value.Len(); i++ { + out = append(out, flattenQueries(value.Index(i).Interface())...) + } + return out + default: + return []string{} + } +} diff --git a/templates/go/query.go.twig b/templates/go/query.go.twig index f3e39b85ca..d09dc09988 100644 --- a/templates/go/query.go.twig +++ b/templates/go/query.go.twig @@ -1051,7 +1051,7 @@ func (b *QueryBuilder) Page(page, perPage int) *QueryBuilder { return b.push(Page(page, perPage)...) } -func Normalize(queries interface{}) []string { +func normalize(queries interface{}) []string { if queries == nil { return []string{} } @@ -1073,7 +1073,7 @@ func Normalize(queries interface{}) []string { case reflect.Slice, reflect.Array: out := make([]string, 0, value.Len()) for i := 0; i < value.Len(); i++ { - out = append(out, Normalize(value.Index(i).Interface())...) + out = append(out, normalize(value.Index(i).Interface())...) } return out default: diff --git a/templates/go/query_test.go.twig b/templates/go/query_test.go.twig index 83a11bbaf2..aa8149c92e 100644 --- a/templates/go/query_test.go.twig +++ b/templates/go/query_test.go.twig @@ -401,21 +401,21 @@ func assertQueriesEqual(t *testing.T, got, expected []string) { func TestNormalizeBuilderDirect(t *testing.T) { builder := Builder().Equal("status", "active").Limit(10) - assertQueriesEqual(t, Normalize(builder), builder.Build()) + assertQueriesEqual(t, normalize(builder), builder.Build()) } func TestNormalizeBuilderWrappedInArray(t *testing.T) { builder := Builder().Equal("status", "active").Limit(10) - assertQueriesEqual(t, Normalize([]interface{}{builder}), builder.Build()) + assertQueriesEqual(t, normalize([]interface{}{builder}), builder.Build()) } func TestNormalizeBuildResultWrappedInArray(t *testing.T) { built := Builder().Equal("status", "active").Limit(10).Build() - assertQueriesEqual(t, Normalize([]interface{}{built}), built) + assertQueriesEqual(t, normalize([]interface{}{built}), built) } func TestNormalizeMixedAndPage(t *testing.T) { - normalized := Normalize([]interface{}{ + normalized := normalize([]interface{}{ Equal("status", "active"), Page(2, 10), }) 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 cc982d08cf..e134603204 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -282,7 +282,7 @@ class Query( fun builder() = QueryBuilder() - fun normalize(queries: Any?): List { + internal fun normalize(queries: Any?): List { if (queries == null) { return emptyList() } diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 66c4040e8a..398b7acc20 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -1,7 +1,7 @@ import { fetch, FormData, File, Agent, type RequestInit } from 'undici'; import { Models } from './models'; import { InputFile } from './inputFile'; -import { Query } from './query'; +import { normalizeQueries } from './query'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true }); @@ -224,7 +224,7 @@ class Client { method = method.toUpperCase(); if (Object.prototype.hasOwnProperty.call(params, 'queries')) { - params = { ...params, queries: Query.normalize(params.queries) }; + params = { ...params, queries: normalizeQueries(params.queries) }; } headers = Object.assign({}, this.headers, headers); diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index 6c7feca3db..53b6d7bd7a 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -1,4 +1,4 @@ -const { Query } = require("../dist/query"); +const { Query, normalizeQueries } = require("../dist/query"); const tests = [ { @@ -229,13 +229,13 @@ describe('Query', () => { test('normalize builder wrapped in array', () => { const builder = Query.builder().equal('status', 'active').limit(10); - expect(Query.normalize([builder])).toEqual(builder.build()); - expect(Query.normalize([builder.build()])).toEqual(builder.build()); - expect(Query.normalize(builder)).toEqual(builder.build()); + expect(normalizeQueries([builder])).toEqual(builder.build()); + expect(normalizeQueries([builder.build()])).toEqual(builder.build()); + expect(normalizeQueries(builder)).toEqual(builder.build()); }); test('normalize page wrapped in array', () => { - expect(Query.normalize([Query.equal('status', 'active'), Query.page(2, 10)])).toEqual([ + expect(normalizeQueries([Query.equal('status', 'active'), Query.page(2, 10)])).toEqual([ Query.equal('status', 'active'), Query.limit(10), Query.offset(10), diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 459c387d60..93f5bd4f0f 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -282,7 +282,7 @@ class Client {% endif %} $headers = array_merge($this->headers, $headers); if (\array_key_exists('queries', $params)) { - $params['queries'] = Query::normalize($params['queries']); + $params['queries'] = self::flattenQueries($params['queries']); } $querySeparator = str_contains($path, '?') ? '&' : '?'; $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? $querySeparator . http_build_query($params) : '')); @@ -421,4 +421,30 @@ class Client return $data; } + + private static function flattenQueries(mixed $queries): array + { + if ($queries instanceof QueryBuilder) { + return $queries->build(); + } + + if (!\is_array($queries)) { + return []; + } + + $out = []; + foreach ($queries as $item) { + if ($item instanceof QueryBuilder) { + array_push($out, ...$item->build()); + } elseif ($item instanceof Query) { + $out[] = $item->__toString(); + } elseif (\is_array($item)) { + array_push($out, ...self::flattenQueries($item)); + } else { + $out[] = $item; + } + } + + return $out; + } } diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 41c2483e9f..87d673db19 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -889,41 +889,6 @@ class Query implements \JsonSerializable return new QueryBuilder(); } - /** - * Flatten a queries argument into a list of query strings. - * - * Accepts a QueryBuilder, a list of query strings, builders, or nested - * lists (the common `[$builder]` / `[$builder->build()]` mistakes). - * - * @param mixed $queries - * @return array - */ - public static function normalize(mixed $queries): array - { - if ($queries instanceof QueryBuilder) { - return $queries->build(); - } - - if (!\is_array($queries)) { - return []; - } - - $out = []; - foreach ($queries as $item) { - if ($item instanceof QueryBuilder) { - array_push($out, ...$item->build()); - } elseif ($item instanceof self) { - $out[] = $item->__toString(); - } elseif (\is_array($item)) { - array_push($out, ...self::normalize($item)); - } else { - $out[] = $item; - } - } - - return $out; - } - private static function joinQuery(string $method, string $table, string $left, string $right, string $operator, string $alias): string { $values = [$left, $operator, $right]; diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index ec400ce00a..4a77202c64 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -1,6 +1,6 @@ tests = array( new BasicFilterQueryTest('with a string', 's', '["s"]'), new BasicFilterQueryTest('with a integer', 1, '[1]'), @@ -443,32 +443,4 @@ final class QueryTest extends TestCase { $this->assertSame($expected, $built); } - - public function testNormalizeBuilderDirect(): void { - $builder = Query::builder()->equal('status', 'active')->limit(10); - $this->assertSame($builder->build(), Query::normalize($builder)); - } - - public function testNormalizeBuilderWrappedInArray(): void { - $builder = Query::builder()->equal('status', 'active')->limit(10); - $this->assertSame($builder->build(), Query::normalize([$builder])); - } - - public function testNormalizeBuildResultWrappedInArray(): void { - $built = Query::builder()->equal('status', 'active')->limit(10)->build(); - $this->assertSame($built, Query::normalize([$built])); - } - - public function testNormalizeMixedAndPage(): void { - $normalized = Query::normalize([ - Query::equal('status', 'active'), - Query::page(2, 10), - ]); - - $this->assertSame([ - Query::equal('status', 'active'), - Query::limit(10), - Query::offset(10), - ], $normalized); - } } diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index 82e3807568..e7eb654a5d 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -71,7 +71,7 @@ class Client: params = {} if 'queries' in params: - params['queries'] = Query.normalize(params['queries']) + params['queries'] = Query._normalize(params['queries']) data = {} files = {} diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index c8e24db89c..c2de6193ec 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -417,7 +417,7 @@ class Query(): return QueryBuilder() @staticmethod - def normalize(queries): + def _normalize(queries): if isinstance(queries, QueryBuilder): return queries.build() @@ -431,7 +431,7 @@ class Query(): elif isinstance(item, Query): out.append(str(item)) elif isinstance(item, list): - out.extend(Query.normalize(item)) + out.extend(Query._normalize(item)) else: out.append(item) diff --git a/templates/python/test/test_query.py.twig b/templates/python/test/test_query.py.twig index 5e56cfbab5..a13aadfd59 100644 --- a/templates/python/test/test_query.py.twig +++ b/templates/python/test/test_query.py.twig @@ -134,6 +134,6 @@ class TestQueryMethods(unittest.TestCase): def test_normalize_builder(self): builder = Query.builder().equal('status', 'active').limit(10) - self.assertEqual(builder.build(), Query.normalize(builder)) - self.assertEqual(builder.build(), Query.normalize([builder])) - self.assertEqual(builder.build(), Query.normalize([builder.build()])) + self.assertEqual(builder.build(), Query._normalize(builder)) + self.assertEqual(builder.build(), Query._normalize([builder])) + self.assertEqual(builder.build(), Query._normalize([builder.build()])) diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index c2cc48e976..80f74fdb11 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -1,7 +1,7 @@ import { Models } from './models'; import { Service } from './service'; import { Platform } from 'react-native'; -import { Query } from './query'; +import { Query, normalizeQueries } from './query'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true }); @@ -570,7 +570,7 @@ class Client { method = method.toUpperCase(); if (Object.prototype.hasOwnProperty.call(params, 'queries')) { - params = { ...params, queries: Query.normalize(params.queries) }; + params = { ...params, queries: normalizeQueries(params.queries) }; } headers = Object.assign({}, this.headers, headers); diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index 60cc99def7..ebeca8928c 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -600,36 +600,6 @@ export class Query { }; static builder = (): QueryBuilder => new QueryBuilder(); - - /** - * Flatten a queries argument into query strings. - * - * Accepts a QueryBuilder, a list of query strings, builders, or nested - * lists (the common `[builder]` / `[builder.build()]` mistakes). - */ - static normalize = (queries?: QueryInput | null): string[] => { - if (queries == null) { - return []; - } - if (queries instanceof QueryBuilder) { - return queries.build(); - } - if (!Array.isArray(queries)) { - if (queries instanceof Query) { - return [queries.toString()]; - } - if (typeof queries === "string") { - return [queries]; - } - return []; - } - - const out: string[] = []; - for (const item of queries) { - out.push(...Query.normalize(item)); - } - return out; - }; } export type QueryInput = @@ -990,3 +960,27 @@ export class QueryBuilder { return this; } } + +export function normalizeQueries(queries?: QueryInput | null): string[] { + if (queries == null) { + return []; + } + if (queries instanceof QueryBuilder) { + return queries.build(); + } + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === "string") { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...normalizeQueries(item)); + } + return out; +} diff --git a/templates/ruby/lib/container/client.rb.twig b/templates/ruby/lib/container/client.rb.twig index 66ff8f39d2..92e7eb044a 100644 --- a/templates/ruby/lib/container/client.rb.twig +++ b/templates/ruby/lib/container/client.rb.twig @@ -112,9 +112,9 @@ module {{ spec.info.title | caseUcfirst }} response_type: nil ) if params.key?(:queries) && !params[:queries].nil? - params[:queries] = Query.normalize(params[:queries]) + params[:queries] = Query._normalize(params[:queries]) elsif params.key?('queries') && !params['queries'].nil? - params['queries'] = Query.normalize(params['queries']) + params['queries'] = Query._normalize(params['queries']) end separator = path.include?('?') ? '&' : '?' diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index 65d81d83f6..d8e09948dc 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -402,7 +402,7 @@ module {{spec.info.title | caseUcfirst}} return QueryBuilder.new end - def normalize(queries) + def _normalize(queries) if queries.is_a?(QueryBuilder) return queries.build end @@ -418,7 +418,7 @@ module {{spec.info.title | caseUcfirst}} elsif item.is_a?(Query) out << item.to_s elsif item.is_a?(Array) - out.concat(normalize(item)) + out.concat(_normalize(item)) else out << item end diff --git a/templates/rust/src/query.rs.twig b/templates/rust/src/query.rs.twig index b99687aac6..fc2e57e6c4 100644 --- a/templates/rust/src/query.rs.twig +++ b/templates/rust/src/query.rs.twig @@ -438,14 +438,14 @@ impl Query { QueryBuilder::new() } - pub fn normalize(value: impl Serialize) -> Vec { + pub(crate) fn normalize(value: impl Serialize) -> Vec { match serde_json::to_value(value) { Ok(v) => Self::normalize_value(&v), Err(_) => vec![], } } - pub fn normalize_value(value: &Value) -> Vec { + pub(crate) fn normalize_value(value: &Value) -> Vec { match value { Value::Null => vec![], Value::String(s) => vec![s.clone()], diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index d4ce04ee56..f7d79f3604 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -846,7 +846,7 @@ public struct Query : Codable, CustomStringConvertible { return QueryBuilder() } - public static func normalize(_ queries: Any?) -> [String] { + static func normalize(_ queries: Any?) -> [String] { guard let queries else { return [] } diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 7f43efe3e3..93300fc338 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1,6 +1,6 @@ import { Models } from './models'; import { Channel, ActionableChannel, ResolvedChannel } from './channel'; -import { Query, type QueryInput } from './query'; +import { normalizeQueries, type QueryInput } from './query'; import { ID } from './id'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); @@ -779,7 +779,7 @@ class Client { }); channelStrings.forEach(channel => this.realtime.channels.add(channel)); - const queryStrings = Query.normalize(queries); + const queryStrings = normalizeQueries(queries); let subscriptionId = ''; const attempts = this.realtime.subscriptions.size + 1; @@ -826,7 +826,7 @@ class Client { method = method.toUpperCase(); if (Object.prototype.hasOwnProperty.call(params, 'queries')) { - params = { ...params, queries: Query.normalize(params.queries) }; + params = { ...params, queries: normalizeQueries(params.queries) }; } headers = Object.assign({}, this.headers, headers); diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index ac10f9e828..104df3f0ec 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -758,36 +758,6 @@ export class Query { }; static builder = (): QueryBuilder => new QueryBuilder(); - - /** - * Flatten a queries argument into query strings. - * - * Accepts a QueryBuilder, a list of query strings, builders, or nested - * lists (the common `[builder]` / `[builder.build()]` mistakes). - */ - static normalize = (queries?: QueryInput | null): string[] => { - if (queries == null) { - return []; - } - if (queries instanceof QueryBuilder) { - return queries.build(); - } - if (!Array.isArray(queries)) { - if (queries instanceof Query) { - return [queries.toString()]; - } - if (typeof queries === "string") { - return [queries]; - } - return []; - } - - const out: string[] = []; - for (const item of queries) { - out.push(...Query.normalize(item)); - } - return out; - }; } export type QueryInput = @@ -1148,3 +1118,27 @@ export class QueryBuilder { return this; } } + +export function normalizeQueries(queries?: QueryInput | null): string[] { + if (queries == null) { + return []; + } + if (queries instanceof QueryBuilder) { + return queries.build(); + } + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === "string") { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...normalizeQueries(item)); + } + return out; +} diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index eafd58eb76..ba36c90e1e 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,6 +1,6 @@ import { {{ spec.info.title | caseUcfirst}}Exception, Client, JSONbig } from '../client'; import { Channel, ActionableChannel, ResolvedChannel } from '../channel'; -import { Query, type QueryInput } from '../query'; +import { normalizeQueries, type QueryInput } from '../query'; import { ID } from '../id'; {% if language.name == 'ReactNative' %} import { Platform } from 'react-native'; @@ -537,7 +537,7 @@ export class Realtime { const channelStrings = channelArray.map(ch => this.channelToString(ch)); const channels = new Set(channelStrings); - const queryStrings = Query.normalize(queries); + const queryStrings = normalizeQueries(queries); const subscriptionId = this.generateUniqueSubscriptionId(); @@ -584,7 +584,7 @@ export class Realtime { } if (changes.queries !== undefined) { - subscription.queries = Query.normalize(changes.queries); + subscription.queries = normalizeQueries(changes.queries); } this.enqueuePendingSubscribe(subscriptionId); From bc7d6b419ed680ea4bac2831aa052455a3b21e4e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 17:29:34 +1200 Subject: [PATCH 04/42] feat: add Query.on and nested join ON overloads Keep the simple join(table, left, right, op, alias) triple. Array overloads encode Query.on() plus filter queries into nested join values so extra ON predicates stay on the JOIN. --- .../src/main/java/io/package/Query.kt.twig | 43 +++++++ templates/dart/lib/query.dart.twig | 94 ++++++++++++---- templates/dart/test/query_test.dart.twig | 18 +++ templates/deno/src/query.ts.twig | 105 ++++++++++++------ templates/deno/test/query.test.ts.twig | 17 +++ templates/dotnet/Package/Query.cs.twig | 104 +++++++++++++++++ templates/go/query.go.twig | 62 +++++++++++ templates/go/query_test.go.twig | 40 +++++++ .../src/main/kotlin/io/appwrite/Query.kt.twig | 43 +++++++ templates/node/test/query.test.js.twig | 17 +++ templates/php/src/Query.php.twig | 69 ++++++++++-- templates/php/src/QueryBuilder.php.twig | 32 ++++-- templates/php/tests/QueryTest.php.twig | 20 ++++ templates/python/package/query.py.twig | 45 ++++++-- templates/python/test/test_query.py.twig | 15 +++ templates/react-native/src/query.ts.twig | 105 ++++++++++++------ templates/ruby/lib/container/query.rb.twig | 43 +++++-- templates/rust/src/query.rs.twig | 103 +++++++++++++++++ templates/skills/dart.md.twig | 4 + templates/skills/dotnet.md.twig | 4 + templates/skills/go.md.twig | 4 + templates/skills/kotlin.md.twig | 4 + templates/skills/php.md.twig | 6 + templates/skills/python.md.twig | 4 + templates/skills/ruby.md.twig | 4 + templates/skills/swift.md.twig | 4 + templates/skills/typescript.md.twig | 4 + templates/swift/Sources/Query.swift.twig | 97 ++++++++++++++++ templates/web/src/query.ts.twig | 105 ++++++++++++------ 29 files changed, 1043 insertions(+), 172 deletions(-) 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 2b360014c3..6b1a34ce6e 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 @@ -548,14 +548,32 @@ class Query( fun distinct() = Query("distinct").toJson() + fun on(left: String, right: String, operator: String = "=") = Query("on", values = listOf(left, operator, right)).toJson() + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + fun join(table: String, on: List) = nestedJoin("join", table, "", on) + + fun join(table: String, alias: String, on: List) = nestedJoin("join", table, alias, on) + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + fun leftJoin(table: String, on: List) = nestedJoin("leftJoin", table, "", on) + + fun leftJoin(table: String, alias: String, on: List) = nestedJoin("leftJoin", table, alias, on) + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + fun rightJoin(table: String, on: List) = nestedJoin("rightJoin", table, "", on) + + fun rightJoin(table: String, alias: String, on: List) = nestedJoin("rightJoin", table, alias, on) + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + fun fullOuterJoin(table: String, on: List) = nestedJoin("fullOuterJoin", table, "", on) + + fun fullOuterJoin(table: String, alias: String, on: List) = nestedJoin("fullOuterJoin", table, alias, on) + fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() @@ -601,6 +619,15 @@ class Query( return Query(method, table, values).toJson() } + private fun nestedJoin(method: String, table: String, alias: String, on: List): String { + val values = mutableListOf() + if (alias.isNotEmpty()) { + values.add(alias) + } + on.forEach { values.add(it.fromJson()) } + return Query(method, table, values).toJson() + } + /** * Parse the value to a list of values. * @@ -763,12 +790,28 @@ class QueryBuilder { fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + fun join(table: String, on: List) = push(Query.join(table, on)) + + fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) + + fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) + + fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) + + fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index 964b99e3f2..24659b00fa 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -372,42 +372,46 @@ class Query { static String distinct() => Query._('distinct').toString(); + /// Column-to-column join ON condition. + static String on(String left, String right, [String operator = '=']) => + Query._('on', null, [left, operator, right]).toString(); + /// Inner join. [table] is the joined collection/table ID. static String join( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - Query._joinQuery('join', table, left, right, operator, alias); + Query._createJoin('join', table, leftOrAliasOrOn, rightOrOn, operator, alias); static String leftJoin( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - Query._joinQuery('leftJoin', table, left, right, operator, alias); + Query._createJoin('leftJoin', table, leftOrAliasOrOn, rightOrOn, operator, alias); static String rightJoin( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - Query._joinQuery('rightJoin', table, left, right, operator, alias); + Query._createJoin('rightJoin', table, leftOrAliasOrOn, rightOrOn, operator, alias); static String fullOuterJoin( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - Query._joinQuery('fullOuterJoin', table, left, right, operator, alias); + Query._createJoin('fullOuterJoin', table, leftOrAliasOrOn, rightOrOn, operator, alias); static String crossJoin(String table, [String alias = '']) => Query._('crossJoin', table, alias.isNotEmpty ? alias : null).toString(); @@ -441,6 +445,48 @@ class Query { static QueryBuilder builder() => QueryBuilder(); + static String _createJoin( + String method, + String table, + Object leftOrAliasOrOn, + Object rightOrOn, + String operator, + String alias, + ) { + if (leftOrAliasOrOn is List) { + return Query._nestedJoin(method, table, '', leftOrAliasOrOn); + } + if (rightOrOn is List) { + return Query._nestedJoin(method, table, leftOrAliasOrOn.toString(), rightOrOn); + } + + return Query._joinQuery( + method, + table, + leftOrAliasOrOn.toString(), + rightOrOn.toString(), + operator, + alias, + ); + } + + static String _nestedJoin( + String method, + String table, + String alias, + List on, + ) { + final values = []; + if (alias.isNotEmpty) { + values.add(alias); + } + for (final query in on) { + values.add(jsonDecode(query.toString())); + } + + return Query._(method, table, values).toString(); + } + static String _joinQuery( String method, String table, @@ -689,39 +735,39 @@ class QueryBuilder { QueryBuilder join( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - _push(Query.join(table, left, right, operator, alias)); + _push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); QueryBuilder leftJoin( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - _push(Query.leftJoin(table, left, right, operator, alias)); + _push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); QueryBuilder rightJoin( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - _push(Query.rightJoin(table, left, right, operator, alias)); + _push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); QueryBuilder fullOuterJoin( String table, - String left, - String right, [ + Object leftOrAliasOrOn, [ + Object rightOrOn = '', String operator = '=', String alias = '', ]) => - _push(Query.fullOuterJoin(table, left, right, operator, alias)); + _push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); QueryBuilder crossJoin(String table, [String alias = '']) => _push(Query.crossJoin(table, alias)); diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index 96bbb7215a..6f1a298cda 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -424,6 +424,24 @@ void main() { expect(query['values'], ['\$id', '=', 'customerId', 'ord']); }); + test('returns on', () { + final query = jsonDecode(Query.on('\$id', 'customerId')); + expect(query['method'], 'on'); + expect(query['values'], ['\$id', '=', 'customerId']); + }); + + test('returns nested leftJoin', () { + final query = jsonDecode(Query.leftJoin('orders', 'ord', [ + Query.on('\$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ])); + expect(query['method'], 'leftJoin'); + expect(query['attribute'], 'orders'); + expect(query['values'][0], 'ord'); + expect(query['values'][1]['method'], 'on'); + expect(query['values'][2]['method'], 'equal'); + }); + test('returns crossJoin', () { final query = jsonDecode(Query.crossJoin('orders', 'ord')); expect(query['method'], 'crossJoin'); diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index c8826541a3..26f880d4c5 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -516,60 +516,54 @@ export class Query { static distinct = (): string => new Query("distinct").toString(); + /** + * Column-to-column join ON condition. + */ + static on = ( + left: string, + right: string, + operator: string = "=" + ): string => + new Query("on", undefined, [left, operator, right]).toString(); + /** * Inner join. `table` is the joined collection/table ID. */ static join = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "join", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("join", table, leftOrAliasOrOn, rightOrOn, operator, alias); static leftJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "leftJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("leftJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static rightJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "rightJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("rightJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static fullOuterJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "fullOuterJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("fullOuterJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static crossJoin = (table: string, alias: string = ""): string => new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); @@ -600,6 +594,45 @@ export class Query { }; static builder = (): QueryBuilder => new QueryBuilder(); + + private static createJoin( + method: string, + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", + operator: string = "=", + alias: string = "" + ): string { + if (Array.isArray(leftOrAliasOrOn)) { + return Query.nestedJoin(method, table, "", leftOrAliasOrOn); + } + if (Array.isArray(rightOrOn)) { + return Query.nestedJoin(method, table, leftOrAliasOrOn, rightOrOn); + } + return new Query( + method, + table, + alias !== "" + ? [leftOrAliasOrOn, operator, rightOrOn, alias] + : [leftOrAliasOrOn, operator, rightOrOn] + ).toString(); + } + + private static nestedJoin( + method: string, + table: string, + alias: string, + on: string[] + ): string { + const values: any[] = []; + if (alias !== "") { + values.push(alias); + } + for (const query of on) { + values.push(JSONbig.parse(query)); + } + return new Query(method, table, values).toString(); + } } export type QueryInput = @@ -899,20 +932,20 @@ export class QueryBuilder { return this.push(Query.distinct()); } - join(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.join(table, left, right, operator, alias)); + join(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - leftJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.leftJoin(table, left, right, operator, alias)); + leftJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - rightJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.rightJoin(table, left, right, operator, alias)); + rightJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - fullOuterJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.fullOuterJoin(table, left, right, operator, alias)); + fullOuterJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } crossJoin(table: string, alias: string = ""): this { diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index a5784bac86..da6b073236 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -313,6 +313,23 @@ describe('Query', () => { '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', )); + test('on', () => assertEquals( + Query.on('$id', 'customerId').toString(), + '{"method":"on","values":["$id","=","customerId"]}', + )); + + test('nested leftJoin', () => { + const query = JSON.parse(Query.leftJoin('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ]).toString()); + assertEquals(query.method, 'leftJoin'); + assertEquals(query.attribute, 'orders'); + assertEquals(query.values[0], 'ord'); + assertEquals(query.values[1].method, 'on'); + assertEquals(query.values[2].method, 'equal'); + }); + test('crossJoin', () => assertEquals( Query.crossJoin('orders', 'ord').toString(), '{"method":"crossJoin","attribute":"orders","values":["ord"]}', diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 07a1c4ea8c..3310845c4c 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -446,26 +446,71 @@ namespace {{ spec.info.title | caseUcfirst }} return new Query("distinct", null, null).ToString(); } + public static string On(string left, string right, string @operator = "=") + { + return new Query("on", null, new List { left, @operator, right }).ToString(); + } + public static string Join(string table, string left, string right, string @operator = "=", string alias = "") { return JoinQuery("join", table, left, right, @operator, alias); } + public static string Join(string table, List on) + { + return NestedJoin("join", table, "", on); + } + + public static string Join(string table, string alias, List on) + { + return NestedJoin("join", table, alias, on); + } + public static string LeftJoin(string table, string left, string right, string @operator = "=", string alias = "") { return JoinQuery("leftJoin", table, left, right, @operator, alias); } + public static string LeftJoin(string table, List on) + { + return NestedJoin("leftJoin", table, "", on); + } + + public static string LeftJoin(string table, string alias, List on) + { + return NestedJoin("leftJoin", table, alias, on); + } + public static string RightJoin(string table, string left, string right, string @operator = "=", string alias = "") { return JoinQuery("rightJoin", table, left, right, @operator, alias); } + public static string RightJoin(string table, List on) + { + return NestedJoin("rightJoin", table, "", on); + } + + public static string RightJoin(string table, string alias, List on) + { + return NestedJoin("rightJoin", table, alias, on); + } + public static string FullOuterJoin(string table, string left, string right, string @operator = "=", string alias = "") { return JoinQuery("fullOuterJoin", table, left, right, @operator, alias); } + public static string FullOuterJoin(string table, List on) + { + return NestedJoin("fullOuterJoin", table, "", on); + } + + public static string FullOuterJoin(string table, string alias, List on) + { + return NestedJoin("fullOuterJoin", table, alias, on); + } + public static string CrossJoin(string table, string alias = "") { return new Query("crossJoin", table, alias != "" ? alias : null).ToString(); @@ -551,6 +596,25 @@ namespace {{ spec.info.title | caseUcfirst }} return new Query(method, table, values).ToString(); } + + private static string NestedJoin(string method, string table, string alias, List on) + { + var values = new List(); + if (alias != "") + { + values.Add(alias); + } + foreach (var query in on) + { + var parsed = JsonSerializer.Deserialize(query, Client.DeserializerOptions); + if (parsed != null) + { + values.Add(parsed); + } + } + + return new Query(method, table, values).ToString(); + } } public class QueryBuilder @@ -937,21 +1001,61 @@ namespace {{ spec.info.title | caseUcfirst }} return Push(Query.Join(table, left, right, @operator, alias)); } + public QueryBuilder Join(string table, List on) + { + return Push(Query.Join(table, on)); + } + + public QueryBuilder Join(string table, string alias, List on) + { + return Push(Query.Join(table, alias, on)); + } + public QueryBuilder LeftJoin(string table, string left, string right, string @operator = "=", string alias = "") { return Push(Query.LeftJoin(table, left, right, @operator, alias)); } + public QueryBuilder LeftJoin(string table, List on) + { + return Push(Query.LeftJoin(table, on)); + } + + public QueryBuilder LeftJoin(string table, string alias, List on) + { + return Push(Query.LeftJoin(table, alias, on)); + } + public QueryBuilder RightJoin(string table, string left, string right, string @operator = "=", string alias = "") { return Push(Query.RightJoin(table, left, right, @operator, alias)); } + public QueryBuilder RightJoin(string table, List on) + { + return Push(Query.RightJoin(table, on)); + } + + public QueryBuilder RightJoin(string table, string alias, List on) + { + return Push(Query.RightJoin(table, alias, on)); + } + public QueryBuilder FullOuterJoin(string table, string left, string right, string @operator = "=", string alias = "") { return Push(Query.FullOuterJoin(table, left, right, @operator, alias)); } + public QueryBuilder FullOuterJoin(string table, List on) + { + return Push(Query.FullOuterJoin(table, on)); + } + + public QueryBuilder FullOuterJoin(string table, string alias, List on) + { + return Push(Query.FullOuterJoin(table, alias, on)); + } + public QueryBuilder CrossJoin(string table, string alias = "") { return Push(Query.CrossJoin(table, alias)); diff --git a/templates/go/query.go.twig b/templates/go/query.go.twig index d09dc09988..4f82acc60a 100644 --- a/templates/go/query.go.twig +++ b/templates/go/query.go.twig @@ -550,6 +550,25 @@ func joinQuery(method, table, left, right, operator, alias string) string { }) } +func nestedJoin(method, table, alias string, on []string) string { + var values []interface{} + if alias != "" { + values = append(values, alias) + } + for _, query := range on { + var parsed interface{} + if err := json.Unmarshal([]byte(query), &parsed); err != nil { + continue + } + values = append(values, parsed) + } + return parseQuery(queryOptions{ + Method: method, + Attribute: &table, + Values: &values, + }) +} + func Count(attribute, alias string) string { return aliasedQuery("count", attribute, alias) } @@ -638,6 +657,17 @@ func Distinct() string { }) } +func On(left, right, operator string) string { + if operator == "" { + operator = "=" + } + values := []interface{}{left, operator, right} + return parseQuery(queryOptions{ + Method: "on", + Values: &values, + }) +} + func Join(table, left, right, operator, alias string) string { return joinQuery("join", table, left, right, operator, alias) } @@ -654,6 +684,22 @@ func FullOuterJoin(table, left, right, operator, alias string) string { return joinQuery("fullOuterJoin", table, left, right, operator, alias) } +func JoinOn(table, alias string, on []string) string { + return nestedJoin("join", table, alias, on) +} + +func LeftJoinOn(table, alias string, on []string) string { + return nestedJoin("leftJoin", table, alias, on) +} + +func RightJoinOn(table, alias string, on []string) string { + return nestedJoin("rightJoin", table, alias, on) +} + +func FullOuterJoinOn(table, alias string, on []string) string { + return nestedJoin("fullOuterJoin", table, alias, on) +} + func CrossJoin(table, alias string) string { return aliasedQuery("crossJoin", table, alias) } @@ -1015,6 +1061,22 @@ func (b *QueryBuilder) FullOuterJoin(table, left, right, operator, alias string) return b.push(FullOuterJoin(table, left, right, operator, alias)) } +func (b *QueryBuilder) JoinOn(table, alias string, on []string) *QueryBuilder { + return b.push(JoinOn(table, alias, on)) +} + +func (b *QueryBuilder) LeftJoinOn(table, alias string, on []string) *QueryBuilder { + return b.push(LeftJoinOn(table, alias, on)) +} + +func (b *QueryBuilder) RightJoinOn(table, alias string, on []string) *QueryBuilder { + return b.push(RightJoinOn(table, alias, on)) +} + +func (b *QueryBuilder) FullOuterJoinOn(table, alias string, on []string) *QueryBuilder { + return b.push(FullOuterJoinOn(table, alias, on)) +} + func (b *QueryBuilder) CrossJoin(table, alias string) *QueryBuilder { return b.push(CrossJoin(table, alias)) } diff --git a/templates/go/query_test.go.twig b/templates/go/query_test.go.twig index aa8149c92e..5124b37753 100644 --- a/templates/go/query_test.go.twig +++ b/templates/go/query_test.go.twig @@ -283,6 +283,46 @@ func TestJoinWithAlias(t *testing.T) { } } +func TestOn(t *testing.T) { + q := On("$id", "customerId", "") + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + if data["method"] != "on" { + t.Errorf("Expected on, got %v", data["method"]) + } + values := data["values"].([]interface{}) + if values[0] != "$id" || values[1] != "=" || values[2] != "customerId" { + t.Errorf("Expected [$id, =, customerId], got %v", values) + } +} + +func TestNestedLeftJoin(t *testing.T) { + q := LeftJoinOn("orders", "ord", []string{ + On("$id", "customerId", ""), + Equal("ord.status", "paid"), + }) + var data map[string]interface{} + if err := json.Unmarshal([]byte(q), &data); err != nil { + t.Fatal(err) + } + if data["method"] != "leftJoin" { + t.Errorf("Expected leftJoin, got %v", data["method"]) + } + if data["attribute"] != "orders" { + t.Errorf("Expected orders, got %v", data["attribute"]) + } + values := data["values"].([]interface{}) + if values[0] != "ord" { + t.Errorf("Expected alias ord, got %v", values[0]) + } + on := values[1].(map[string]interface{}) + if on["method"] != "on" { + t.Errorf("Expected on, got %v", on["method"]) + } +} + func TestCrossJoin(t *testing.T) { q := CrossJoin("orders", "ord") var data map[string]interface{} 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 e134603204..f72da86a0a 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -256,14 +256,32 @@ class Query( fun distinct() = Query("distinct").toJson() + fun on(left: String, right: String, operator: String = "=") = Query("on", values = listOf(left, operator, right)).toJson() + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + fun join(table: String, on: List) = nestedJoin("join", table, "", on) + + fun join(table: String, alias: String, on: List) = nestedJoin("join", table, alias, on) + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + fun leftJoin(table: String, on: List) = nestedJoin("leftJoin", table, "", on) + + fun leftJoin(table: String, alias: String, on: List) = nestedJoin("leftJoin", table, alias, on) + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + fun rightJoin(table: String, on: List) = nestedJoin("rightJoin", table, "", on) + + fun rightJoin(table: String, alias: String, on: List) = nestedJoin("rightJoin", table, alias, on) + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + fun fullOuterJoin(table: String, on: List) = nestedJoin("fullOuterJoin", table, "", on) + + fun fullOuterJoin(table: String, alias: String, on: List) = nestedJoin("fullOuterJoin", table, alias, on) + fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() @@ -309,6 +327,15 @@ class Query( return Query(method, table, values).toJson() } + private fun nestedJoin(method: String, table: String, alias: String, on: List): String { + val values = mutableListOf() + if (alias.isNotEmpty()) { + values.add(alias) + } + on.forEach { values.add(it.fromJson()) } + return Query(method, table, values).toJson() + } + private fun parseValue(value: Any): List { return when (value) { is List<*> -> value as List @@ -465,12 +492,28 @@ class QueryBuilder { fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + fun join(table: String, on: List) = push(Query.join(table, on)) + + fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) + + fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) + + fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) + + fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index 53b6d7bd7a..3680a16fc7 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -191,6 +191,23 @@ describe('Query', () => { .toEqual('{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}') ); + test('on', () => + expect(Query.on('$id', 'customerId')) + .toEqual('{"method":"on","values":["$id","=","customerId"]}') + ); + + test('nested leftJoin', () => { + const query = JSON.parse(Query.leftJoin('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ])); + expect(query.method).toEqual('leftJoin'); + expect(query.attribute).toEqual('orders'); + expect(query.values[0]).toEqual('ord'); + expect(query.values[1].method).toEqual('on'); + expect(query.values[2].method).toEqual('equal'); + }); + test('crossJoin', () => expect(Query.crossJoin('orders', 'ord')) .toEqual('{"method":"crossJoin","attribute":"orders","values":["ord"]}') diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 87d673db19..852f248f94 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -809,24 +809,45 @@ class Query implements \JsonSerializable return (new Query('distinct', null, null))->__toString(); } - public static function join(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + public static function on(string $left, string $right, string $operator = '='): string { - return self::joinQuery('join', $table, $left, $right, $operator, $alias); + return (new Query('on', null, [$left, $operator, $right]))->__toString(); } - public static function leftJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public static function join(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { - return self::joinQuery('leftJoin', $table, $left, $right, $operator, $alias); + return self::createJoin('join', $table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias); } - public static function rightJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public static function leftJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { - return self::joinQuery('rightJoin', $table, $left, $right, $operator, $alias); + return self::createJoin('leftJoin', $table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias); } - public static function fullOuterJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): string + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public static function rightJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { - return self::joinQuery('fullOuterJoin', $table, $left, $right, $operator, $alias); + return self::createJoin('rightJoin', $table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias); + } + + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public static function fullOuterJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string + { + return self::createJoin('fullOuterJoin', $table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias); } public static function crossJoin(string $table, string $alias = ''): string @@ -889,6 +910,38 @@ class Query implements \JsonSerializable return new QueryBuilder(); } + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + private static function createJoin(string $method, string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn, string $operator, string $alias): string + { + if (\is_array($leftOrAliasOrOn)) { + return self::nestedJoin($method, $table, '', $leftOrAliasOrOn); + } + if (\is_array($rightOrOn)) { + return self::nestedJoin($method, $table, $leftOrAliasOrOn, $rightOrOn); + } + + return self::joinQuery($method, $table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias); + } + + /** + * @param array $on + */ + private static function nestedJoin(string $method, string $table, string $alias, array $on): string + { + $values = []; + if ($alias !== '') { + $values[] = $alias; + } + foreach ($on as $query) { + $values[] = \json_decode($query, true); + } + + return (new Query($method, $table, $values))->__toString(); + } + private static function joinQuery(string $method, string $table, string $left, string $right, string $operator, string $alias): string { $values = [$left, $operator, $right]; diff --git a/templates/php/src/QueryBuilder.php.twig b/templates/php/src/QueryBuilder.php.twig index 412315355f..e4686dd0c1 100644 --- a/templates/php/src/QueryBuilder.php.twig +++ b/templates/php/src/QueryBuilder.php.twig @@ -433,24 +433,40 @@ class QueryBuilder return $this->push(Query::distinct()); } - public function join(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public function join(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): self { - return $this->push(Query::join($table, $left, $right, $operator, $alias)); + return $this->push(Query::join($table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias)); } - public function leftJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public function leftJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): self { - return $this->push(Query::leftJoin($table, $left, $right, $operator, $alias)); + return $this->push(Query::leftJoin($table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias)); } - public function rightJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public function rightJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): self { - return $this->push(Query::rightJoin($table, $left, $right, $operator, $alias)); + return $this->push(Query::rightJoin($table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias)); } - public function fullOuterJoin(string $table, string $left, string $right, string $operator = '=', string $alias = ''): self + /** + * @param string|array $leftOrAliasOrOn + * @param string|array $rightOrOn + */ + public function fullOuterJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): self { - return $this->push(Query::fullOuterJoin($table, $left, $right, $operator, $alias)); + return $this->push(Query::fullOuterJoin($table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias)); } public function crossJoin(string $table, string $alias = ''): self diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index 4a77202c64..1d85857f72 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -388,6 +388,26 @@ final class QueryTest extends TestCase { $this->assertSame(['$id', '=', 'customerId', 'ord'], $query['values']); } + public function testOn(): void { + $query = json_decode(Query::on('$id', 'customerId'), true); + $this->assertSame('on', $query['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + + public function testNestedLeftJoin(): void { + $query = json_decode(Query::leftJoin('orders', 'ord', [ + Query::on('$id', 'customerId'), + Query::equal('ord.status', 'paid'), + ]), true); + $this->assertSame('leftJoin', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame('ord', $query['values'][0]); + $this->assertSame('on', $query['values'][1]['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values'][1]['values']); + $this->assertSame('equal', $query['values'][2]['method']); + $this->assertSame('ord.status', $query['values'][2]['attribute']); + } + public function testCrossJoin(): void { $query = json_decode(Query::crossJoin('orders', 'ord'), true); $this->assertSame('crossJoin', $query['method']); diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index c2de6193ec..ca03f3bedf 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -365,20 +365,24 @@ class Query(): return str(Query("distinct", None, None)) @staticmethod - def join(table, left, right, operator='=', alias=''): - return Query._join("join", table, left, right, operator, alias) + def on(left, right, operator='='): + return str(Query("on", None, [left, operator, right])) @staticmethod - def left_join(table, left, right, operator='=', alias=''): - return Query._join("leftJoin", table, left, right, operator, alias) + def join(table, left, right='', operator='=', alias=''): + return Query._create_join("join", table, left, right, operator, alias) @staticmethod - def right_join(table, left, right, operator='=', alias=''): - return Query._join("rightJoin", table, left, right, operator, alias) + def left_join(table, left, right='', operator='=', alias=''): + return Query._create_join("leftJoin", table, left, right, operator, alias) @staticmethod - def full_outer_join(table, left, right, operator='=', alias=''): - return Query._join("fullOuterJoin", table, left, right, operator, alias) + def right_join(table, left, right='', operator='=', alias=''): + return Query._create_join("rightJoin", table, left, right, operator, alias) + + @staticmethod + def full_outer_join(table, left, right='', operator='=', alias=''): + return Query._create_join("fullOuterJoin", table, left, right, operator, alias) @staticmethod def cross_join(table, alias=''): @@ -437,6 +441,23 @@ class Query(): return out + @staticmethod + def _create_join(method, table, left, right, operator, alias): + if isinstance(left, list): + return Query._nested_join(method, table, '', left) + if isinstance(right, list): + return Query._nested_join(method, table, left, right) + return Query._join(method, table, left, right, operator, alias) + + @staticmethod + def _nested_join(method, table, alias, on): + values = [] + if alias != '': + values.append(alias) + for query in on: + values.append(json.loads(query)) + return str(Query(method, table, values)) + @staticmethod def _join(method, table, left, right, operator, alias): values = [left, operator, right] @@ -661,16 +682,16 @@ class QueryBuilder: def distinct(self): return self._push(Query.distinct()) - def join(self, table, left, right, operator='=', alias=''): + def join(self, table, left, right='', operator='=', alias=''): return self._push(Query.join(table, left, right, operator, alias)) - def left_join(self, table, left, right, operator='=', alias=''): + def left_join(self, table, left, right='', operator='=', alias=''): return self._push(Query.left_join(table, left, right, operator, alias)) - def right_join(self, table, left, right, operator='=', alias=''): + def right_join(self, table, left, right='', operator='=', alias=''): return self._push(Query.right_join(table, left, right, operator, alias)) - def full_outer_join(self, table, left, right, operator='=', alias=''): + def full_outer_join(self, table, left, right='', operator='=', alias=''): return self._push(Query.full_outer_join(table, left, right, operator, alias)) def cross_join(self, table, alias=''): diff --git a/templates/python/test/test_query.py.twig b/templates/python/test/test_query.py.twig index a13aadfd59..c29df9f529 100644 --- a/templates/python/test/test_query.py.twig +++ b/templates/python/test/test_query.py.twig @@ -1,3 +1,4 @@ +import json import unittest from {{ namespace | caseSnake }}.query import Query @@ -112,6 +113,20 @@ class TestQueryMethods(unittest.TestCase): def test_join(self): self.assertEqual(Query.join('orders', '$id', 'customerId'), '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}') + def test_on(self): + self.assertEqual(Query.on('$id', 'customerId'), '{"method":"on","values":["$id","=","customerId"]}') + + def test_nested_left_join(self): + query = json.loads(Query.left_join('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ])) + self.assertEqual(query['method'], 'leftJoin') + self.assertEqual(query['attribute'], 'orders') + self.assertEqual(query['values'][0], 'ord') + self.assertEqual(query['values'][1]['method'], 'on') + self.assertEqual(query['values'][2]['method'], 'equal') + def test_group_by(self): self.assertEqual(Query.group_by(['status']), '{"method":"groupBy","values":["status"]}') diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index ebeca8928c..edb12ef79e 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -516,60 +516,54 @@ export class Query { static distinct = (): string => new Query("distinct").toString(); + /** + * Column-to-column join ON condition. + */ + static on = ( + left: string, + right: string, + operator: string = "=" + ): string => + new Query("on", undefined, [left, operator, right]).toString(); + /** * Inner join. `table` is the joined collection/table ID. */ static join = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "join", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("join", table, leftOrAliasOrOn, rightOrOn, operator, alias); static leftJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "leftJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("leftJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static rightJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "rightJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("rightJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static fullOuterJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "fullOuterJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("fullOuterJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static crossJoin = (table: string, alias: string = ""): string => new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); @@ -600,6 +594,45 @@ export class Query { }; static builder = (): QueryBuilder => new QueryBuilder(); + + private static createJoin( + method: string, + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", + operator: string = "=", + alias: string = "" + ): string { + if (Array.isArray(leftOrAliasOrOn)) { + return Query.nestedJoin(method, table, "", leftOrAliasOrOn); + } + if (Array.isArray(rightOrOn)) { + return Query.nestedJoin(method, table, leftOrAliasOrOn, rightOrOn); + } + return new Query( + method, + table, + alias !== "" + ? [leftOrAliasOrOn, operator, rightOrOn, alias] + : [leftOrAliasOrOn, operator, rightOrOn] + ).toString(); + } + + private static nestedJoin( + method: string, + table: string, + alias: string, + on: string[] + ): string { + const values: any[] = []; + if (alias !== "") { + values.push(alias); + } + for (const query of on) { + values.push(JSONbig.parse(query)); + } + return new Query(method, table, values).toString(); + } } export type QueryInput = @@ -899,20 +932,20 @@ export class QueryBuilder { return this.push(Query.distinct()); } - join(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.join(table, left, right, operator, alias)); + join(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - leftJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.leftJoin(table, left, right, operator, alias)); + leftJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - rightJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.rightJoin(table, left, right, operator, alias)); + rightJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - fullOuterJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.fullOuterJoin(table, left, right, operator, alias)); + fullOuterJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } crossJoin(table: string, alias: string = ""): this { diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index d8e09948dc..0bb346a7bd 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -348,20 +348,24 @@ module {{spec.info.title | caseUcfirst}} return Query.new("distinct", nil, nil).to_s end - def join(table, left, right, operator = '=', alias_name = '') - return join_query("join", table, left, right, operator, alias_name) + def on(left, right, operator = '=') + return Query.new("on", nil, [left, operator, right]).to_s end - def left_join(table, left, right, operator = '=', alias_name = '') - return join_query("leftJoin", table, left, right, operator, alias_name) + def join(table, left, right = '', operator = '=', alias_name = '') + return create_join("join", table, left, right, operator, alias_name) end - def right_join(table, left, right, operator = '=', alias_name = '') - return join_query("rightJoin", table, left, right, operator, alias_name) + def left_join(table, left, right = '', operator = '=', alias_name = '') + return create_join("leftJoin", table, left, right, operator, alias_name) end - def full_outer_join(table, left, right, operator = '=', alias_name = '') - return join_query("fullOuterJoin", table, left, right, operator, alias_name) + def right_join(table, left, right = '', operator = '=', alias_name = '') + return create_join("rightJoin", table, left, right, operator, alias_name) + end + + def full_outer_join(table, left, right = '', operator = '=', alias_name = '') + return create_join("fullOuterJoin", table, left, right, operator, alias_name) end def cross_join(table, alias_name = '') @@ -429,6 +433,21 @@ module {{spec.info.title | caseUcfirst}} private + def create_join(method, table, left, right, operator, alias_name) + return nested_join(method, table, '', left) if left.is_a?(Array) + return nested_join(method, table, left, right) if right.is_a?(Array) + + return join_query(method, table, left, right, operator, alias_name) + end + + def nested_join(method, table, alias_name, on) + values = [] + values << alias_name if alias_name != '' + on.each { |query| values << JSON.parse(query) } + + return Query.new(method, table, values).to_s + end + def join_query(method, table, left, right, operator, alias_name) values = [left, operator, right] values << alias_name if alias_name != '' @@ -727,19 +746,19 @@ module {{spec.info.title | caseUcfirst}} return push(Query.distinct()) end - def join(table, left, right, operator = '=', alias_name = '') + def join(table, left, right = '', operator = '=', alias_name = '') return push(Query.join(table, left, right, operator, alias_name)) end - def left_join(table, left, right, operator = '=', alias_name = '') + def left_join(table, left, right = '', operator = '=', alias_name = '') return push(Query.left_join(table, left, right, operator, alias_name)) end - def right_join(table, left, right, operator = '=', alias_name = '') + def right_join(table, left, right = '', operator = '=', alias_name = '') return push(Query.right_join(table, left, right, operator, alias_name)) end - def full_outer_join(table, left, right, operator = '=', alias_name = '') + def full_outer_join(table, left, right = '', operator = '=', alias_name = '') return push(Query.full_outer_join(table, left, right, operator, alias_name)) end diff --git a/templates/rust/src/query.rs.twig b/templates/rust/src/query.rs.twig index fc2e57e6c4..8145d7f8e5 100644 --- a/templates/rust/src/query.rs.twig +++ b/templates/rust/src/query.rs.twig @@ -364,6 +364,22 @@ impl Query { Self::new("distinct".to_string(), None, vec![]) } + pub fn on( + left: impl Into, + right: impl Into, + operator: impl Into, + ) -> Self { + Self::new( + "on".to_string(), + None, + vec![ + Value::String(left.into()), + Value::String(operator.into()), + Value::String(right.into()), + ], + ) + } + pub fn join( table: impl Into, left: impl Into, @@ -408,6 +424,38 @@ impl Query { Self::aliased("crossJoin", table, alias) } + pub fn join_on(table: impl Into, alias: impl Into, on: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + Self::nested_join("join", table, alias, on) + } + + pub fn left_join_on(table: impl Into, alias: impl Into, on: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + Self::nested_join("leftJoin", table, alias, on) + } + + pub fn right_join_on(table: impl Into, alias: impl Into, on: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + Self::nested_join("rightJoin", table, alias, on) + } + + pub fn full_outer_join_on(table: impl Into, alias: impl Into, on: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + Self::nested_join("fullOuterJoin", table, alias, on) + } + pub fn covers, V: Into>(attribute: S, values: V) -> Self { Self::new("covers".to_string(), Some(attribute.into()), vec![values.into()]) } @@ -485,6 +533,24 @@ impl Query { Self::new(method.to_string(), Some(table.into()), values) } + fn nested_join(method: &str, table: impl Into, alias: impl Into, on: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let mut values = Vec::new(); + let alias = alias.into(); + if !alias.is_empty() { + values.push(Value::String(alias)); + } + for query in on { + if let Ok(parsed) = serde_json::from_str::(query.as_ref()) { + values.push(parsed); + } + } + Self::new(method.to_string(), Some(table.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(); @@ -919,6 +985,15 @@ impl QueryBuilder { self } + pub fn left_join_on(mut self, table: impl Into, alias: impl Into, on: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::left_join_on(table, alias, on)); + self + } + pub fn right_join( mut self, table: impl Into, @@ -1642,6 +1717,34 @@ mod tests { assert_eq!(value["values"][3], "ord"); } + #[test] + fn test_on() { + let query = Query::on("$id", "customerId", "="); + let value = query.to_value(); + assert_eq!(value["method"], "on"); + assert_eq!(value["values"][0], "$id"); + assert_eq!(value["values"][1], "="); + assert_eq!(value["values"][2], "customerId"); + } + + #[test] + fn test_nested_left_join() { + let query = Query::left_join_on( + "orders", + "ord", + [ + Query::on("$id", "customerId", "=").to_string(), + Query::equal("ord.status", "paid").to_string(), + ], + ); + let value = query.to_value(); + assert_eq!(value["method"], "leftJoin"); + assert_eq!(value["attribute"], "orders"); + assert_eq!(value["values"][0], "ord"); + assert_eq!(value["values"][1]["method"], "on"); + assert_eq!(value["values"][2]["method"], "equal"); + } + #[test] fn test_cross_join() { let query = Query::cross_join("orders", "ord"); diff --git a/templates/skills/dart.md.twig b/templates/skills/dart.md.twig index c70bdd23cb..b469d1bef1 100644 --- a/templates/skills/dart.md.twig +++ b/templates/skills/dart.md.twig @@ -205,6 +205,10 @@ Query.distinct() // Joins (table id, left key, right key, operator, alias) Query.join('orders', r'$id', 'customerId') Query.leftJoin('orders', r'$id', 'customerId', '=', 'ord') +Query.leftJoin('orders', 'ord', [ + Query.on(r'$id', 'customerId'), + Query.equal('ord.status', 'paid'), +]) // Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. final queries = Query.builder() diff --git a/templates/skills/dotnet.md.twig b/templates/skills/dotnet.md.twig index 0f9ca1c669..5c61a2b153 100644 --- a/templates/skills/dotnet.md.twig +++ b/templates/skills/dotnet.md.twig @@ -148,6 +148,10 @@ Query.Distinct() // Joins (table id, left key, right key, operator, alias) Query.Join("orders", "$id", "customerId") Query.LeftJoin("orders", "$id", "customerId", "=", "ord") +Query.LeftJoin("orders", "ord", new List { + Query.On("$id", "customerId"), + Query.Equal("ord.status", "paid"), +}) // Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. var queries = Query.Builder() diff --git a/templates/skills/go.md.twig b/templates/skills/go.md.twig index 3f48cfb7dd..52fe57cb91 100644 --- a/templates/skills/go.md.twig +++ b/templates/skills/go.md.twig @@ -173,6 +173,10 @@ query.Distinct() // Joins (table id, left key, right key, operator, alias) query.Join("orders", "$id", "customerId") query.LeftJoin("orders", "$id", "customerId", "=", "ord") +query.LeftJoinOn("orders", "ord", []string{ + query.On("$id", "customerId", ""), + query.Equal("ord.status", "paid"), +}) // Fluent builder. Wrapping the builder in an extra slice is OK — the SDK flattens it. queries := query.Builder(). diff --git a/templates/skills/kotlin.md.twig b/templates/skills/kotlin.md.twig index c3d1aa56bd..97aa825e70 100644 --- a/templates/skills/kotlin.md.twig +++ b/templates/skills/kotlin.md.twig @@ -229,6 +229,10 @@ Query.distinct() // Joins (table id, left key, right key, operator, alias) Query.join("orders", "\$id", "customerId") Query.leftJoin("orders", "\$id", "customerId", "=", "ord") +Query.leftJoin("orders", "ord", listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), +)) // Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. val queries = Query.builder() diff --git a/templates/skills/php.md.twig b/templates/skills/php.md.twig index 26a35b292a..a35dbbc78d 100644 --- a/templates/skills/php.md.twig +++ b/templates/skills/php.md.twig @@ -157,6 +157,12 @@ Query::distinct() Query::join('orders', '$id', 'customerId') Query::leftJoin('orders', '$id', 'customerId', '=', 'ord') +// Nested join ON: extra predicates stay on the JOIN, not WHERE +Query::leftJoin('orders', 'ord', [ + Query::on('$id', 'customerId'), + Query::equal('ord.status', 'paid'), +]) + // Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. $queries = Query::builder() ->equal('status', 'published') diff --git a/templates/skills/python.md.twig b/templates/skills/python.md.twig index 4ae5d84566..b76e20b245 100644 --- a/templates/skills/python.md.twig +++ b/templates/skills/python.md.twig @@ -163,6 +163,10 @@ Query.distinct() # Joins (table id, left key, right key, operator, alias) Query.join('orders', '$id', 'customerId') Query.left_join('orders', '$id', 'customerId', '=', 'ord') +Query.left_join('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), +]) # Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. queries = ( diff --git a/templates/skills/ruby.md.twig b/templates/skills/ruby.md.twig index 0cb2282622..672daac093 100644 --- a/templates/skills/ruby.md.twig +++ b/templates/skills/ruby.md.twig @@ -163,6 +163,10 @@ Query.distinct # Joins (table id, left key, right key, operator, alias) Query.join('orders', '$id', 'customerId') Query.left_join('orders', '$id', 'customerId', '=', 'ord') +Query.left_join('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), +]) # Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. queries = Query.builder diff --git a/templates/skills/swift.md.twig b/templates/skills/swift.md.twig index c99d6f1c8a..4efb4a0cfd 100644 --- a/templates/skills/swift.md.twig +++ b/templates/skills/swift.md.twig @@ -187,6 +187,10 @@ Query.distinct() // Joins (table id, left key, right key, operator, alias) Query.join("orders", left: "$id", right: "customerId") Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord") +Query.leftJoin("orders", alias: "ord", on: [ + Query.on("$id", "customerId"), + Query.equal("ord.status", value: "paid"), +]) // Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. let queries = Query.builder() diff --git a/templates/skills/typescript.md.twig b/templates/skills/typescript.md.twig index 4e6c8a2b79..b4c3aa6d1d 100644 --- a/templates/skills/typescript.md.twig +++ b/templates/skills/typescript.md.twig @@ -346,6 +346,10 @@ Query.distinct() // Joins (table id, left key, right key, operator, alias) Query.join('orders', '$id', 'customerId') Query.leftJoin('orders', '$id', 'customerId', '=', 'ord') +Query.leftJoin('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), +]) // Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. const queries = Query.builder() diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index f7d79f3604..6599a4148e 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -780,22 +780,61 @@ public struct Query : Codable, CustomStringConvertible { ).description } + public static func on(_ left: String, _ right: String, operator op: String = "=") -> String { + return Query( + method: "on", + values: [left, op, right] + ).description + } + public static func join(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { return joinQuery("join", table: table, left: left, right: right, operator: op, alias: alias) } + public static func join(_ table: String, on: [String]) -> String { + return nestedJoin("join", table: table, alias: "", on: on) + } + + public static func join(_ table: String, alias: String, on: [String]) -> String { + return nestedJoin("join", table: table, alias: alias, on: on) + } + public static func leftJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { return joinQuery("leftJoin", table: table, left: left, right: right, operator: op, alias: alias) } + public static func leftJoin(_ table: String, on: [String]) -> String { + return nestedJoin("leftJoin", table: table, alias: "", on: on) + } + + public static func leftJoin(_ table: String, alias: String, on: [String]) -> String { + return nestedJoin("leftJoin", table: table, alias: alias, on: on) + } + public static func rightJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { return joinQuery("rightJoin", table: table, left: left, right: right, operator: op, alias: alias) } + public static func rightJoin(_ table: String, on: [String]) -> String { + return nestedJoin("rightJoin", table: table, alias: "", on: on) + } + + public static func rightJoin(_ table: String, alias: String, on: [String]) -> String { + return nestedJoin("rightJoin", table: table, alias: alias, on: on) + } + public static func fullOuterJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> String { return joinQuery("fullOuterJoin", table: table, left: left, right: right, operator: op, alias: alias) } + public static func fullOuterJoin(_ table: String, on: [String]) -> String { + return nestedJoin("fullOuterJoin", table: table, alias: "", on: on) + } + + public static func fullOuterJoin(_ table: String, alias: String, on: [String]) -> String { + return nestedJoin("fullOuterJoin", table: table, alias: alias, on: on) + } + public static func crossJoin(_ table: String, alias: String = "") -> String { return Query( method: "crossJoin", @@ -892,6 +931,24 @@ public struct Query : Codable, CustomStringConvertible { ).description } + private static func nestedJoin(_ method: String, table: String, alias: String, on: [String]) -> String { + var values: [Any] = [] + if !alias.isEmpty { + values.append(alias) + } + for query in on { + if let data = query.data(using: .utf8), + let parsed = try? JSONDecoder().decode(Query.self, from: data) { + values.append(parsed) + } + } + return Query( + method: method, + attribute: table, + values: values + ).description + } + private static func decodeQueries(_ queries: [String]) -> [Query] { let decoder = JSONDecoder() return queries.compactMap { queryStr -> Query? in @@ -1295,21 +1352,61 @@ public class QueryBuilder { return push(Query.join(table, left: left, right: right, operator: op, alias: alias)) } + @discardableResult + public func join(_ table: String, on: [String]) -> QueryBuilder { + return push(Query.join(table, on: on)) + } + + @discardableResult + public func join(_ table: String, alias: String, on: [String]) -> QueryBuilder { + return push(Query.join(table, alias: alias, on: on)) + } + @discardableResult public func leftJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { return push(Query.leftJoin(table, left: left, right: right, operator: op, alias: alias)) } + @discardableResult + public func leftJoin(_ table: String, on: [String]) -> QueryBuilder { + return push(Query.leftJoin(table, on: on)) + } + + @discardableResult + public func leftJoin(_ table: String, alias: String, on: [String]) -> QueryBuilder { + return push(Query.leftJoin(table, alias: alias, on: on)) + } + @discardableResult public func rightJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { return push(Query.rightJoin(table, left: left, right: right, operator: op, alias: alias)) } + @discardableResult + public func rightJoin(_ table: String, on: [String]) -> QueryBuilder { + return push(Query.rightJoin(table, on: on)) + } + + @discardableResult + public func rightJoin(_ table: String, alias: String, on: [String]) -> QueryBuilder { + return push(Query.rightJoin(table, alias: alias, on: on)) + } + @discardableResult public func fullOuterJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { return push(Query.fullOuterJoin(table, left: left, right: right, operator: op, alias: alias)) } + @discardableResult + public func fullOuterJoin(_ table: String, on: [String]) -> QueryBuilder { + return push(Query.fullOuterJoin(table, on: on)) + } + + @discardableResult + public func fullOuterJoin(_ table: String, alias: String, on: [String]) -> QueryBuilder { + return push(Query.fullOuterJoin(table, alias: alias, on: on)) + } + @discardableResult public func crossJoin(_ table: String, alias: String = "") -> QueryBuilder { return push(Query.crossJoin(table, alias: alias)) diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index 104df3f0ec..6ac5754ebc 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -674,60 +674,54 @@ export class Query { static distinct = (): string => new Query("distinct").toString(); + /** + * Column-to-column join ON condition. + */ + static on = ( + left: string, + right: string, + operator: string = "=" + ): string => + new Query("on", undefined, [left, operator, right]).toString(); + /** * Inner join. `table` is the joined collection/table ID. */ static join = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "join", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("join", table, leftOrAliasOrOn, rightOrOn, operator, alias); static leftJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "leftJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("leftJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static rightJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "rightJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("rightJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static fullOuterJoin = ( table: string, - left: string, - right: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", operator: string = "=", alias: string = "" ): string => - new Query( - "fullOuterJoin", - table, - alias !== "" ? [left, operator, right, alias] : [left, operator, right] - ).toString(); + Query.createJoin("fullOuterJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); static crossJoin = (table: string, alias: string = ""): string => new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); @@ -758,6 +752,45 @@ export class Query { }; static builder = (): QueryBuilder => new QueryBuilder(); + + private static createJoin( + method: string, + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = "", + operator: string = "=", + alias: string = "" + ): string { + if (Array.isArray(leftOrAliasOrOn)) { + return Query.nestedJoin(method, table, "", leftOrAliasOrOn); + } + if (Array.isArray(rightOrOn)) { + return Query.nestedJoin(method, table, leftOrAliasOrOn, rightOrOn); + } + return new Query( + method, + table, + alias !== "" + ? [leftOrAliasOrOn, operator, rightOrOn, alias] + : [leftOrAliasOrOn, operator, rightOrOn] + ).toString(); + } + + private static nestedJoin( + method: string, + table: string, + alias: string, + on: string[] + ): string { + const values: any[] = []; + if (alias !== "") { + values.push(alias); + } + for (const query of on) { + values.push(JSONbig.parse(query)); + } + return new Query(method, table, values).toString(); + } } export type QueryInput = @@ -1057,20 +1090,20 @@ export class QueryBuilder { return this.push(Query.distinct()); } - join(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.join(table, left, right, operator, alias)); + join(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - leftJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.leftJoin(table, left, right, operator, alias)); + leftJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - rightJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.rightJoin(table, left, right, operator, alias)); + rightJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } - fullOuterJoin(table: string, left: string, right: string, operator: string = "=", alias: string = ""): this { - return this.push(Query.fullOuterJoin(table, left, right, operator, alias)); + fullOuterJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { + return this.push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); } crossJoin(table: string, alias: string = ""): this { From 4e3fef24d279662f71f8aeea2a9aedc23c9cfea2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 18:02:41 +1200 Subject: [PATCH 05/42] test: cover new query helpers in every SDK Add e2e prints and unit tests for aggregates, joins, ON, spatial, and page so each SDK exercises every new query type. --- src/SDK/Language/Android.php | 5 + src/SDK/Language/Kotlin.php | 5 + templates/dart/test/query_test.dart.twig | 72 +++++++++ templates/deno/test/query.test.ts.twig | 95 +++++++++++ templates/go/query_test.go.twig | 39 +++++ .../test/kotlin/io/appwrite/QueryTest.kt.twig | 152 ++++++++++++++++++ templates/node/test/query.test.js.twig | 95 +++++++++++ templates/php/tests/QueryTest.php.twig | 66 ++++++++ templates/python/test/test_query.py.twig | 75 +++++++++ templates/rust/src/query.rs.twig | 108 +++++++++++++ templates/rust/tests/tests.rs | 30 ++++ templates/swift/Tests/Tests.swift.twig | 119 +++++++++++++- tests/e2e/Base.php | 26 +++ tests/e2e/languages/android/Tests.kt | 30 ++++ tests/e2e/languages/apple/Tests.swift | 30 ++++ tests/e2e/languages/dart/tests.dart | 30 ++++ tests/e2e/languages/deno/tests.ts | 30 ++++ tests/e2e/languages/dotnet/Tests.cs | 30 ++++ tests/e2e/languages/flutter/tests.dart | 30 ++++ tests/e2e/languages/go-v2/tests.go | 30 ++++ tests/e2e/languages/go/tests.go | 30 ++++ tests/e2e/languages/kotlin/Tests.kt | 30 ++++ tests/e2e/languages/node/test.js | 30 ++++ tests/e2e/languages/php/test.php | 30 ++++ tests/e2e/languages/python/tests.py | 30 ++++ tests/e2e/languages/react-native/browser.js | 30 ++++ tests/e2e/languages/ruby/tests.rb | 30 ++++ tests/e2e/languages/swift/Tests.swift | 30 ++++ tests/e2e/languages/unity/Tests.cs | 30 ++++ tests/e2e/languages/web/index.html | 30 ++++ tests/e2e/languages/web/node.js | 30 ++++ 31 files changed, 1426 insertions(+), 1 deletion(-) create mode 100644 templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index 507e5878b0..1efd6a43ac 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -120,6 +120,11 @@ public function getFiles(): array 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Query.kt', 'template' => '/android/library/src/main/java/io/package/Query.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/library/src/test/java/{{ sdk.namespace | caseSlash }}/QueryTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Operator.kt', diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index 334127f17d..a9570caa0b 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -542,6 +542,11 @@ public function getFiles(): array 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Query.kt', 'template' => '/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig', ], + [ + 'scope' => 'default', + 'destination' => '/src/test/kotlin/{{ sdk.namespace | caseSlash }}/QueryTest.kt', + 'template' => '/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig', + ], [ 'scope' => 'default', 'destination' => '/src/main/kotlin/{{ sdk.namespace | caseSlash }}/Operator.kt', diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index 6f1a298cda..12e4289f91 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -386,6 +386,42 @@ void main() { expect(jsonDecode(Query.max('price', 'highest'))['method'], 'max'); }); + test('returns stddev', () { + expect(jsonDecode(Query.stddev('price', 'sd'))['method'], 'stddev'); + }); + + test('returns stddevPop', () { + expect(jsonDecode(Query.stddevPop('price', 'sdp'))['method'], 'stddevPop'); + }); + + test('returns stddevSamp', () { + expect(jsonDecode(Query.stddevSamp('price', 'sds'))['method'], 'stddevSamp'); + }); + + test('returns variance', () { + expect(jsonDecode(Query.variance('price', 'var'))['method'], 'variance'); + }); + + test('returns varPop', () { + expect(jsonDecode(Query.varPop('price', 'vp'))['method'], 'varPop'); + }); + + test('returns varSamp', () { + expect(jsonDecode(Query.varSamp('price', 'vs'))['method'], 'varSamp'); + }); + + test('returns bitAnd', () { + expect(jsonDecode(Query.bitAnd('flags', 'band'))['method'], 'bitAnd'); + }); + + test('returns bitOr', () { + expect(jsonDecode(Query.bitOr('flags', 'bor'))['method'], 'bitOr'); + }); + + test('returns bitXor', () { + expect(jsonDecode(Query.bitXor('flags', 'bxor'))['method'], 'bitXor'); + }); + test('returns groupBy', () { final query = jsonDecode(Query.groupBy(['status', 'year'])); expect(query['method'], 'groupBy'); @@ -424,6 +460,18 @@ void main() { expect(query['values'], ['\$id', '=', 'customerId', 'ord']); }); + test('returns rightJoin', () { + final query = jsonDecode(Query.rightJoin('orders', '\$id', 'customerId')); + expect(query['method'], 'rightJoin'); + expect(query['values'], ['\$id', '=', 'customerId']); + }); + + test('returns fullOuterJoin', () { + final query = jsonDecode(Query.fullOuterJoin('orders', '\$id', 'customerId')); + expect(query['method'], 'fullOuterJoin'); + expect(query['values'], ['\$id', '=', 'customerId']); + }); + test('returns on', () { final query = jsonDecode(Query.on('\$id', 'customerId')); expect(query['method'], 'on'); @@ -466,6 +514,30 @@ void main() { ]); }); + test('returns notCovers', () { + final query = jsonDecode(Query.notCovers('location', [1, 2])); + expect(query['method'], 'notCovers'); + expect(query['values'], [ + [1, 2] + ]); + }); + + test('returns spatialEquals', () { + final query = jsonDecode(Query.spatialEquals('location', [1, 2])); + expect(query['method'], 'spatialEquals'); + expect(query['values'], [ + [1, 2] + ]); + }); + + test('returns notSpatialEquals', () { + final query = jsonDecode(Query.notSpatialEquals('location', [1, 2])); + expect(query['method'], 'notSpatialEquals'); + expect(query['values'], [ + [1, 2] + ]); + }); + test('returns page', () { final queries = Query.page(2, 10); expect(queries.length, 2); diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index da6b073236..b5110b8c49 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -285,6 +285,76 @@ describe('Query', () => { '{"method":"count","attribute":"*"}', )); + test('countDistinct', () => assertEquals( + Query.countDistinct('year', 'uniqueYears').toString(), + '{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}', + )); + + test('sum', () => assertEquals( + Query.sum('price', 'total').toString(), + '{"method":"sum","attribute":"price","values":["total"]}', + )); + + test('avg', () => assertEquals( + Query.avg('price', 'avgPrice').toString(), + '{"method":"avg","attribute":"price","values":["avgPrice"]}', + )); + + test('min', () => assertEquals( + Query.min('price', 'lowest').toString(), + '{"method":"min","attribute":"price","values":["lowest"]}', + )); + + test('max', () => assertEquals( + Query.max('price', 'highest').toString(), + '{"method":"max","attribute":"price","values":["highest"]}', + )); + + test('stddev', () => assertEquals( + Query.stddev('price', 'sd').toString(), + '{"method":"stddev","attribute":"price","values":["sd"]}', + )); + + test('stddevPop', () => assertEquals( + Query.stddevPop('price', 'sdp').toString(), + '{"method":"stddevPop","attribute":"price","values":["sdp"]}', + )); + + test('stddevSamp', () => assertEquals( + Query.stddevSamp('price', 'sds').toString(), + '{"method":"stddevSamp","attribute":"price","values":["sds"]}', + )); + + test('variance', () => assertEquals( + Query.variance('price', 'var').toString(), + '{"method":"variance","attribute":"price","values":["var"]}', + )); + + test('varPop', () => assertEquals( + Query.varPop('price', 'vp').toString(), + '{"method":"varPop","attribute":"price","values":["vp"]}', + )); + + test('varSamp', () => assertEquals( + Query.varSamp('price', 'vs').toString(), + '{"method":"varSamp","attribute":"price","values":["vs"]}', + )); + + test('bitAnd', () => assertEquals( + Query.bitAnd('flags', 'band').toString(), + '{"method":"bitAnd","attribute":"flags","values":["band"]}', + )); + + test('bitOr', () => assertEquals( + Query.bitOr('flags', 'bor').toString(), + '{"method":"bitOr","attribute":"flags","values":["bor"]}', + )); + + test('bitXor', () => assertEquals( + Query.bitXor('flags', 'bxor').toString(), + '{"method":"bitXor","attribute":"flags","values":["bxor"]}', + )); + test('groupBy', () => assertEquals( Query.groupBy(['status', 'year']).toString(), '{"method":"groupBy","values":["status","year"]}', @@ -313,6 +383,16 @@ describe('Query', () => { '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', )); + test('rightJoin', () => assertEquals( + Query.rightJoin('orders', '$id', 'customerId').toString(), + '{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}', + )); + + test('fullOuterJoin', () => assertEquals( + Query.fullOuterJoin('orders', '$id', 'customerId').toString(), + '{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}', + )); + test('on', () => assertEquals( Query.on('$id', 'customerId').toString(), '{"method":"on","values":["$id","=","customerId"]}', @@ -340,6 +420,21 @@ describe('Query', () => { '{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}', )); + test('notCovers', () => assertEquals( + Query.notCovers('location', [1, 2]).toString(), + '{"method":"notCovers","attribute":"location","values":[[1,2]]}', + )); + + test('spatialEquals', () => assertEquals( + Query.spatialEquals('location', [1, 2]).toString(), + '{"method":"spatialEquals","attribute":"location","values":[[1,2]]}', + )); + + test('notSpatialEquals', () => assertEquals( + Query.notSpatialEquals('location', [1, 2]).toString(), + '{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}', + )); + test('page', () => { const queries = Query.page(2, 10); assertEquals(queries, [ diff --git a/templates/go/query_test.go.twig b/templates/go/query_test.go.twig index 5124b37753..294bf2d88f 100644 --- a/templates/go/query_test.go.twig +++ b/templates/go/query_test.go.twig @@ -182,6 +182,45 @@ func TestCountDistinct(t *testing.T) { } } +func TestNewQueryMethods(t *testing.T) { + cases := []struct { + name string + query string + method string + }{ + {"sum", Sum("price", "total"), "sum"}, + {"avg", Avg("price", "avgPrice"), "avg"}, + {"min", Min("price", "lowest"), "min"}, + {"max", Max("price", "highest"), "max"}, + {"stddev", Stddev("price", "sd"), "stddev"}, + {"stddevPop", StddevPop("price", "sdp"), "stddevPop"}, + {"stddevSamp", StddevSamp("price", "sds"), "stddevSamp"}, + {"variance", Variance("price", "var"), "variance"}, + {"varPop", VarPop("price", "vp"), "varPop"}, + {"varSamp", VarSamp("price", "vs"), "varSamp"}, + {"bitAnd", BitAnd("flags", "band"), "bitAnd"}, + {"bitOr", BitOr("flags", "bor"), "bitOr"}, + {"bitXor", BitXor("flags", "bxor"), "bitXor"}, + {"rightJoin", RightJoin("orders", "$id", "customerId", "=", ""), "rightJoin"}, + {"fullOuterJoin", FullOuterJoin("orders", "$id", "customerId", "=", ""), "fullOuterJoin"}, + {"notCovers", NotCovers("location", []interface{}{1, 2}), "notCovers"}, + {"spatialEquals", SpatialEquals("location", []interface{}{1, 2}), "spatialEquals"}, + {"notSpatialEquals", NotSpatialEquals("location", []interface{}{1, 2}), "notSpatialEquals"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var data map[string]interface{} + if err := json.Unmarshal([]byte(c.query), &data); err != nil { + t.Fatal(err) + } + if data["method"] != c.method { + t.Errorf("Expected %s, got %v", c.method, data["method"]) + } + }) + } +} + func TestGroupBy(t *testing.T) { q := GroupBy([]interface{}{"status", "year"}) var data map[string]interface{} diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig new file mode 100644 index 0000000000..36ba7a58d9 --- /dev/null +++ b/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig @@ -0,0 +1,152 @@ +package {{ sdk.namespace | caseDot }} + +import com.google.gson.JsonParser +import org.junit.Assert.assertEquals +import org.junit.Test + +class QueryTest { + private fun method(json: String): String = + JsonParser.parseString(json).asJsonObject.get("method").asString + + @Test + fun countDistinct() { + assertEquals("countDistinct", method(Query.countDistinct("year", "uniqueYears"))) + } + + @Test + fun sum() { + assertEquals("sum", method(Query.sum("price", "total"))) + } + + @Test + fun avg() { + assertEquals("avg", method(Query.avg("price", "avgPrice"))) + } + + @Test + fun min() { + assertEquals("min", method(Query.min("price", "lowest"))) + } + + @Test + fun max() { + assertEquals("max", method(Query.max("price", "highest"))) + } + + @Test + fun stddev() { + assertEquals("stddev", method(Query.stddev("price", "sd"))) + } + + @Test + fun stddevPop() { + assertEquals("stddevPop", method(Query.stddevPop("price", "sdp"))) + } + + @Test + fun stddevSamp() { + assertEquals("stddevSamp", method(Query.stddevSamp("price", "sds"))) + } + + @Test + fun variance() { + assertEquals("variance", method(Query.variance("price", "var"))) + } + + @Test + fun varPop() { + assertEquals("varPop", method(Query.varPop("price", "vp"))) + } + + @Test + fun varSamp() { + assertEquals("varSamp", method(Query.varSamp("price", "vs"))) + } + + @Test + fun bitAnd() { + assertEquals("bitAnd", method(Query.bitAnd("flags", "band"))) + } + + @Test + fun bitOr() { + assertEquals("bitOr", method(Query.bitOr("flags", "bor"))) + } + + @Test + fun bitXor() { + assertEquals("bitXor", method(Query.bitXor("flags", "bxor"))) + } + + @Test + fun having() { + assertEquals("having", method(Query.having(listOf(Query.greaterThan("total", 1))))) + } + + @Test + fun leftJoin() { + assertEquals("leftJoin", method(Query.leftJoin("orders", "\$id", "customerId", "=", "ord"))) + } + + @Test + fun rightJoin() { + assertEquals("rightJoin", method(Query.rightJoin("orders", "\$id", "customerId"))) + } + + @Test + fun fullOuterJoin() { + assertEquals("fullOuterJoin", method(Query.fullOuterJoin("orders", "\$id", "customerId"))) + } + + @Test + fun crossJoin() { + assertEquals("crossJoin", method(Query.crossJoin("orders", "ord"))) + } + + @Test + fun on() { + assertEquals("on", method(Query.on("\$id", "customerId"))) + } + + @Test + fun nestedLeftJoin() { + val json = JsonParser.parseString( + Query.leftJoin( + "orders", + "ord", + listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), + ) + ) + ).asJsonObject + assertEquals("leftJoin", json.get("method").asString) + val values = json.get("values").asJsonArray + assertEquals("ord", values[0].asString) + assertEquals("on", values[1].asJsonObject.get("method").asString) + assertEquals("equal", values[2].asJsonObject.get("method").asString) + } + + @Test + fun notCovers() { + assertEquals("notCovers", method(Query.notCovers("location", listOf(1, 2)))) + } + + @Test + fun spatialEquals() { + assertEquals("spatialEquals", method(Query.spatialEquals("location", listOf(1, 2)))) + } + + @Test + fun notSpatialEquals() { + assertEquals("notSpatialEquals", method(Query.notSpatialEquals("location", listOf(1, 2)))) + } + + @Test + fun page() { + val queries = Query.page(2, 10) + assertEquals(2, queries.size) + assertEquals("limit", method(queries[0])) + assertEquals("offset", method(queries[1])) + } +} diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index 3680a16fc7..ac8a8b0bec 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -163,6 +163,76 @@ describe('Query', () => { .toEqual('{"method":"count","attribute":"*"}') ); + test('countDistinct', () => + expect(Query.countDistinct('year', 'uniqueYears')) + .toEqual('{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}') + ); + + test('sum', () => + expect(Query.sum('price', 'total')) + .toEqual('{"method":"sum","attribute":"price","values":["total"]}') + ); + + test('avg', () => + expect(Query.avg('price', 'avgPrice')) + .toEqual('{"method":"avg","attribute":"price","values":["avgPrice"]}') + ); + + test('min', () => + expect(Query.min('price', 'lowest')) + .toEqual('{"method":"min","attribute":"price","values":["lowest"]}') + ); + + test('max', () => + expect(Query.max('price', 'highest')) + .toEqual('{"method":"max","attribute":"price","values":["highest"]}') + ); + + test('stddev', () => + expect(Query.stddev('price', 'sd')) + .toEqual('{"method":"stddev","attribute":"price","values":["sd"]}') + ); + + test('stddevPop', () => + expect(Query.stddevPop('price', 'sdp')) + .toEqual('{"method":"stddevPop","attribute":"price","values":["sdp"]}') + ); + + test('stddevSamp', () => + expect(Query.stddevSamp('price', 'sds')) + .toEqual('{"method":"stddevSamp","attribute":"price","values":["sds"]}') + ); + + test('variance', () => + expect(Query.variance('price', 'var')) + .toEqual('{"method":"variance","attribute":"price","values":["var"]}') + ); + + test('varPop', () => + expect(Query.varPop('price', 'vp')) + .toEqual('{"method":"varPop","attribute":"price","values":["vp"]}') + ); + + test('varSamp', () => + expect(Query.varSamp('price', 'vs')) + .toEqual('{"method":"varSamp","attribute":"price","values":["vs"]}') + ); + + test('bitAnd', () => + expect(Query.bitAnd('flags', 'band')) + .toEqual('{"method":"bitAnd","attribute":"flags","values":["band"]}') + ); + + test('bitOr', () => + expect(Query.bitOr('flags', 'bor')) + .toEqual('{"method":"bitOr","attribute":"flags","values":["bor"]}') + ); + + test('bitXor', () => + expect(Query.bitXor('flags', 'bxor')) + .toEqual('{"method":"bitXor","attribute":"flags","values":["bxor"]}') + ); + test('groupBy', () => expect(Query.groupBy(['status', 'year'])) .toEqual('{"method":"groupBy","values":["status","year"]}') @@ -191,6 +261,16 @@ describe('Query', () => { .toEqual('{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}') ); + test('rightJoin', () => + expect(Query.rightJoin('orders', '$id', 'customerId')) + .toEqual('{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}') + ); + + test('fullOuterJoin', () => + expect(Query.fullOuterJoin('orders', '$id', 'customerId')) + .toEqual('{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}') + ); + test('on', () => expect(Query.on('$id', 'customerId')) .toEqual('{"method":"on","values":["$id","=","customerId"]}') @@ -218,6 +298,21 @@ describe('Query', () => { .toEqual('{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}') ); + test('notCovers', () => + expect(Query.notCovers('location', [1, 2])) + .toEqual('{"method":"notCovers","attribute":"location","values":[[1,2]]}') + ); + + test('spatialEquals', () => + expect(Query.spatialEquals('location', [1, 2])) + .toEqual('{"method":"spatialEquals","attribute":"location","values":[[1,2]]}') + ); + + test('notSpatialEquals', () => + expect(Query.notSpatialEquals('location', [1, 2])) + .toEqual('{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}') + ); + test('page', () => { const queries = Query.page(2, 10); expect(queries).toEqual([ diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index 1d85857f72..327d093d91 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -352,6 +352,42 @@ final class QueryTest extends TestCase { $this->assertSame('max', json_decode(Query::max('price', 'highest'), true)['method']); } + public function testStddev(): void { + $this->assertSame('stddev', json_decode(Query::stddev('price', 'sd'), true)['method']); + } + + public function testStddevPop(): void { + $this->assertSame('stddevPop', json_decode(Query::stddevPop('price', 'sdp'), true)['method']); + } + + public function testStddevSamp(): void { + $this->assertSame('stddevSamp', json_decode(Query::stddevSamp('price', 'sds'), true)['method']); + } + + public function testVariance(): void { + $this->assertSame('variance', json_decode(Query::variance('price', 'var'), true)['method']); + } + + public function testVarPop(): void { + $this->assertSame('varPop', json_decode(Query::varPop('price', 'vp'), true)['method']); + } + + public function testVarSamp(): void { + $this->assertSame('varSamp', json_decode(Query::varSamp('price', 'vs'), true)['method']); + } + + public function testBitAnd(): void { + $this->assertSame('bitAnd', json_decode(Query::bitAnd('flags', 'band'), true)['method']); + } + + public function testBitOr(): void { + $this->assertSame('bitOr', json_decode(Query::bitOr('flags', 'bor'), true)['method']); + } + + public function testBitXor(): void { + $this->assertSame('bitXor', json_decode(Query::bitXor('flags', 'bxor'), true)['method']); + } + public function testGroupBy(): void { $query = json_decode(Query::groupBy(['status', 'year']), true); $this->assertSame('groupBy', $query['method']); @@ -408,6 +444,18 @@ final class QueryTest extends TestCase { $this->assertSame('ord.status', $query['values'][2]['attribute']); } + public function testRightJoin(): void { + $query = json_decode(Query::rightJoin('orders', '$id', 'customerId'), true); + $this->assertSame('rightJoin', $query['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + + public function testFullOuterJoin(): void { + $query = json_decode(Query::fullOuterJoin('orders', '$id', 'customerId'), true); + $this->assertSame('fullOuterJoin', $query['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + public function testCrossJoin(): void { $query = json_decode(Query::crossJoin('orders', 'ord'), true); $this->assertSame('crossJoin', $query['method']); @@ -415,6 +463,24 @@ final class QueryTest extends TestCase { $this->assertSame(['ord'], $query['values']); } + public function testNotCovers(): void { + $query = json_decode(Query::notCovers('location', [1, 2]), true); + $this->assertSame('notCovers', $query['method']); + $this->assertSame([[1, 2]], $query['values']); + } + + public function testSpatialEquals(): void { + $query = json_decode(Query::spatialEquals('location', [1, 2]), true); + $this->assertSame('spatialEquals', $query['method']); + $this->assertSame([[1, 2]], $query['values']); + } + + public function testNotSpatialEquals(): void { + $query = json_decode(Query::notSpatialEquals('location', [1, 2]), true); + $this->assertSame('notSpatialEquals', $query['method']); + $this->assertSame([[1, 2]], $query['values']); + } + public function testVectorDot(): void { $query = json_decode(Query::vectorDot('embedding', [0.1, 0.2]), true); $this->assertSame('vectorDot', $query['method']); diff --git a/templates/python/test/test_query.py.twig b/templates/python/test/test_query.py.twig index c29df9f529..0c72e4438a 100644 --- a/templates/python/test/test_query.py.twig +++ b/templates/python/test/test_query.py.twig @@ -110,6 +110,53 @@ class TestQueryMethods(unittest.TestCase): def test_count(self): self.assertEqual(Query.count('*', 'total'), '{"method":"count","attribute":"*","values":["total"]}') + def test_count_distinct(self): + self.assertEqual(Query.count_distinct('year', 'uniqueYears'), '{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}') + + def test_sum(self): + self.assertEqual(Query.sum('price', 'total'), '{"method":"sum","attribute":"price","values":["total"]}') + + def test_avg(self): + self.assertEqual(Query.avg('price', 'avgPrice'), '{"method":"avg","attribute":"price","values":["avgPrice"]}') + + def test_min(self): + self.assertEqual(Query.min('price', 'lowest'), '{"method":"min","attribute":"price","values":["lowest"]}') + + def test_max(self): + self.assertEqual(Query.max('price', 'highest'), '{"method":"max","attribute":"price","values":["highest"]}') + + def test_stddev(self): + self.assertEqual(Query.stddev('price', 'sd'), '{"method":"stddev","attribute":"price","values":["sd"]}') + + def test_stddev_pop(self): + self.assertEqual(Query.stddev_pop('price', 'sdp'), '{"method":"stddevPop","attribute":"price","values":["sdp"]}') + + def test_stddev_samp(self): + self.assertEqual(Query.stddev_samp('price', 'sds'), '{"method":"stddevSamp","attribute":"price","values":["sds"]}') + + def test_variance(self): + self.assertEqual(Query.variance('price', 'var'), '{"method":"variance","attribute":"price","values":["var"]}') + + def test_var_pop(self): + self.assertEqual(Query.var_pop('price', 'vp'), '{"method":"varPop","attribute":"price","values":["vp"]}') + + def test_var_samp(self): + self.assertEqual(Query.var_samp('price', 'vs'), '{"method":"varSamp","attribute":"price","values":["vs"]}') + + def test_bit_and(self): + self.assertEqual(Query.bit_and('flags', 'band'), '{"method":"bitAnd","attribute":"flags","values":["band"]}') + + def test_bit_or(self): + self.assertEqual(Query.bit_or('flags', 'bor'), '{"method":"bitOr","attribute":"flags","values":["bor"]}') + + def test_bit_xor(self): + self.assertEqual(Query.bit_xor('flags', 'bxor'), '{"method":"bitXor","attribute":"flags","values":["bxor"]}') + + def test_having(self): + query = json.loads(Query.having([Query.greater_than('total', 1)])) + self.assertEqual(query['method'], 'having') + self.assertEqual(query['values'][0]['method'], 'greaterThan') + def test_join(self): self.assertEqual(Query.join('orders', '$id', 'customerId'), '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}') @@ -136,6 +183,34 @@ class TestQueryMethods(unittest.TestCase): def test_covers(self): self.assertEqual(Query.covers('location', [1, 2]), '{"method":"covers","attribute":"location","values":[[1,2]]}') + def test_left_join(self): + self.assertEqual(Query.left_join('orders', '$id', 'customerId', '=', 'ord'), '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}') + + def test_right_join(self): + self.assertEqual(Query.right_join('orders', '$id', 'customerId'), '{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}') + + def test_full_outer_join(self): + self.assertEqual(Query.full_outer_join('orders', '$id', 'customerId'), '{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}') + + def test_cross_join(self): + self.assertEqual(Query.cross_join('orders', 'ord'), '{"method":"crossJoin","attribute":"orders","values":["ord"]}') + + def test_not_covers(self): + self.assertEqual(Query.not_covers('location', [1, 2]), '{"method":"notCovers","attribute":"location","values":[[1,2]]}') + + def test_spatial_equals(self): + self.assertEqual(Query.spatial_equals('location', [1, 2]), '{"method":"spatialEquals","attribute":"location","values":[[1,2]]}') + + def test_not_spatial_equals(self): + self.assertEqual(Query.not_spatial_equals('location', [1, 2]), '{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}') + + def test_page(self): + queries = Query.page(2, 10) + self.assertEqual(queries, [ + '{"method":"limit","values":[10]}', + '{"method":"offset","values":[10]}', + ]) + def test_builder_build(self): expected = [ Query.equal('status', 'published'), diff --git a/templates/rust/src/query.rs.twig b/templates/rust/src/query.rs.twig index 8145d7f8e5..90c961a74b 100644 --- a/templates/rust/src/query.rs.twig +++ b/templates/rust/src/query.rs.twig @@ -1670,6 +1670,79 @@ mod tests { assert!(value.get("values").is_none()); } + #[test] + fn test_count_distinct() { + let value = Query::count_distinct("year", "uniqueYears").to_value(); + assert_eq!(value["method"], "countDistinct"); + assert_eq!(value["attribute"], "year"); + assert_eq!(value["values"][0], "uniqueYears"); + } + + #[test] + fn test_sum() { + assert_eq!(Query::sum("price", "total").to_value()["method"], "sum"); + } + + #[test] + fn test_avg() { + assert_eq!(Query::avg("price", "avgPrice").to_value()["method"], "avg"); + } + + #[test] + fn test_min() { + assert_eq!(Query::min("price", "lowest").to_value()["method"], "min"); + } + + #[test] + fn test_max() { + assert_eq!(Query::max("price", "highest").to_value()["method"], "max"); + } + + #[test] + fn test_stddev() { + assert_eq!(Query::stddev("price", "sd").to_value()["method"], "stddev"); + } + + #[test] + fn test_stddev_pop() { + assert_eq!(Query::stddev_pop("price", "sdp").to_value()["method"], "stddevPop"); + } + + #[test] + fn test_stddev_samp() { + assert_eq!(Query::stddev_samp("price", "sds").to_value()["method"], "stddevSamp"); + } + + #[test] + fn test_variance() { + assert_eq!(Query::variance("price", "var").to_value()["method"], "variance"); + } + + #[test] + fn test_var_pop() { + assert_eq!(Query::var_pop("price", "vp").to_value()["method"], "varPop"); + } + + #[test] + fn test_var_samp() { + assert_eq!(Query::var_samp("price", "vs").to_value()["method"], "varSamp"); + } + + #[test] + fn test_bit_and() { + assert_eq!(Query::bit_and("flags", "band").to_value()["method"], "bitAnd"); + } + + #[test] + fn test_bit_or() { + assert_eq!(Query::bit_or("flags", "bor").to_value()["method"], "bitOr"); + } + + #[test] + fn test_bit_xor() { + assert_eq!(Query::bit_xor("flags", "bxor").to_value()["method"], "bitXor"); + } + #[test] fn test_group_by() { let query = Query::group_by(vec!["status", "year"]); @@ -1754,6 +1827,41 @@ mod tests { assert_eq!(value["values"][0], "ord"); } + #[test] + fn test_right_join() { + let value = Query::right_join("orders", "$id", "customerId", "=", "").to_value(); + assert_eq!(value["method"], "rightJoin"); + assert_eq!(value["values"][0], "$id"); + assert_eq!(value["values"][1], "="); + assert_eq!(value["values"][2], "customerId"); + } + + #[test] + fn test_full_outer_join() { + let value = Query::full_outer_join("orders", "$id", "customerId", "=", "").to_value(); + assert_eq!(value["method"], "fullOuterJoin"); + } + + #[test] + fn test_not_covers() { + let value = Query::not_covers("location", Value::Array(vec![Value::from(1), Value::from(2)])).to_value(); + assert_eq!(value["method"], "notCovers"); + assert_eq!(value["values"][0][0], 1); + assert_eq!(value["values"][0][1], 2); + } + + #[test] + fn test_spatial_equals() { + let value = Query::spatial_equals("location", Value::Array(vec![Value::from(1), Value::from(2)])).to_value(); + assert_eq!(value["method"], "spatialEquals"); + } + + #[test] + fn test_not_spatial_equals() { + let value = Query::not_spatial_equals("location", Value::Array(vec![Value::from(1), Value::from(2)])).to_value(); + assert_eq!(value["method"], "notSpatialEquals"); + } + #[test] fn test_page() { let queries = Query::page(2, 10); diff --git a/templates/rust/tests/tests.rs b/templates/rust/tests/tests.rs index 1614d52a15..c55264c196 100644 --- a/templates/rust/tests/tests.rs +++ b/templates/rust/tests/tests.rs @@ -298,6 +298,36 @@ fn test_queries() { println!("{}", Query::group_by(vec!["status"])); println!("{}", Query::distinct()); println!("{}", Query::covers("location", json!([1, 2]))); + println!("{}", Query::count_distinct("year", "uniqueYears")); + println!("{}", Query::sum("price", "total")); + println!("{}", Query::avg("price", "avgPrice")); + println!("{}", Query::min("price", "lowest")); + println!("{}", Query::max("price", "highest")); + println!("{}", Query::stddev("price", "sd")); + println!("{}", Query::stddev_pop("price", "sdp")); + println!("{}", Query::stddev_samp("price", "sds")); + println!("{}", Query::variance("price", "var")); + println!("{}", Query::var_pop("price", "vp")); + println!("{}", Query::var_samp("price", "vs")); + println!("{}", Query::bit_and("flags", "band")); + println!("{}", Query::bit_or("flags", "bor")); + println!("{}", Query::bit_xor("flags", "bxor")); + println!("{}", Query::having(vec![Query::greater_than("total", 1).to_string()])); + println!("{}", Query::left_join("orders", "$id", "customerId", "=", "ord")); + println!("{}", Query::right_join("orders", "$id", "customerId", "=", "")); + println!("{}", Query::full_outer_join("orders", "$id", "customerId", "=", "")); + println!("{}", Query::cross_join("orders", "ord")); + println!("{}", Query::on("$id", "customerId", "=")); + println!("{}", Query::left_join_on("orders", "ord", vec![ + Query::on("$id", "customerId", "=").to_string(), + Query::equal("ord.status", "paid").to_string(), + ])); + println!("{}", Query::not_covers("location", json!([1, 2]))); + println!("{}", Query::spatial_equals("location", json!([1, 2]))); + println!("{}", Query::not_spatial_equals("location", json!([1, 2]))); + let page_queries = Query::page(2, 10); + println!("{}", page_queries[0]); + println!("{}", page_queries[1]); println!("{}", Query::builder().limit(1).build()[0]); } diff --git a/templates/swift/Tests/Tests.swift.twig b/templates/swift/Tests/Tests.swift.twig index 80a577efac..1d93805754 100644 --- a/templates/swift/Tests/Tests.swift.twig +++ b/templates/swift/Tests/Tests.swift.twig @@ -1,5 +1,122 @@ +import Foundation import XCTest +import {{ spec.info.title | caseUcfirst }} class Tests: XCTestCase { + func method(_ raw: String) -> String { + let object = try! JSONSerialization.jsonObject(with: Data(raw.utf8)) as! [String: Any] + return object["method"] as! String + } -} \ No newline at end of file + func testCountDistinct() { + XCTAssertEqual(method(Query.countDistinct("year", alias: "uniqueYears")), "countDistinct") + } + + func testSum() { + XCTAssertEqual(method(Query.sum("price", alias: "total")), "sum") + } + + func testAvg() { + XCTAssertEqual(method(Query.avg("price", alias: "avgPrice")), "avg") + } + + func testMin() { + XCTAssertEqual(method(Query.min("price", alias: "lowest")), "min") + } + + func testMax() { + XCTAssertEqual(method(Query.max("price", alias: "highest")), "max") + } + + func testStddev() { + XCTAssertEqual(method(Query.stddev("price", alias: "sd")), "stddev") + } + + func testStddevPop() { + XCTAssertEqual(method(Query.stddevPop("price", alias: "sdp")), "stddevPop") + } + + func testStddevSamp() { + XCTAssertEqual(method(Query.stddevSamp("price", alias: "sds")), "stddevSamp") + } + + func testVariance() { + XCTAssertEqual(method(Query.variance("price", alias: "var")), "variance") + } + + func testVarPop() { + XCTAssertEqual(method(Query.varPop("price", alias: "vp")), "varPop") + } + + func testVarSamp() { + XCTAssertEqual(method(Query.varSamp("price", alias: "vs")), "varSamp") + } + + func testBitAnd() { + XCTAssertEqual(method(Query.bitAnd("flags", alias: "band")), "bitAnd") + } + + func testBitOr() { + XCTAssertEqual(method(Query.bitOr("flags", alias: "bor")), "bitOr") + } + + func testBitXor() { + XCTAssertEqual(method(Query.bitXor("flags", alias: "bxor")), "bitXor") + } + + func testHaving() { + XCTAssertEqual(method(Query.having([Query.greaterThan("total", value: 1)])), "having") + } + + func testLeftJoin() { + XCTAssertEqual(method(Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")), "leftJoin") + } + + func testRightJoin() { + XCTAssertEqual(method(Query.rightJoin("orders", left: "$id", right: "customerId")), "rightJoin") + } + + func testFullOuterJoin() { + XCTAssertEqual(method(Query.fullOuterJoin("orders", left: "$id", right: "customerId")), "fullOuterJoin") + } + + func testCrossJoin() { + XCTAssertEqual(method(Query.crossJoin("orders", alias: "ord")), "crossJoin") + } + + func testOn() { + XCTAssertEqual(method(Query.on("$id", "customerId")), "on") + } + + func testNestedLeftJoin() { + let raw = Query.leftJoin("orders", alias: "ord", on: [ + Query.on("$id", "customerId"), + Query.equal("ord.status", value: "paid"), + ]) + let object = try! JSONSerialization.jsonObject(with: Data(raw.utf8)) as! [String: Any] + XCTAssertEqual(object["method"] as? String, "leftJoin") + let values = object["values"] as! [Any] + XCTAssertEqual(values[0] as? String, "ord") + XCTAssertEqual((values[1] as! [String: Any])["method"] as? String, "on") + XCTAssertEqual((values[2] as! [String: Any])["method"] as? String, "equal") + } + + func testNotCovers() { + XCTAssertEqual(method(Query.notCovers("location", values: [1, 2])), "notCovers") + } + + func testSpatialEquals() { + XCTAssertEqual(method(Query.spatialEquals("location", values: [1, 2])), "spatialEquals") + } + + func testNotSpatialEquals() { + XCTAssertEqual(method(Query.notSpatialEquals("location", values: [1, 2])), "notSpatialEquals") + } + + func testPage() { + let queries = Query.page(2, perPage: 10) + XCTAssertEqual(queries.count, 2) + XCTAssertEqual(method(queries[0]), "limit") + XCTAssertEqual(method(queries[1]), "offset") + } +} diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 97330a47b9..8e1c0e2960 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -203,6 +203,32 @@ abstract class Base extends TestCase '{"method":"groupBy","values":["status"]}', '{"method":"distinct"}', '{"method":"covers","attribute":"location","values":[[1,2]]}', + '{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}', + '{"method":"sum","attribute":"price","values":["total"]}', + '{"method":"avg","attribute":"price","values":["avgPrice"]}', + '{"method":"min","attribute":"price","values":["lowest"]}', + '{"method":"max","attribute":"price","values":["highest"]}', + '{"method":"stddev","attribute":"price","values":["sd"]}', + '{"method":"stddevPop","attribute":"price","values":["sdp"]}', + '{"method":"stddevSamp","attribute":"price","values":["sds"]}', + '{"method":"variance","attribute":"price","values":["var"]}', + '{"method":"varPop","attribute":"price","values":["vp"]}', + '{"method":"varSamp","attribute":"price","values":["vs"]}', + '{"method":"bitAnd","attribute":"flags","values":["band"]}', + '{"method":"bitOr","attribute":"flags","values":["bor"]}', + '{"method":"bitXor","attribute":"flags","values":["bxor"]}', + '{"method":"having","values":[{"method":"greaterThan","attribute":"total","values":[1]}]}', + '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', + '{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}', + '{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}', + '{"method":"crossJoin","attribute":"orders","values":["ord"]}', + '{"method":"on","values":["$id","=","customerId"]}', + '{"method":"leftJoin","attribute":"orders","values":["ord",{"method":"on","values":["$id","=","customerId"]},{"method":"equal","attribute":"ord.status","values":["paid"]}]}', + '{"method":"notCovers","attribute":"location","values":[[1,2]]}', + '{"method":"spatialEquals","attribute":"location","values":[[1,2]]}', + '{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}', + '{"method":"limit","values":[10]}', + '{"method":"offset","values":[10]}', '{"method":"limit","values":[1]}', ]; diff --git a/tests/e2e/languages/android/Tests.kt b/tests/e2e/languages/android/Tests.kt index dbc9d91e97..df46f5cfdb 100644 --- a/tests/e2e/languages/android/Tests.kt +++ b/tests/e2e/languages/android/Tests.kt @@ -393,6 +393,36 @@ class ServiceTest { writeToFile(Query.groupBy(listOf("status"))) writeToFile(Query.distinct()) writeToFile(Query.covers("location", listOf(1, 2))) + writeToFile(Query.countDistinct("year", "uniqueYears")) + writeToFile(Query.sum("price", "total")) + writeToFile(Query.avg("price", "avgPrice")) + writeToFile(Query.min("price", "lowest")) + writeToFile(Query.max("price", "highest")) + writeToFile(Query.stddev("price", "sd")) + writeToFile(Query.stddevPop("price", "sdp")) + writeToFile(Query.stddevSamp("price", "sds")) + writeToFile(Query.variance("price", "var")) + writeToFile(Query.varPop("price", "vp")) + writeToFile(Query.varSamp("price", "vs")) + writeToFile(Query.bitAnd("flags", "band")) + writeToFile(Query.bitOr("flags", "bor")) + writeToFile(Query.bitXor("flags", "bxor")) + writeToFile(Query.having(listOf(Query.greaterThan("total", 1)))) + writeToFile(Query.leftJoin("orders", "\$id", "customerId", "=", "ord")) + writeToFile(Query.rightJoin("orders", "\$id", "customerId")) + writeToFile(Query.fullOuterJoin("orders", "\$id", "customerId")) + writeToFile(Query.crossJoin("orders", "ord")) + writeToFile(Query.on("\$id", "customerId")) + writeToFile(Query.leftJoin("orders", "ord", listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), + ))) + writeToFile(Query.notCovers("location", listOf(1, 2))) + writeToFile(Query.spatialEquals("location", listOf(1, 2))) + writeToFile(Query.notSpatialEquals("location", listOf(1, 2))) + val pageQueries = Query.page(2, 10) + writeToFile(pageQueries[0]) + writeToFile(pageQueries[1]) writeToFile(Query.builder().limit(1).build()[0]) // Permission & Roles helper tests diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index b72793e775..b7a1d90998 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -362,6 +362,36 @@ class Tests: XCTestCase { print(Query.groupBy(["status"])) print(Query.distinct()) print(Query.covers("location", values: [1, 2])) + print(Query.countDistinct("year", alias: "uniqueYears")) + print(Query.sum("price", alias: "total")) + print(Query.avg("price", alias: "avgPrice")) + print(Query.min("price", alias: "lowest")) + print(Query.max("price", alias: "highest")) + print(Query.stddev("price", alias: "sd")) + print(Query.stddevPop("price", alias: "sdp")) + print(Query.stddevSamp("price", alias: "sds")) + print(Query.variance("price", alias: "var")) + print(Query.varPop("price", alias: "vp")) + print(Query.varSamp("price", alias: "vs")) + print(Query.bitAnd("flags", alias: "band")) + print(Query.bitOr("flags", alias: "bor")) + print(Query.bitXor("flags", alias: "bxor")) + print(Query.having([Query.greaterThan("total", value: 1)])) + print(Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")) + print(Query.rightJoin("orders", left: "$id", right: "customerId")) + print(Query.fullOuterJoin("orders", left: "$id", right: "customerId")) + print(Query.crossJoin("orders", alias: "ord")) + print(Query.on("$id", "customerId")) + print(Query.leftJoin("orders", alias: "ord", on: [ + Query.on("$id", "customerId"), + Query.equal("ord.status", value: "paid"), + ])) + print(Query.notCovers("location", values: [1, 2])) + print(Query.spatialEquals("location", values: [1, 2])) + print(Query.notSpatialEquals("location", values: [1, 2])) + let pageQueries = Query.page(2, perPage: 10) + print(pageQueries[0]) + print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) // Permission & Role helper tests diff --git a/tests/e2e/languages/dart/tests.dart b/tests/e2e/languages/dart/tests.dart index 9a65d5839d..000230a78f 100644 --- a/tests/e2e/languages/dart/tests.dart +++ b/tests/e2e/languages/dart/tests.dart @@ -230,6 +230,36 @@ void main() async { print(Query.groupBy(['status'])); print(Query.distinct()); print(Query.covers('location', [1, 2])); + print(Query.countDistinct('year', 'uniqueYears')); + print(Query.sum('price', 'total')); + print(Query.avg('price', 'avgPrice')); + print(Query.min('price', 'lowest')); + print(Query.max('price', 'highest')); + print(Query.stddev('price', 'sd')); + print(Query.stddevPop('price', 'sdp')); + print(Query.stddevSamp('price', 'sds')); + print(Query.variance('price', 'var')); + print(Query.varPop('price', 'vp')); + print(Query.varSamp('price', 'vs')); + print(Query.bitAnd('flags', 'band')); + print(Query.bitOr('flags', 'bor')); + print(Query.bitXor('flags', 'bxor')); + print(Query.having([Query.greaterThan('total', 1)])); + print(Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord')); + print(Query.rightJoin('orders', '\$id', 'customerId')); + print(Query.fullOuterJoin('orders', '\$id', 'customerId')); + print(Query.crossJoin('orders', 'ord')); + print(Query.on('\$id', 'customerId')); + print(Query.leftJoin('orders', 'ord', [ + Query.on('\$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ])); + print(Query.notCovers('location', [1, 2])); + print(Query.spatialEquals('location', [1, 2])); + print(Query.notSpatialEquals('location', [1, 2])); + final pageQueries = Query.page(2, 10); + print(pageQueries[0]); + print(pageQueries[1]); print(Query.builder().limit(1).build()[0]); // Permission & Role helper tests diff --git a/tests/e2e/languages/deno/tests.ts b/tests/e2e/languages/deno/tests.ts index ab932a43c5..3d39789560 100644 --- a/tests/e2e/languages/deno/tests.ts +++ b/tests/e2e/languages/deno/tests.ts @@ -261,6 +261,36 @@ async function start() { console.log(Query.groupBy(["status"])); console.log(Query.distinct()); console.log(Query.covers("location", [1, 2])); + console.log(Query.countDistinct("year", "uniqueYears")); + console.log(Query.sum("price", "total")); + console.log(Query.avg("price", "avgPrice")); + console.log(Query.min("price", "lowest")); + console.log(Query.max("price", "highest")); + console.log(Query.stddev("price", "sd")); + console.log(Query.stddevPop("price", "sdp")); + console.log(Query.stddevSamp("price", "sds")); + console.log(Query.variance("price", "var")); + console.log(Query.varPop("price", "vp")); + console.log(Query.varSamp("price", "vs")); + console.log(Query.bitAnd("flags", "band")); + console.log(Query.bitOr("flags", "bor")); + console.log(Query.bitXor("flags", "bxor")); + console.log(Query.having([Query.greaterThan("total", 1)])); + console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); + console.log(Query.rightJoin("orders", "$id", "customerId")); + console.log(Query.fullOuterJoin("orders", "$id", "customerId")); + console.log(Query.crossJoin("orders", "ord")); + console.log(Query.on("$id", "customerId")); + console.log(Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ])); + console.log(Query.notCovers("location", [1, 2])); + console.log(Query.spatialEquals("location", [1, 2])); + console.log(Query.notSpatialEquals("location", [1, 2])); + const pageQueries = Query.page(2, 10); + console.log(pageQueries[0]); + console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); // Permission & Role helper tests diff --git a/tests/e2e/languages/dotnet/Tests.cs b/tests/e2e/languages/dotnet/Tests.cs index f8b7e8e2c0..f8288cc9b1 100644 --- a/tests/e2e/languages/dotnet/Tests.cs +++ b/tests/e2e/languages/dotnet/Tests.cs @@ -242,6 +242,36 @@ public async Task Test1() TestContext.WriteLine(Query.GroupBy(new List { "status" })); TestContext.WriteLine(Query.Distinct()); TestContext.WriteLine(Query.Covers("location", new List { 1, 2 })); + TestContext.WriteLine(Query.CountDistinct("year", "uniqueYears")); + TestContext.WriteLine(Query.Sum("price", "total")); + TestContext.WriteLine(Query.Avg("price", "avgPrice")); + TestContext.WriteLine(Query.Min("price", "lowest")); + TestContext.WriteLine(Query.Max("price", "highest")); + TestContext.WriteLine(Query.Stddev("price", "sd")); + TestContext.WriteLine(Query.StddevPop("price", "sdp")); + TestContext.WriteLine(Query.StddevSamp("price", "sds")); + TestContext.WriteLine(Query.Variance("price", "var")); + TestContext.WriteLine(Query.VarPop("price", "vp")); + TestContext.WriteLine(Query.VarSamp("price", "vs")); + TestContext.WriteLine(Query.BitAnd("flags", "band")); + TestContext.WriteLine(Query.BitOr("flags", "bor")); + TestContext.WriteLine(Query.BitXor("flags", "bxor")); + TestContext.WriteLine(Query.Having(new List { Query.GreaterThan("total", 1) })); + TestContext.WriteLine(Query.LeftJoin("orders", "$id", "customerId", "=", "ord")); + TestContext.WriteLine(Query.RightJoin("orders", "$id", "customerId")); + TestContext.WriteLine(Query.FullOuterJoin("orders", "$id", "customerId")); + TestContext.WriteLine(Query.CrossJoin("orders", "ord")); + TestContext.WriteLine(Query.On("$id", "customerId")); + TestContext.WriteLine(Query.LeftJoin("orders", "ord", new List { + Query.On("$id", "customerId"), + Query.Equal("ord.status", "paid"), + })); + TestContext.WriteLine(Query.NotCovers("location", new List { 1, 2 })); + TestContext.WriteLine(Query.SpatialEquals("location", new List { 1, 2 })); + TestContext.WriteLine(Query.NotSpatialEquals("location", new List { 1, 2 })); + var pageQueries = Query.Page(2, 10); + TestContext.WriteLine(pageQueries[0]); + TestContext.WriteLine(pageQueries[1]); TestContext.WriteLine(Query.Builder().Limit(1).Build()[0]); // Permission & Roles helper tests diff --git a/tests/e2e/languages/flutter/tests.dart b/tests/e2e/languages/flutter/tests.dart index 5a5465535a..6912e9fe03 100644 --- a/tests/e2e/languages/flutter/tests.dart +++ b/tests/e2e/languages/flutter/tests.dart @@ -359,6 +359,36 @@ void main() async { print(Query.groupBy(['status'])); print(Query.distinct()); print(Query.covers('location', [1, 2])); + print(Query.countDistinct('year', 'uniqueYears')); + print(Query.sum('price', 'total')); + print(Query.avg('price', 'avgPrice')); + print(Query.min('price', 'lowest')); + print(Query.max('price', 'highest')); + print(Query.stddev('price', 'sd')); + print(Query.stddevPop('price', 'sdp')); + print(Query.stddevSamp('price', 'sds')); + print(Query.variance('price', 'var')); + print(Query.varPop('price', 'vp')); + print(Query.varSamp('price', 'vs')); + print(Query.bitAnd('flags', 'band')); + print(Query.bitOr('flags', 'bor')); + print(Query.bitXor('flags', 'bxor')); + print(Query.having([Query.greaterThan('total', 1)])); + print(Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord')); + print(Query.rightJoin('orders', '\$id', 'customerId')); + print(Query.fullOuterJoin('orders', '\$id', 'customerId')); + print(Query.crossJoin('orders', 'ord')); + print(Query.on('\$id', 'customerId')); + print(Query.leftJoin('orders', 'ord', [ + Query.on('\$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ])); + print(Query.notCovers('location', [1, 2])); + print(Query.spatialEquals('location', [1, 2])); + print(Query.notSpatialEquals('location', [1, 2])); + final pageQueries = Query.page(2, 10); + print(pageQueries[0]); + print(pageQueries[1]); print(Query.builder().limit(1).build()[0]); // Permission & Role helper tests diff --git a/tests/e2e/languages/go-v2/tests.go b/tests/e2e/languages/go-v2/tests.go index afe0be5abe..1e2b19ee34 100644 --- a/tests/e2e/languages/go-v2/tests.go +++ b/tests/e2e/languages/go-v2/tests.go @@ -295,6 +295,36 @@ func testQueries() { fmt.Println(query.GroupBy([]interface{}{"status"})) fmt.Println(query.Distinct()) fmt.Println(query.Covers("location", []interface{}{1, 2})) + fmt.Println(query.CountDistinct("year", "uniqueYears")) + fmt.Println(query.Sum("price", "total")) + fmt.Println(query.Avg("price", "avgPrice")) + fmt.Println(query.Min("price", "lowest")) + fmt.Println(query.Max("price", "highest")) + fmt.Println(query.Stddev("price", "sd")) + fmt.Println(query.StddevPop("price", "sdp")) + fmt.Println(query.StddevSamp("price", "sds")) + fmt.Println(query.Variance("price", "var")) + fmt.Println(query.VarPop("price", "vp")) + fmt.Println(query.VarSamp("price", "vs")) + fmt.Println(query.BitAnd("flags", "band")) + fmt.Println(query.BitOr("flags", "bor")) + fmt.Println(query.BitXor("flags", "bxor")) + fmt.Println(query.Having([]string{query.GreaterThan("total", 1)})) + fmt.Println(query.LeftJoin("orders", "$id", "customerId", "=", "ord")) + fmt.Println(query.RightJoin("orders", "$id", "customerId", "=", "")) + fmt.Println(query.FullOuterJoin("orders", "$id", "customerId", "=", "")) + fmt.Println(query.CrossJoin("orders", "ord")) + fmt.Println(query.On("$id", "customerId", "")) + fmt.Println(query.LeftJoinOn("orders", "ord", []string{ + query.On("$id", "customerId", ""), + query.Equal("ord.status", "paid"), + })) + fmt.Println(query.NotCovers("location", []interface{}{1, 2})) + fmt.Println(query.SpatialEquals("location", []interface{}{1, 2})) + fmt.Println(query.NotSpatialEquals("location", []interface{}{1, 2})) + pageQueries := query.Page(2, 10) + fmt.Println(pageQueries[0]) + fmt.Println(pageQueries[1]) fmt.Println(query.Builder().Limit(1).Build()[0]) } diff --git a/tests/e2e/languages/go/tests.go b/tests/e2e/languages/go/tests.go index e2b28f86ae..86deec7996 100644 --- a/tests/e2e/languages/go/tests.go +++ b/tests/e2e/languages/go/tests.go @@ -295,6 +295,36 @@ func testQueries() { fmt.Println(query.GroupBy([]interface{}{"status"})) fmt.Println(query.Distinct()) fmt.Println(query.Covers("location", []interface{}{1, 2})) + fmt.Println(query.CountDistinct("year", "uniqueYears")) + fmt.Println(query.Sum("price", "total")) + fmt.Println(query.Avg("price", "avgPrice")) + fmt.Println(query.Min("price", "lowest")) + fmt.Println(query.Max("price", "highest")) + fmt.Println(query.Stddev("price", "sd")) + fmt.Println(query.StddevPop("price", "sdp")) + fmt.Println(query.StddevSamp("price", "sds")) + fmt.Println(query.Variance("price", "var")) + fmt.Println(query.VarPop("price", "vp")) + fmt.Println(query.VarSamp("price", "vs")) + fmt.Println(query.BitAnd("flags", "band")) + fmt.Println(query.BitOr("flags", "bor")) + fmt.Println(query.BitXor("flags", "bxor")) + fmt.Println(query.Having([]string{query.GreaterThan("total", 1)})) + fmt.Println(query.LeftJoin("orders", "$id", "customerId", "=", "ord")) + fmt.Println(query.RightJoin("orders", "$id", "customerId", "=", "")) + fmt.Println(query.FullOuterJoin("orders", "$id", "customerId", "=", "")) + fmt.Println(query.CrossJoin("orders", "ord")) + fmt.Println(query.On("$id", "customerId", "")) + fmt.Println(query.LeftJoinOn("orders", "ord", []string{ + query.On("$id", "customerId", ""), + query.Equal("ord.status", "paid"), + })) + fmt.Println(query.NotCovers("location", []interface{}{1, 2})) + fmt.Println(query.SpatialEquals("location", []interface{}{1, 2})) + fmt.Println(query.NotSpatialEquals("location", []interface{}{1, 2})) + pageQueries := query.Page(2, 10) + fmt.Println(pageQueries[0]) + fmt.Println(pageQueries[1]) fmt.Println(query.Builder().Limit(1).Build()[0]) } diff --git a/tests/e2e/languages/kotlin/Tests.kt b/tests/e2e/languages/kotlin/Tests.kt index a50a4aca5b..dabb3f0a2c 100644 --- a/tests/e2e/languages/kotlin/Tests.kt +++ b/tests/e2e/languages/kotlin/Tests.kt @@ -261,6 +261,36 @@ class ServiceTest { writeToFile(Query.groupBy(listOf("status"))) writeToFile(Query.distinct()) writeToFile(Query.covers("location", listOf(1, 2))) + writeToFile(Query.countDistinct("year", "uniqueYears")) + writeToFile(Query.sum("price", "total")) + writeToFile(Query.avg("price", "avgPrice")) + writeToFile(Query.min("price", "lowest")) + writeToFile(Query.max("price", "highest")) + writeToFile(Query.stddev("price", "sd")) + writeToFile(Query.stddevPop("price", "sdp")) + writeToFile(Query.stddevSamp("price", "sds")) + writeToFile(Query.variance("price", "var")) + writeToFile(Query.varPop("price", "vp")) + writeToFile(Query.varSamp("price", "vs")) + writeToFile(Query.bitAnd("flags", "band")) + writeToFile(Query.bitOr("flags", "bor")) + writeToFile(Query.bitXor("flags", "bxor")) + writeToFile(Query.having(listOf(Query.greaterThan("total", 1)))) + writeToFile(Query.leftJoin("orders", "\$id", "customerId", "=", "ord")) + writeToFile(Query.rightJoin("orders", "\$id", "customerId")) + writeToFile(Query.fullOuterJoin("orders", "\$id", "customerId")) + writeToFile(Query.crossJoin("orders", "ord")) + writeToFile(Query.on("\$id", "customerId")) + writeToFile(Query.leftJoin("orders", "ord", listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), + ))) + writeToFile(Query.notCovers("location", listOf(1, 2))) + writeToFile(Query.spatialEquals("location", listOf(1, 2))) + writeToFile(Query.notSpatialEquals("location", listOf(1, 2))) + val pageQueries = Query.page(2, 10) + writeToFile(pageQueries[0]) + writeToFile(pageQueries[1]) writeToFile(Query.builder().limit(1).build()[0]) // Permission & Roles helper tests diff --git a/tests/e2e/languages/node/test.js b/tests/e2e/languages/node/test.js index e76fa5d35c..65da812031 100644 --- a/tests/e2e/languages/node/test.js +++ b/tests/e2e/languages/node/test.js @@ -346,6 +346,36 @@ async function start() { console.log(Query.groupBy(["status"])); console.log(Query.distinct()); console.log(Query.covers("location", [1, 2])); + console.log(Query.countDistinct("year", "uniqueYears")); + console.log(Query.sum("price", "total")); + console.log(Query.avg("price", "avgPrice")); + console.log(Query.min("price", "lowest")); + console.log(Query.max("price", "highest")); + console.log(Query.stddev("price", "sd")); + console.log(Query.stddevPop("price", "sdp")); + console.log(Query.stddevSamp("price", "sds")); + console.log(Query.variance("price", "var")); + console.log(Query.varPop("price", "vp")); + console.log(Query.varSamp("price", "vs")); + console.log(Query.bitAnd("flags", "band")); + console.log(Query.bitOr("flags", "bor")); + console.log(Query.bitXor("flags", "bxor")); + console.log(Query.having([Query.greaterThan("total", 1)])); + console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); + console.log(Query.rightJoin("orders", "$id", "customerId")); + console.log(Query.fullOuterJoin("orders", "$id", "customerId")); + console.log(Query.crossJoin("orders", "ord")); + console.log(Query.on("$id", "customerId")); + console.log(Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ])); + console.log(Query.notCovers("location", [1, 2])); + console.log(Query.spatialEquals("location", [1, 2])); + console.log(Query.notSpatialEquals("location", [1, 2])); + const pageQueries = Query.page(2, 10); + console.log(pageQueries[0]); + console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); // Permission & Role helper tests diff --git a/tests/e2e/languages/php/test.php b/tests/e2e/languages/php/test.php index 85d3dc0047..d25adcf738 100644 --- a/tests/e2e/languages/php/test.php +++ b/tests/e2e/languages/php/test.php @@ -353,6 +353,36 @@ public function toArray(): array echo Query::groupBy(['status']) . "\n"; echo Query::distinct() . "\n"; echo Query::covers('location', [1, 2]) . "\n"; +echo Query::countDistinct('year', 'uniqueYears') . "\n"; +echo Query::sum('price', 'total') . "\n"; +echo Query::avg('price', 'avgPrice') . "\n"; +echo Query::min('price', 'lowest') . "\n"; +echo Query::max('price', 'highest') . "\n"; +echo Query::stddev('price', 'sd') . "\n"; +echo Query::stddevPop('price', 'sdp') . "\n"; +echo Query::stddevSamp('price', 'sds') . "\n"; +echo Query::variance('price', 'var') . "\n"; +echo Query::varPop('price', 'vp') . "\n"; +echo Query::varSamp('price', 'vs') . "\n"; +echo Query::bitAnd('flags', 'band') . "\n"; +echo Query::bitOr('flags', 'bor') . "\n"; +echo Query::bitXor('flags', 'bxor') . "\n"; +echo Query::having([Query::greaterThan('total', 1)]) . "\n"; +echo Query::leftJoin('orders', '$id', 'customerId', '=', 'ord') . "\n"; +echo Query::rightJoin('orders', '$id', 'customerId') . "\n"; +echo Query::fullOuterJoin('orders', '$id', 'customerId') . "\n"; +echo Query::crossJoin('orders', 'ord') . "\n"; +echo Query::on('$id', 'customerId') . "\n"; +echo Query::leftJoin('orders', 'ord', [ + Query::on('$id', 'customerId'), + Query::equal('ord.status', 'paid'), +]) . "\n"; +echo Query::notCovers('location', [1, 2]) . "\n"; +echo Query::spatialEquals('location', [1, 2]) . "\n"; +echo Query::notSpatialEquals('location', [1, 2]) . "\n"; +$page = Query::page(2, 10); +echo $page[0] . "\n"; +echo $page[1] . "\n"; echo Query::builder()->limit(1)->build()[0] . "\n"; // Permission & Role helper tests diff --git a/tests/e2e/languages/python/tests.py b/tests/e2e/languages/python/tests.py index 399bd20376..54018ea86c 100644 --- a/tests/e2e/languages/python/tests.py +++ b/tests/e2e/languages/python/tests.py @@ -216,6 +216,36 @@ print(Query.group_by(["status"])) print(Query.distinct()) print(Query.covers("location", [1, 2])) +print(Query.count_distinct("year", "uniqueYears")) +print(Query.sum("price", "total")) +print(Query.avg("price", "avgPrice")) +print(Query.min("price", "lowest")) +print(Query.max("price", "highest")) +print(Query.stddev("price", "sd")) +print(Query.stddev_pop("price", "sdp")) +print(Query.stddev_samp("price", "sds")) +print(Query.variance("price", "var")) +print(Query.var_pop("price", "vp")) +print(Query.var_samp("price", "vs")) +print(Query.bit_and("flags", "band")) +print(Query.bit_or("flags", "bor")) +print(Query.bit_xor("flags", "bxor")) +print(Query.having([Query.greater_than("total", 1)])) +print(Query.left_join("orders", "$id", "customerId", "=", "ord")) +print(Query.right_join("orders", "$id", "customerId")) +print(Query.full_outer_join("orders", "$id", "customerId")) +print(Query.cross_join("orders", "ord")) +print(Query.on("$id", "customerId")) +print(Query.left_join("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), +])) +print(Query.not_covers("location", [1, 2])) +print(Query.spatial_equals("location", [1, 2])) +print(Query.not_spatial_equals("location", [1, 2])) +page_queries = Query.page(2, 10) +print(page_queries[0]) +print(page_queries[1]) print(Query.builder().limit(1).build()[0]) # Permission & Role helper tests diff --git a/tests/e2e/languages/react-native/browser.js b/tests/e2e/languages/react-native/browser.js index dbcfa2d2e2..f6bbeb864e 100644 --- a/tests/e2e/languages/react-native/browser.js +++ b/tests/e2e/languages/react-native/browser.js @@ -295,6 +295,36 @@ import { console.log(Query.groupBy(['status'])); console.log(Query.distinct()); console.log(Query.covers('location', [1, 2])); + console.log(Query.countDistinct('year', 'uniqueYears')); + console.log(Query.sum('price', 'total')); + console.log(Query.avg('price', 'avgPrice')); + console.log(Query.min('price', 'lowest')); + console.log(Query.max('price', 'highest')); + console.log(Query.stddev('price', 'sd')); + console.log(Query.stddevPop('price', 'sdp')); + console.log(Query.stddevSamp('price', 'sds')); + console.log(Query.variance('price', 'var')); + console.log(Query.varPop('price', 'vp')); + console.log(Query.varSamp('price', 'vs')); + console.log(Query.bitAnd('flags', 'band')); + console.log(Query.bitOr('flags', 'bor')); + console.log(Query.bitXor('flags', 'bxor')); + console.log(Query.having([Query.greaterThan('total', 1)])); + console.log(Query.leftJoin('orders', '$id', 'customerId', '=', 'ord')); + console.log(Query.rightJoin('orders', '$id', 'customerId')); + console.log(Query.fullOuterJoin('orders', '$id', 'customerId')); + console.log(Query.crossJoin('orders', 'ord')); + console.log(Query.on('$id', 'customerId')); + console.log(Query.leftJoin('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ])); + console.log(Query.notCovers('location', [1, 2])); + console.log(Query.spatialEquals('location', [1, 2])); + console.log(Query.notSpatialEquals('location', [1, 2])); + const pageQueries = Query.page(2, 10); + console.log(pageQueries[0]); + console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); // Permission & Role helper tests diff --git a/tests/e2e/languages/ruby/tests.rb b/tests/e2e/languages/ruby/tests.rb index c1c2f90921..bfe256b499 100644 --- a/tests/e2e/languages/ruby/tests.rb +++ b/tests/e2e/languages/ruby/tests.rb @@ -222,6 +222,36 @@ puts Query.group_by(['status']) puts Query.distinct() puts Query.covers('location', [1, 2]) +puts Query.count_distinct('year', 'uniqueYears') +puts Query.sum('price', 'total') +puts Query.avg('price', 'avgPrice') +puts Query.min('price', 'lowest') +puts Query.max('price', 'highest') +puts Query.stddev('price', 'sd') +puts Query.stddev_pop('price', 'sdp') +puts Query.stddev_samp('price', 'sds') +puts Query.variance('price', 'var') +puts Query.var_pop('price', 'vp') +puts Query.var_samp('price', 'vs') +puts Query.bit_and('flags', 'band') +puts Query.bit_or('flags', 'bor') +puts Query.bit_xor('flags', 'bxor') +puts Query.having([Query.greater_than('total', 1)]) +puts Query.left_join('orders', '$id', 'customerId', '=', 'ord') +puts Query.right_join('orders', '$id', 'customerId') +puts Query.full_outer_join('orders', '$id', 'customerId') +puts Query.cross_join('orders', 'ord') +puts Query.on('$id', 'customerId') +puts Query.left_join('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), +]) +puts Query.not_covers('location', [1, 2]) +puts Query.spatial_equals('location', [1, 2]) +puts Query.not_spatial_equals('location', [1, 2]) +page_queries = Query.page(2, 10) +puts page_queries[0] +puts page_queries[1] puts Query.builder.limit(1).build[0] # Permission & Role helper tests diff --git a/tests/e2e/languages/swift/Tests.swift b/tests/e2e/languages/swift/Tests.swift index 43258a9db6..a9dd69b2c0 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -252,6 +252,36 @@ class Tests: XCTestCase { print(Query.groupBy(["status"])) print(Query.distinct()) print(Query.covers("location", values: [1, 2])) + print(Query.countDistinct("year", alias: "uniqueYears")) + print(Query.sum("price", alias: "total")) + print(Query.avg("price", alias: "avgPrice")) + print(Query.min("price", alias: "lowest")) + print(Query.max("price", alias: "highest")) + print(Query.stddev("price", alias: "sd")) + print(Query.stddevPop("price", alias: "sdp")) + print(Query.stddevSamp("price", alias: "sds")) + print(Query.variance("price", alias: "var")) + print(Query.varPop("price", alias: "vp")) + print(Query.varSamp("price", alias: "vs")) + print(Query.bitAnd("flags", alias: "band")) + print(Query.bitOr("flags", alias: "bor")) + print(Query.bitXor("flags", alias: "bxor")) + print(Query.having([Query.greaterThan("total", value: 1)])) + print(Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")) + print(Query.rightJoin("orders", left: "$id", right: "customerId")) + print(Query.fullOuterJoin("orders", left: "$id", right: "customerId")) + print(Query.crossJoin("orders", alias: "ord")) + print(Query.on("$id", "customerId")) + print(Query.leftJoin("orders", alias: "ord", on: [ + Query.on("$id", "customerId"), + Query.equal("ord.status", value: "paid"), + ])) + print(Query.notCovers("location", values: [1, 2])) + print(Query.spatialEquals("location", values: [1, 2])) + print(Query.notSpatialEquals("location", values: [1, 2])) + let pageQueries = Query.page(2, perPage: 10) + print(pageQueries[0]) + print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) // Permission & Role helper tests diff --git a/tests/e2e/languages/unity/Tests.cs b/tests/e2e/languages/unity/Tests.cs index 40c113ad76..4c92fa875e 100644 --- a/tests/e2e/languages/unity/Tests.cs +++ b/tests/e2e/languages/unity/Tests.cs @@ -370,6 +370,36 @@ private async Task RunAsyncTest() LogResult(Query.GroupBy(new List { "status" })); LogResult(Query.Distinct()); LogResult(Query.Covers("location", new List { 1, 2 })); + LogResult(Query.CountDistinct("year", "uniqueYears")); + LogResult(Query.Sum("price", "total")); + LogResult(Query.Avg("price", "avgPrice")); + LogResult(Query.Min("price", "lowest")); + LogResult(Query.Max("price", "highest")); + LogResult(Query.Stddev("price", "sd")); + LogResult(Query.StddevPop("price", "sdp")); + LogResult(Query.StddevSamp("price", "sds")); + LogResult(Query.Variance("price", "var")); + LogResult(Query.VarPop("price", "vp")); + LogResult(Query.VarSamp("price", "vs")); + LogResult(Query.BitAnd("flags", "band")); + LogResult(Query.BitOr("flags", "bor")); + LogResult(Query.BitXor("flags", "bxor")); + LogResult(Query.Having(new List { Query.GreaterThan("total", 1) })); + LogResult(Query.LeftJoin("orders", "$id", "customerId", "=", "ord")); + LogResult(Query.RightJoin("orders", "$id", "customerId")); + LogResult(Query.FullOuterJoin("orders", "$id", "customerId")); + LogResult(Query.CrossJoin("orders", "ord")); + LogResult(Query.On("$id", "customerId")); + LogResult(Query.LeftJoin("orders", "ord", new List { + Query.On("$id", "customerId"), + Query.Equal("ord.status", "paid"), + })); + LogResult(Query.NotCovers("location", new List { 1, 2 })); + LogResult(Query.SpatialEquals("location", new List { 1, 2 })); + LogResult(Query.NotSpatialEquals("location", new List { 1, 2 })); + var pageQueries = Query.Page(2, 10); + LogResult(pageQueries[0]); + LogResult(pageQueries[1]); LogResult(Query.Builder().Limit(1).Build()[0]); // Permission & Roles helper tests LogResult(Permission.Read(Role.Any())); diff --git a/tests/e2e/languages/web/index.html b/tests/e2e/languages/web/index.html index a5f71f21ab..73f91a2f5d 100644 --- a/tests/e2e/languages/web/index.html +++ b/tests/e2e/languages/web/index.html @@ -437,6 +437,36 @@ console.log(Query.groupBy(["status"])); console.log(Query.distinct()); console.log(Query.covers("location", [1, 2])); + console.log(Query.countDistinct("year", "uniqueYears")); + console.log(Query.sum("price", "total")); + console.log(Query.avg("price", "avgPrice")); + console.log(Query.min("price", "lowest")); + console.log(Query.max("price", "highest")); + console.log(Query.stddev("price", "sd")); + console.log(Query.stddevPop("price", "sdp")); + console.log(Query.stddevSamp("price", "sds")); + console.log(Query.variance("price", "var")); + console.log(Query.varPop("price", "vp")); + console.log(Query.varSamp("price", "vs")); + console.log(Query.bitAnd("flags", "band")); + console.log(Query.bitOr("flags", "bor")); + console.log(Query.bitXor("flags", "bxor")); + console.log(Query.having([Query.greaterThan("total", 1)])); + console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); + console.log(Query.rightJoin("orders", "$id", "customerId")); + console.log(Query.fullOuterJoin("orders", "$id", "customerId")); + console.log(Query.crossJoin("orders", "ord")); + console.log(Query.on("$id", "customerId")); + console.log(Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ])); + console.log(Query.notCovers("location", [1, 2])); + console.log(Query.spatialEquals("location", [1, 2])); + console.log(Query.notSpatialEquals("location", [1, 2])); + const pageQueries = Query.page(2, 10); + console.log(pageQueries[0]); + console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); // Permission & Role helper tests diff --git a/tests/e2e/languages/web/node.js b/tests/e2e/languages/web/node.js index d92b75751f..e13ed9cd4b 100644 --- a/tests/e2e/languages/web/node.js +++ b/tests/e2e/languages/web/node.js @@ -287,6 +287,36 @@ async function start() { console.log(Query.groupBy(["status"])); console.log(Query.distinct()); console.log(Query.covers("location", [1, 2])); + console.log(Query.countDistinct("year", "uniqueYears")); + console.log(Query.sum("price", "total")); + console.log(Query.avg("price", "avgPrice")); + console.log(Query.min("price", "lowest")); + console.log(Query.max("price", "highest")); + console.log(Query.stddev("price", "sd")); + console.log(Query.stddevPop("price", "sdp")); + console.log(Query.stddevSamp("price", "sds")); + console.log(Query.variance("price", "var")); + console.log(Query.varPop("price", "vp")); + console.log(Query.varSamp("price", "vs")); + console.log(Query.bitAnd("flags", "band")); + console.log(Query.bitOr("flags", "bor")); + console.log(Query.bitXor("flags", "bxor")); + console.log(Query.having([Query.greaterThan("total", 1)])); + console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); + console.log(Query.rightJoin("orders", "$id", "customerId")); + console.log(Query.fullOuterJoin("orders", "$id", "customerId")); + console.log(Query.crossJoin("orders", "ord")); + console.log(Query.on("$id", "customerId")); + console.log(Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ])); + console.log(Query.notCovers("location", [1, 2])); + console.log(Query.spatialEquals("location", [1, 2])); + console.log(Query.notSpatialEquals("location", [1, 2])); + const pageQueries = Query.page(2, 10); + console.log(pageQueries[0]); + console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); // Permission & Role helper tests From 0b1874fe18cd967466ddcb9ba4d5ff5c8a14aaca Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 19:13:32 +1200 Subject: [PATCH 06/42] refactor: flatten query values without a queries key check Clients now map every param through a type-based flatten so builders and extra nested lists unwrap without special-casing the queries key. Document the new public Query helpers. --- .../src/main/java/io/package/Client.kt.twig | 9 +- .../src/main/java/io/package/Query.kt.twig | 327 +++++++++++++++++- templates/apple/Sources/Client.swift.twig | 8 +- templates/dart/lib/query.dart.twig | 23 ++ templates/dart/lib/src/client_mixin.dart.twig | 42 ++- templates/deno/src/client.ts.twig | 6 +- templates/deno/src/query.ts.twig | 20 ++ templates/dotnet/Package/Client.cs.twig | 4 +- templates/dotnet/Package/Query.cs.twig | 77 ++++- .../flutter/lib/src/client_mixin.dart.twig | 42 ++- templates/go/client.go.twig | 83 ++++- .../main/kotlin/io/appwrite/Client.kt.twig | 9 +- .../src/main/kotlin/io/appwrite/Query.kt.twig | 327 +++++++++++++++++- templates/node/src/client.ts.twig | 6 +- templates/php/src/Client.php.twig | 63 +++- templates/php/src/Query.php.twig | 180 ++++++++++ templates/python/package/client.py.twig | 3 +- templates/python/package/query.py.twig | 52 ++- templates/react-native/src/client.ts.twig | 6 +- templates/react-native/src/query.ts.twig | 20 ++ templates/ruby/lib/container/client.rb.twig | 6 +- templates/ruby/lib/container/query.rb.twig | 49 ++- templates/rust/src/client.rs.twig | 30 +- templates/swift/Sources/Client.swift.twig | 8 +- templates/swift/Sources/Query.swift.twig | 62 ++-- templates/web/src/client.ts.twig | 6 +- templates/web/src/query.ts.twig | 43 +++ 27 files changed, 1276 insertions(+), 235 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 9aba4cf562..0d3a9f5cc7 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -299,12 +299,9 @@ class Client @JvmOverloads constructor( responseType: Class, converter: ((Any) -> T)? = null ): T { - val requestParams = if (params.containsKey("queries")) { - params + ("queries" to Query.normalize(params["queries"])) - } else { - params - } - val filteredParams = requestParams.filterValues { it != null } + val filteredParams = params + .mapValues { Query.flatten(it.value) } + .filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) 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 6b1a34ce6e..808222fbea 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 @@ -512,106 +512,401 @@ class Query( */ fun notTouches(attribute: String, values: List) = Query("notTouches", attribute, listOf(values)).toJson() + /** + * Count rows of [attribute], optionally aliased. + * + * @param attribute The attribute to count. Defaults to `*`. + * @param alias Optional result alias. + * @returns The query string. + */ fun count(attribute: String = "*", alias: String = "") = Query("count", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Count distinct values of [attribute], optionally aliased. + * + * @param attribute The attribute to count. + * @param alias Optional result alias. + * @returns The query string. + */ fun countDistinct(attribute: String, alias: String = "") = Query("countDistinct", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Sum values of [attribute], optionally aliased. + * + * @param attribute The attribute to sum. + * @param alias Optional result alias. + * @returns The query string. + */ fun sum(attribute: String, alias: String = "") = Query("sum", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Average values of [attribute], optionally aliased. + * + * @param attribute The attribute to average. + * @param alias Optional result alias. + * @returns The query string. + */ fun avg(attribute: String, alias: String = "") = Query("avg", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Minimum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun min(attribute: String, alias: String = "") = Query("min", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Maximum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun max(attribute: String, alias: String = "") = Query("max", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun stddev(attribute: String, alias: String = "") = Query("stddev", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Population standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun stddevPop(attribute: String, alias: String = "") = Query("stddevPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Sample standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun stddevSamp(attribute: String, alias: String = "") = Query("stddevSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun variance(attribute: String, alias: String = "") = Query("variance", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Population variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun varPop(attribute: String, alias: String = "") = Query("varPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Sample variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun varSamp(attribute: String, alias: String = "") = Query("varSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Bitwise AND of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun bitAnd(attribute: String, alias: String = "") = Query("bitAnd", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Bitwise OR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun bitOr(attribute: String, alias: String = "") = Query("bitOr", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Bitwise XOR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun bitXor(attribute: String, alias: String = "") = Query("bitXor", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Group results by [attributes]. + * + * @param attributes Attributes to group by. + * @returns The query string. + */ fun groupBy(attributes: List) = Query("groupBy", null, attributes).toJson() + /** + * Filter grouped results. [queries] are nested like `and`/`or`. + * + * @param queries Nested query strings. + * @returns The query string. + */ fun having(queries: List) = Query("having", null, queries.map { it.fromJson() }).toJson() + /** + * Select distinct rows. + * + * @returns The query string. + */ fun distinct() = Query("distinct").toJson() + /** + * Column-to-column join ON condition. + * + * @param left Left-hand column. + * @param right Right-hand column. + * @param operator Comparison operator. Defaults to `=`. + * @returns The query string. + */ fun on(left: String, right: String, operator: String = "=") = Query("on", values = listOf(left, operator, right)).toJson() + /** + * Inner join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + /** + * Inner join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun join(table: String, on: List) = nestedJoin("join", table, "", on) + /** + * Inner join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun join(table: String, alias: String, on: List) = nestedJoin("join", table, alias, on) + /** + * Left join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + /** + * Left join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun leftJoin(table: String, on: List) = nestedJoin("leftJoin", table, "", on) + /** + * Left join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun leftJoin(table: String, alias: String, on: List) = nestedJoin("leftJoin", table, alias, on) + /** + * Right join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + /** + * Right join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun rightJoin(table: String, on: List) = nestedJoin("rightJoin", table, "", on) + /** + * Right join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun rightJoin(table: String, alias: String, on: List) = nestedJoin("rightJoin", table, alias, on) + /** + * Full outer join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + /** + * Full outer join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun fullOuterJoin(table: String, on: List) = nestedJoin("fullOuterJoin", table, "", on) + /** + * Full outer join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun fullOuterJoin(table: String, alias: String, on: List) = nestedJoin("fullOuterJoin", table, alias, on) + /** + * Cross join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param alias Optional table alias. + * @returns The query string. + */ fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Filter resources where [attribute] covers the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() + /** + * Filter resources where [attribute] does not cover the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun notCovers(attribute: String, values: List) = Query("notCovers", attribute, listOf(values)).toJson() + /** + * Filter resources where [attribute] is spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun spatialEquals(attribute: String, values: List) = Query("spatialEquals", attribute, listOf(values)).toJson() + /** + * Filter resources where [attribute] is not spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun notSpatialEquals(attribute: String, values: List) = Query("notSpatialEquals", attribute, listOf(values)).toJson() + /** + * Return limit and offset queries for 1-based page pagination. + * + * @param page 1-based page number. + * @param perPage Rows per page. Defaults to 25. + * @returns Limit and offset query strings. + */ fun page(page: Int, perPage: Int = 25): List { require(page >= 1) { "Page must be >= 1, got $page" } require(perPage >= 1) { "Per page must be >= 1, got $perPage" } return listOf(limit(perPage), offset((page - 1) * perPage)) } + /** + * Start a fluent query builder. + * + * @returns A new query builder. + */ fun builder() = QueryBuilder() - internal fun normalize(queries: Any?): List { - if (queries == null) { - return emptyList() - } - if (queries is QueryBuilder) { - return queries.build() - } - if (queries is Query) { - return listOf(queries.toString()) + internal fun flatten(value: Any?): Any? { + if (value is QueryBuilder) { + return value.build() } - if (queries is String) { - return listOf(queries) + if (value is Query) { + return value.toString() } - if (queries is Array<*>) { - return queries.flatMap { normalize(it) } + + val items = when (value) { + is Array<*> -> value.asList() + is Collection<*> -> value + else -> return value } - if (queries is Collection<*>) { - return queries.flatMap { normalize(it) } + + val out = ArrayList(items.size) + var changed = false + for (item in items) { + val flat = flatten(item) + if (item is QueryBuilder || item is Query) { + if (flat is Collection<*>) { + out.addAll(flat) + } else { + out.add(flat) + } + changed = true + continue + } + if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { + out.addAll(flat) + changed = true + continue + } + if (flat !== item) { + changed = true + } + out.add(flat) } - return emptyList() + return if (changed) out else value } private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 25fa2894ec..20e96d45a0 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -313,11 +313,9 @@ open class Client { sink: ((ByteBuffer) -> Void)? = nil, converter: ((Any) throws -> T)? = nil ) async throws -> T { - var params = params - if params.keys.contains("queries") { - params["queries"] = Query.normalize(params["queries"] as Any?) - } - let validParams = params.filter { $0.value != nil } + let validParams = params + .mapValues { Query.flatten($0) } + .filter { $0.value != nil } let queryParameters = method == "GET" && !validParams.isEmpty ? (path.contains("?") ? "&" : "?") + parametersToQueryString(params: validParams) diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index 24659b00fa..e3b15b1a78 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -317,45 +317,58 @@ class Query { Query._('countDistinct', attribute, alias.isNotEmpty ? alias : null) .toString(); + /// Sum values of [attribute], optionally aliased. static String sum(String attribute, [String alias = '']) => Query._('sum', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Average values of [attribute], optionally aliased. static String avg(String attribute, [String alias = '']) => Query._('avg', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Minimum value of [attribute], optionally aliased. static String min(String attribute, [String alias = '']) => Query._('min', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Maximum value of [attribute], optionally aliased. static String max(String attribute, [String alias = '']) => Query._('max', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Standard deviation of [attribute], optionally aliased. static String stddev(String attribute, [String alias = '']) => Query._('stddev', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Population standard deviation of [attribute], optionally aliased. static String stddevPop(String attribute, [String alias = '']) => Query._('stddevPop', attribute, alias.isNotEmpty ? alias : null) .toString(); + /// Sample standard deviation of [attribute], optionally aliased. static String stddevSamp(String attribute, [String alias = '']) => Query._('stddevSamp', attribute, alias.isNotEmpty ? alias : null) .toString(); + /// Variance of [attribute], optionally aliased. static String variance(String attribute, [String alias = '']) => Query._('variance', attribute, alias.isNotEmpty ? alias : null) .toString(); + /// Population variance of [attribute], optionally aliased. static String varPop(String attribute, [String alias = '']) => Query._('varPop', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Sample variance of [attribute], optionally aliased. static String varSamp(String attribute, [String alias = '']) => Query._('varSamp', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Bitwise AND of [attribute], optionally aliased. static String bitAnd(String attribute, [String alias = '']) => Query._('bitAnd', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Bitwise OR of [attribute], optionally aliased. static String bitOr(String attribute, [String alias = '']) => Query._('bitOr', attribute, alias.isNotEmpty ? alias : null).toString(); + /// Bitwise XOR of [attribute], optionally aliased. static String bitXor(String attribute, [String alias = '']) => Query._('bitXor', attribute, alias.isNotEmpty ? alias : null).toString(); @@ -370,6 +383,7 @@ class Query { queries.map((query) => jsonDecode(query)).toList(), ).toString(); + /// Select distinct rows. static String distinct() => Query._('distinct').toString(); /// Column-to-column join ON condition. @@ -386,6 +400,7 @@ class Query { ]) => Query._createJoin('join', table, leftOrAliasOrOn, rightOrOn, operator, alias); + /// Left join. [table] is the joined collection/table ID. static String leftJoin( String table, Object leftOrAliasOrOn, [ @@ -395,6 +410,7 @@ class Query { ]) => Query._createJoin('leftJoin', table, leftOrAliasOrOn, rightOrOn, operator, alias); + /// Right join. [table] is the joined collection/table ID. static String rightJoin( String table, Object leftOrAliasOrOn, [ @@ -404,6 +420,7 @@ class Query { ]) => Query._createJoin('rightJoin', table, leftOrAliasOrOn, rightOrOn, operator, alias); + /// Full outer join. [table] is the joined collection/table ID. static String fullOuterJoin( String table, Object leftOrAliasOrOn, [ @@ -413,18 +430,23 @@ class Query { ]) => Query._createJoin('fullOuterJoin', table, leftOrAliasOrOn, rightOrOn, operator, alias); + /// Cross join. [table] is the joined collection/table ID. static String crossJoin(String table, [String alias = '']) => Query._('crossJoin', table, alias.isNotEmpty ? alias : null).toString(); + /// Filter resources where [attribute] covers the given geometry. static String covers(String attribute, List values) => Query._('covers', attribute, [values]).toString(); + /// Filter resources where [attribute] does not cover the given geometry. static String notCovers(String attribute, List values) => Query._('notCovers', attribute, [values]).toString(); + /// Filter resources where [attribute] is spatially equal to the given geometry. static String spatialEquals(String attribute, List values) => Query._('spatialEquals', attribute, [values]).toString(); + /// Filter resources where [attribute] is not spatially equal to the given geometry. static String notSpatialEquals(String attribute, List values) => Query._('notSpatialEquals', attribute, [values]).toString(); @@ -443,6 +465,7 @@ class Query { ]; } + /// Start a fluent query builder. static QueryBuilder builder() => QueryBuilder(); static String _createJoin( diff --git a/templates/dart/lib/src/client_mixin.dart.twig b/templates/dart/lib/src/client_mixin.dart.twig index d93fea9473..e1269b1a1f 100644 --- a/templates/dart/lib/src/client_mixin.dart.twig +++ b/templates/dart/lib/src/client_mixin.dart.twig @@ -12,10 +12,7 @@ mixin ClientMixin { required Map headers, required Map params, }) { - if (params.containsKey('queries')) { - params = Map.from(params); - params['queries'] = _flattenQueries(params['queries']); - } + params = params.map((key, value) => MapEntry(key, _flattenValue(value))); http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { @@ -147,23 +144,30 @@ mixin ClientMixin { } } -List _flattenQueries(dynamic queries) { - if (queries == null) { - return []; +dynamic _flattenValue(dynamic value) { + if (value == null) { + return null; } - if (queries is String) { - return [queries]; + try { + return _flattenValue(value.build()); + } catch (_) {} + if (value is! List) { + return value; } - if (queries is List) { - final out = []; - for (final item in queries) { - out.addAll(_flattenQueries(item)); + + final out = []; + var changed = false; + for (final item in value) { + final flat = _flattenValue(item); + if (item is List && flat is List && flat.every((entry) => entry is String)) { + out.addAll(flat); + changed = true; + continue; } - return out; - } - try { - return _flattenQueries(queries.build()); - } catch (_) { - return [queries.toString()]; + if (flat != item) { + changed = true; + } + out.add(flat); } + return changed ? out : value; } diff --git a/templates/deno/src/client.ts.twig b/templates/deno/src/client.ts.twig index 76ec55f080..ef5a1149da 100644 --- a/templates/deno/src/client.ts.twig +++ b/templates/deno/src/client.ts.twig @@ -1,5 +1,5 @@ import { {{ spec.info.title | caseUcfirst}}Exception } from './exception.ts'; -import { normalizeQueries } from './query.ts'; +import { flattenParam } from './query.ts'; export interface Payload { [key: string]: any; @@ -82,8 +82,8 @@ export class Client { } async call(method: string, path: string = "", headers: Payload = {}, params: Payload = {}, responseType: string = "json") { - if (params.queries) { - params.queries = normalizeQueries(params.queries); + for (const key of Object.keys(params)) { + params[key] = flattenParam(params[key]); } headers = {...this.headers, ...headers}; diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index 26f880d4c5..50456bc0db 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -1017,3 +1017,23 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { } return out; } + +function isQueryList(value: unknown[]): boolean { + return value.every( + (item) => + item instanceof QueryBuilder || + item instanceof Query || + typeof item === "string" || + (Array.isArray(item) && isQueryList(item)) + ); +} + +export function flattenParam(value: unknown): unknown { + if (value instanceof QueryBuilder || value instanceof Query) { + return normalizeQueries(value as QueryInput); + } + if (Array.isArray(value) && isQueryList(value)) { + return normalizeQueries(value as QueryInput); + } + return value; +} diff --git a/templates/dotnet/Package/Client.cs.twig b/templates/dotnet/Package/Client.cs.twig index 4b38aade39..76ffefc193 100644 --- a/templates/dotnet/Package/Client.cs.twig +++ b/templates/dotnet/Package/Client.cs.twig @@ -151,9 +151,9 @@ namespace {{ spec.info.title | caseUcfirst }} Dictionary headers, Dictionary parameters) { - if (parameters.ContainsKey("queries")) + foreach (var key in parameters.Keys.ToList()) { - parameters["queries"] = Query.Normalize(parameters["queries"]); + parameters[key] = Query.Flatten(parameters[key]); } var methodGet = "GET".Equals(method, StringComparison.OrdinalIgnoreCase); diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 3310845c4c..97bdee8ffb 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -555,34 +555,83 @@ namespace {{ spec.info.title | caseUcfirst }} return new QueryBuilder(); } - internal static List Normalize(object? queries) + internal static object? Flatten(object? value) { - if (queries == null) + if (value is QueryBuilder builder) { - return new List(); + return builder.Build(); } - if (queries is QueryBuilder builder) + if (value is Query query) { - return builder.Build(); + return query.ToString(); } - if (queries is Query query) + if (value is not IEnumerable enumerable || value is string) { - return new List { query.ToString() }; + return value; } - if (queries is string s) + + var items = enumerable.Cast().ToList(); + var output = new List(); + var changed = false; + foreach (var item in items) { - return new List { s }; + var flat = Flatten(item); + if (item is QueryBuilder || item is Query) + { + if (flat is IEnumerable nested && flat is not string) + { + output.AddRange(nested.Cast()); + } + else + { + output.Add(flat); + } + changed = true; + continue; + } + if (item is IEnumerable nestedItems && item is not string && IsStringList(flat)) + { + output.AddRange(((IEnumerable)flat!).Cast()); + changed = true; + continue; + } + if (!Equals(flat, item)) + { + changed = true; + } + output.Add(flat); } - if (queries is IEnumerable enumerable) + + return changed ? output : value; + } + + private static bool IsStringList(object? value) + { + if (value is not IEnumerable enumerable || value is string) + { + return false; + } + foreach (var item in enumerable) { - var output = new List(); - foreach (var item in enumerable) + if (item is not string) { - output.AddRange(Normalize(item)); + return false; } - return output; } + return true; + } + internal static List Normalize(object? queries) + { + var flat = Flatten(queries); + if (flat is string s) + { + return new List { s }; + } + if (flat is IEnumerable enumerable && flat is not string) + { + return enumerable.Cast().Select(item => item?.ToString() ?? string.Empty).ToList(); + } return new List(); } diff --git a/templates/flutter/lib/src/client_mixin.dart.twig b/templates/flutter/lib/src/client_mixin.dart.twig index c7ff181ffe..5562aac126 100644 --- a/templates/flutter/lib/src/client_mixin.dart.twig +++ b/templates/flutter/lib/src/client_mixin.dart.twig @@ -12,10 +12,7 @@ mixin ClientMixin { required Map headers, required Map params, }) { - if (params.containsKey('queries')) { - params = Map.from(params); - params['queries'] = _flattenQueries(params['queries']); - } + params = params.map((key, value) => MapEntry(key, _flattenValue(value))); http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { @@ -150,23 +147,30 @@ mixin ClientMixin { } } -List _flattenQueries(dynamic queries) { - if (queries == null) { - return []; +dynamic _flattenValue(dynamic value) { + if (value == null) { + return null; } - if (queries is String) { - return [queries]; + try { + return _flattenValue(value.build()); + } catch (_) {} + if (value is! List) { + return value; } - if (queries is List) { - final out = []; - for (final item in queries) { - out.addAll(_flattenQueries(item)); + + final out = []; + var changed = false; + for (final item in value) { + final flat = _flattenValue(item); + if (item is List && flat is List && flat.every((entry) => entry is String)) { + out.addAll(flat); + changed = true; + continue; } - return out; - } - try { - return _flattenQueries(queries.build()); - } catch (_) { - return [queries.toString()]; + if (flat != item) { + changed = true; + } + out.add(flat); } + return changed ? out : value; } diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index 658618061a..746605cf96 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -437,8 +437,8 @@ func (client *Client) FileUpload(url string, headers map[string]interface{}, par // Call an API using Client func (client *Client) Call(method string, path string, headers map[string]interface{}, params map[string]interface{}) (*ClientResponse, error) { if params != nil { - if _, ok := params["queries"]; ok { - params["queries"] = flattenQueries(params["queries"]) + for key, value := range params { + params[key] = flattenValue(value) } } @@ -762,32 +762,77 @@ func flatten(params interface{}, prefix string, result *map[string]string) error return nil } -func flattenQueries(queries interface{}) []string { - if queries == nil { - return []string{} +func flattenValue(value interface{}) interface{} { + if value == nil { + return nil } - - switch v := queries.(type) { + switch v := value.(type) { case *query.QueryBuilder: if v == nil { - return []string{} + return value } return v.Build() case query.QueryBuilder: return v.Build() - case string: - return []string{v} } - value := reflect.ValueOf(queries) - switch value.Kind() { - case reflect.Slice, reflect.Array: - out := make([]string, 0, value.Len()) - for i := 0; i < value.Len(); i++ { - out = append(out, flattenQueries(value.Index(i).Interface())...) + reflected := reflect.ValueOf(value) + if reflected.Kind() != reflect.Slice && reflected.Kind() != reflect.Array { + return value + } + + out := make([]interface{}, 0, reflected.Len()) + changed := false + for i := 0; i < reflected.Len(); i++ { + item := reflected.Index(i).Interface() + flat := flattenValue(item) + switch item.(type) { + case *query.QueryBuilder, query.QueryBuilder: + if list, ok := flat.([]string); ok { + for _, entry := range list { + out = append(out, entry) + } + changed = true + continue + } + } + if nested, ok := item.([]string); ok { + for _, entry := range nested { + out = append(out, entry) + } + if len(nested) > 0 { + changed = true + } + continue + } + if nested, ok := item.([]interface{}); ok && isStringSlice(flattenValue(nested)) { + for _, entry := range flattenValue(nested).([]interface{}) { + out = append(out, entry) + } + changed = true + continue + } + if flat != item { + changed = true + } + out = append(out, flat) + } + if !changed { + return value + } + return out +} + +func isStringSlice(value interface{}) bool { + items, ok := value.([]interface{}) + if !ok { + _, ok := value.([]string) + return ok + } + for _, item := range items { + if _, ok := item.(string); !ok { + return false } - return out - default: - return []string{} } + return true } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 2bdd92df01..c33fbd5b7e 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -238,12 +238,9 @@ class Client @JvmOverloads constructor( headers: Map = mapOf(), params: Map = mapOf(), ): Request { - val requestParams = if (params.containsKey("queries")) { - params + ("queries" to Query.normalize(params["queries"])) - } else { - params - } - val filteredParams = requestParams.filterValues { it != null } + val filteredParams = params + .mapValues { Query.flatten(it.value) } + .filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) 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 f72da86a0a..d5f7001fca 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -220,106 +220,401 @@ class Query( fun notTouches(attribute: String, values: List) = Query("notTouches", attribute, listOf(values)).toJson() + /** + * Count rows of [attribute], optionally aliased. + * + * @param attribute The attribute to count. Defaults to `*`. + * @param alias Optional result alias. + * @returns The query string. + */ fun count(attribute: String = "*", alias: String = "") = Query("count", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Count distinct values of [attribute], optionally aliased. + * + * @param attribute The attribute to count. + * @param alias Optional result alias. + * @returns The query string. + */ fun countDistinct(attribute: String, alias: String = "") = Query("countDistinct", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Sum values of [attribute], optionally aliased. + * + * @param attribute The attribute to sum. + * @param alias Optional result alias. + * @returns The query string. + */ fun sum(attribute: String, alias: String = "") = Query("sum", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Average values of [attribute], optionally aliased. + * + * @param attribute The attribute to average. + * @param alias Optional result alias. + * @returns The query string. + */ fun avg(attribute: String, alias: String = "") = Query("avg", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Minimum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun min(attribute: String, alias: String = "") = Query("min", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Maximum value of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun max(attribute: String, alias: String = "") = Query("max", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun stddev(attribute: String, alias: String = "") = Query("stddev", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Population standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun stddevPop(attribute: String, alias: String = "") = Query("stddevPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Sample standard deviation of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun stddevSamp(attribute: String, alias: String = "") = Query("stddevSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun variance(attribute: String, alias: String = "") = Query("variance", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Population variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun varPop(attribute: String, alias: String = "") = Query("varPop", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Sample variance of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun varSamp(attribute: String, alias: String = "") = Query("varSamp", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Bitwise AND of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun bitAnd(attribute: String, alias: String = "") = Query("bitAnd", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Bitwise OR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun bitOr(attribute: String, alias: String = "") = Query("bitOr", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Bitwise XOR of [attribute], optionally aliased. + * + * @param attribute The attribute to inspect. + * @param alias Optional result alias. + * @returns The query string. + */ fun bitXor(attribute: String, alias: String = "") = Query("bitXor", attribute, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Group results by [attributes]. + * + * @param attributes Attributes to group by. + * @returns The query string. + */ fun groupBy(attributes: List) = Query("groupBy", null, attributes).toJson() + /** + * Filter grouped results. [queries] are nested like `and`/`or`. + * + * @param queries Nested query strings. + * @returns The query string. + */ fun having(queries: List) = Query("having", null, queries.map { it.fromJson() }).toJson() + /** + * Select distinct rows. + * + * @returns The query string. + */ fun distinct() = Query("distinct").toJson() + /** + * Column-to-column join ON condition. + * + * @param left Left-hand column. + * @param right Right-hand column. + * @param operator Comparison operator. Defaults to `=`. + * @returns The query string. + */ fun on(left: String, right: String, operator: String = "=") = Query("on", values = listOf(left, operator, right)).toJson() + /** + * Inner join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("join", table, left, right, operator, alias) + /** + * Inner join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun join(table: String, on: List) = nestedJoin("join", table, "", on) + /** + * Inner join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun join(table: String, alias: String, on: List) = nestedJoin("join", table, alias, on) + /** + * Left join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("leftJoin", table, left, right, operator, alias) + /** + * Left join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun leftJoin(table: String, on: List) = nestedJoin("leftJoin", table, "", on) + /** + * Left join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun leftJoin(table: String, alias: String, on: List) = nestedJoin("leftJoin", table, alias, on) + /** + * Right join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("rightJoin", table, left, right, operator, alias) + /** + * Right join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun rightJoin(table: String, on: List) = nestedJoin("rightJoin", table, "", on) + /** + * Right join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun rightJoin(table: String, alias: String, on: List) = nestedJoin("rightJoin", table, alias, on) + /** + * Full outer join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param left Left-hand join column. + * @param right Right-hand join column. + * @param operator Comparison operator. Defaults to `=`. + * @param alias Optional table alias. + * @returns The query string. + */ fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = joinQuery("fullOuterJoin", table, left, right, operator, alias) + /** + * Full outer join with nested ON queries and no alias. + * + * @param table Joined table ID. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun fullOuterJoin(table: String, on: List) = nestedJoin("fullOuterJoin", table, "", on) + /** + * Full outer join with nested ON queries and an alias. + * + * @param table Joined table ID. + * @param alias Table alias. + * @param on Nested ON queries, including [on]. + * @returns The query string. + */ fun fullOuterJoin(table: String, alias: String, on: List) = nestedJoin("fullOuterJoin", table, alias, on) + /** + * Cross join. [table] is the joined collection/table ID. + * + * @param table Joined table ID. + * @param alias Optional table alias. + * @returns The query string. + */ fun crossJoin(table: String, alias: String = "") = Query("crossJoin", table, if (alias.isNotEmpty()) listOf(alias) else null).toJson() + /** + * Filter resources where [attribute] covers the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun covers(attribute: String, values: List) = Query("covers", attribute, listOf(values)).toJson() + /** + * Filter resources where [attribute] does not cover the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun notCovers(attribute: String, values: List) = Query("notCovers", attribute, listOf(values)).toJson() + /** + * Filter resources where [attribute] is spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun spatialEquals(attribute: String, values: List) = Query("spatialEquals", attribute, listOf(values)).toJson() + /** + * Filter resources where [attribute] is not spatially equal to the given geometry. + * + * @param attribute The attribute to filter on. + * @param values The coordinate values. + * @returns The query string. + */ fun notSpatialEquals(attribute: String, values: List) = Query("notSpatialEquals", attribute, listOf(values)).toJson() + /** + * Return limit and offset queries for 1-based page pagination. + * + * @param page 1-based page number. + * @param perPage Rows per page. Defaults to 25. + * @returns Limit and offset query strings. + */ fun page(page: Int, perPage: Int = 25): List { require(page >= 1) { "Page must be >= 1, got $page" } require(perPage >= 1) { "Per page must be >= 1, got $perPage" } return listOf(limit(perPage), offset((page - 1) * perPage)) } + /** + * Start a fluent query builder. + * + * @returns A new query builder. + */ fun builder() = QueryBuilder() - internal fun normalize(queries: Any?): List { - if (queries == null) { - return emptyList() - } - if (queries is QueryBuilder) { - return queries.build() - } - if (queries is Query) { - return listOf(queries.toString()) + internal fun flatten(value: Any?): Any? { + if (value is QueryBuilder) { + return value.build() } - if (queries is String) { - return listOf(queries) + if (value is Query) { + return value.toString() } - if (queries is Array<*>) { - return queries.flatMap { normalize(it) } + + val items = when (value) { + is Array<*> -> value.asList() + is Collection<*> -> value + else -> return value } - if (queries is Collection<*>) { - return queries.flatMap { normalize(it) } + + val out = ArrayList(items.size) + var changed = false + for (item in items) { + val flat = flatten(item) + if (item is QueryBuilder || item is Query) { + if (flat is Collection<*>) { + out.addAll(flat) + } else { + out.add(flat) + } + changed = true + continue + } + if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { + out.addAll(flat) + changed = true + continue + } + if (flat !== item) { + changed = true + } + out.add(flat) } - return emptyList() + return if (changed) out else value } private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 398b7acc20..4edc299701 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -1,7 +1,7 @@ import { fetch, FormData, File, Agent, type RequestInit } from 'undici'; import { Models } from './models'; import { InputFile } from './inputFile'; -import { normalizeQueries } from './query'; +import { flattenParam } from './query'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true }); @@ -223,8 +223,8 @@ class Client { prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { method = method.toUpperCase(); - if (Object.prototype.hasOwnProperty.call(params, 'queries')) { - params = { ...params, queries: normalizeQueries(params.queries) }; + for (const key of Object.keys(params)) { + params[key] = flattenParam(params[key]); } headers = Object.assign({}, this.headers, headers); diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 93f5bd4f0f..0c5b241c99 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -281,9 +281,7 @@ class Client } {% endif %} $headers = array_merge($this->headers, $headers); - if (\array_key_exists('queries', $params)) { - $params['queries'] = self::flattenQueries($params['queries']); - } + $params = \array_map(self::flattenValue(...), $params); $querySeparator = str_contains($path, '?') ? '&' : '?'; $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? $querySeparator . http_build_query($params) : '')); $responseHeaders = []; @@ -422,29 +420,56 @@ class Client return $data; } - private static function flattenQueries(mixed $queries): array + private static function flattenValue(mixed $value): mixed { - if ($queries instanceof QueryBuilder) { - return $queries->build(); + if ($value instanceof QueryBuilder) { + return $value->build(); } - - if (!\is_array($queries)) { - return []; + if ($value instanceof Query) { + return $value->__toString(); + } + if (!\is_array($value) || !\array_is_list($value)) { + return $value; } $out = []; - foreach ($queries as $item) { - if ($item instanceof QueryBuilder) { - array_push($out, ...$item->build()); - } elseif ($item instanceof Query) { - $out[] = $item->__toString(); - } elseif (\is_array($item)) { - array_push($out, ...self::flattenQueries($item)); - } else { - $out[] = $item; + $changed = false; + foreach ($value as $item) { + $flat = self::flattenValue($item); + if ($item instanceof QueryBuilder || $item instanceof Query) { + if (\is_array($flat)) { + array_push($out, ...$flat); + } else { + $out[] = $flat; + } + $changed = true; + continue; + } + if (\is_array($item) && \array_is_list($item) && self::isStringList($flat)) { + array_push($out, ...$flat); + $changed = true; + continue; + } + if ($flat !== $item) { + $changed = true; + } + $out[] = $flat; + } + + return $changed ? $out : $value; + } + + private static function isStringList(mixed $value): bool + { + if (!\is_array($value) || !\array_is_list($value)) { + return false; + } + foreach ($value as $item) { + if (!\is_string($item)) { + return false; } } - return $out; + return true; } } diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 852f248f94..3cd0f67a8f 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -705,83 +705,191 @@ class Query implements \JsonSerializable return (new Query('notTouches', $attribute, [$values]))->__toString(); } + /** + * Count rows of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function count(string $attribute = '*', string $alias = ''): string { return (new Query('count', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Count distinct values of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function countDistinct(string $attribute, string $alias = ''): string { return (new Query('countDistinct', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Sum values of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function sum(string $attribute, string $alias = ''): string { return (new Query('sum', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Average values of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function avg(string $attribute, string $alias = ''): string { return (new Query('avg', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Minimum value of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function min(string $attribute, string $alias = ''): string { return (new Query('min', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Maximum value of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function max(string $attribute, string $alias = ''): string { return (new Query('max', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Standard deviation of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function stddev(string $attribute, string $alias = ''): string { return (new Query('stddev', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Population standard deviation of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function stddevPop(string $attribute, string $alias = ''): string { return (new Query('stddevPop', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Sample standard deviation of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function stddevSamp(string $attribute, string $alias = ''): string { return (new Query('stddevSamp', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Variance of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function variance(string $attribute, string $alias = ''): string { return (new Query('variance', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Population variance of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function varPop(string $attribute, string $alias = ''): string { return (new Query('varPop', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Sample variance of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function varSamp(string $attribute, string $alias = ''): string { return (new Query('varSamp', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Bitwise AND of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function bitAnd(string $attribute, string $alias = ''): string { return (new Query('bitAnd', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Bitwise OR of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function bitOr(string $attribute, string $alias = ''): string { return (new Query('bitOr', $attribute, $alias !== '' ? $alias : null))->__toString(); } + /** + * Bitwise XOR of $attribute, optionally aliased. + * + * @param string $attribute + * @param string $alias + * @return string + */ public static function bitXor(string $attribute, string $alias = ''): string { return (new Query('bitXor', $attribute, $alias !== '' ? $alias : null))->__toString(); } /** + * Group results by attributes. + * * @param array $attributes + * @return string */ public static function groupBy(array $attributes): string { @@ -789,7 +897,10 @@ class Query implements \JsonSerializable } /** + * Filter grouped results. $queries are nested like and/or. + * * @param array $queries + * @return string */ public static function having(array $queries): string { @@ -804,19 +915,38 @@ class Query implements \JsonSerializable return (new Query('having', null, $queries))->__toString(); } + /** + * Select distinct rows. + * + * @return string + */ public static function distinct(): string { return (new Query('distinct', null, null))->__toString(); } + /** + * Column-to-column join ON condition. + * + * @param string $left + * @param string $right + * @param string $operator + * @return string + */ public static function on(string $left, string $right, string $operator = '='): string { return (new Query('on', null, [$left, $operator, $right]))->__toString(); } /** + * Inner join. $table is the joined collection/table ID. + * + * @param string $table * @param string|array $leftOrAliasOrOn * @param string|array $rightOrOn + * @param string $operator + * @param string $alias + * @return string */ public static function join(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { @@ -824,8 +954,14 @@ class Query implements \JsonSerializable } /** + * Left join. $table is the joined collection/table ID. + * + * @param string $table * @param string|array $leftOrAliasOrOn * @param string|array $rightOrOn + * @param string $operator + * @param string $alias + * @return string */ public static function leftJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { @@ -833,8 +969,14 @@ class Query implements \JsonSerializable } /** + * Right join. $table is the joined collection/table ID. + * + * @param string $table * @param string|array $leftOrAliasOrOn * @param string|array $rightOrOn + * @param string $operator + * @param string $alias + * @return string */ public static function rightJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { @@ -842,21 +984,38 @@ class Query implements \JsonSerializable } /** + * Full outer join. $table is the joined collection/table ID. + * + * @param string $table * @param string|array $leftOrAliasOrOn * @param string|array $rightOrOn + * @param string $operator + * @param string $alias + * @return string */ public static function fullOuterJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { return self::createJoin('fullOuterJoin', $table, $leftOrAliasOrOn, $rightOrOn, $operator, $alias); } + /** + * Cross join. $table is the joined collection/table ID. + * + * @param string $table + * @param string $alias + * @return string + */ public static function crossJoin(string $table, string $alias = ''): string { return (new Query('crossJoin', $table, $alias !== '' ? $alias : null))->__toString(); } /** + * Filter resources where $attribute covers the given geometry. + * + * @param string $attribute * @param array $values + * @return string */ public static function covers(string $attribute, array $values): string { @@ -864,7 +1023,11 @@ class Query implements \JsonSerializable } /** + * Filter resources where $attribute does not cover the given geometry. + * + * @param string $attribute * @param array $values + * @return string */ public static function notCovers(string $attribute, array $values): string { @@ -872,7 +1035,11 @@ class Query implements \JsonSerializable } /** + * Filter resources where $attribute is spatially equal to the given geometry. + * + * @param string $attribute * @param array $values + * @return string */ public static function spatialEquals(string $attribute, array $values): string { @@ -880,7 +1047,11 @@ class Query implements \JsonSerializable } /** + * Filter resources where $attribute is not spatially equal to the given geometry. + * + * @param string $attribute * @param array $values + * @return string */ public static function notSpatialEquals(string $attribute, array $values): string { @@ -888,6 +1059,10 @@ class Query implements \JsonSerializable } /** + * Return limit and offset queries for 1-based page pagination. + * + * @param int $page + * @param int $perPage * @return array */ public static function page(int $page, int $perPage = 25): array @@ -905,6 +1080,11 @@ class Query implements \JsonSerializable ]; } + /** + * Start a fluent query builder. + * + * @return QueryBuilder + */ public static function builder(): QueryBuilder { return new QueryBuilder(); diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index e7eb654a5d..877985c087 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -70,8 +70,7 @@ class Client: if params is None: params = {} - if 'queries' in params: - params['queries'] = Query._normalize(params['queries']) + params = {key: Query._flatten(value) for key, value in params.items()} data = {} files = {} diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index ca03f3bedf..c2896279f8 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -421,25 +421,43 @@ class Query(): return QueryBuilder() @staticmethod - def _normalize(queries): - if isinstance(queries, QueryBuilder): - return queries.build() - - if not isinstance(queries, list): - return [] + def _flatten(value): + if isinstance(value, QueryBuilder): + return value.build() + if isinstance(value, Query): + return str(value) + if not isinstance(value, list): + return value out = [] - for item in queries: - if isinstance(item, QueryBuilder): - out.extend(item.build()) - elif isinstance(item, Query): - out.append(str(item)) - elif isinstance(item, list): - out.extend(Query._normalize(item)) - else: - out.append(item) - - return out + changed = False + for item in value: + flat = Query._flatten(item) + if isinstance(item, (QueryBuilder, Query)): + if isinstance(flat, list): + out.extend(flat) + else: + out.append(flat) + changed = True + continue + if isinstance(item, list) and isinstance(flat, list) and all(isinstance(entry, str) for entry in flat): + out.extend(flat) + changed = True + continue + if flat is not item: + changed = True + out.append(flat) + + return out if changed else value + + @staticmethod + def _normalize(queries): + flat = Query._flatten(queries) + if isinstance(flat, list): + return [str(item) for item in flat] + if isinstance(flat, str): + return [flat] + return [] @staticmethod def _create_join(method, table, left, right, operator, alias): diff --git a/templates/react-native/src/client.ts.twig b/templates/react-native/src/client.ts.twig index 80f74fdb11..8640a4e080 100644 --- a/templates/react-native/src/client.ts.twig +++ b/templates/react-native/src/client.ts.twig @@ -1,7 +1,7 @@ import { Models } from './models'; import { Service } from './service'; import { Platform } from 'react-native'; -import { Query, normalizeQueries } from './query'; +import { Query, flattenParam } from './query'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true }); @@ -569,8 +569,8 @@ class Client { async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}, responseType = 'json'): Promise { method = method.toUpperCase(); - if (Object.prototype.hasOwnProperty.call(params, 'queries')) { - params = { ...params, queries: normalizeQueries(params.queries) }; + for (const key of Object.keys(params)) { + params[key] = flattenParam(params[key]); } headers = Object.assign({}, this.headers, headers); diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index edb12ef79e..b510a11214 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -1017,3 +1017,23 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { } return out; } + +function isQueryList(value: unknown[]): boolean { + return value.every( + (item) => + item instanceof QueryBuilder || + item instanceof Query || + typeof item === "string" || + (Array.isArray(item) && isQueryList(item)) + ); +} + +export function flattenParam(value: unknown): unknown { + if (value instanceof QueryBuilder || value instanceof Query) { + return normalizeQueries(value as QueryInput); + } + if (Array.isArray(value) && isQueryList(value)) { + return normalizeQueries(value as QueryInput); + } + return value; +} diff --git a/templates/ruby/lib/container/client.rb.twig b/templates/ruby/lib/container/client.rb.twig index 92e7eb044a..1a028d659f 100644 --- a/templates/ruby/lib/container/client.rb.twig +++ b/templates/ruby/lib/container/client.rb.twig @@ -111,11 +111,7 @@ module {{ spec.info.title | caseUcfirst }} params: {}, response_type: nil ) - if params.key?(:queries) && !params[:queries].nil? - params[:queries] = Query._normalize(params[:queries]) - elsif params.key?('queries') && !params['queries'].nil? - params['queries'] = Query._normalize(params['queries']) - end + params = params.transform_values { |value| Query._flatten(value) } separator = path.include?('?') ? '&' : '?' uri = URI.parse(@endpoint + path + ((method == "GET" && params.length) ? separator + encode(params) : '')) diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index 0bb346a7bd..3f32636f32 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -406,29 +406,42 @@ module {{spec.info.title | caseUcfirst}} return QueryBuilder.new end - def _normalize(queries) - if queries.is_a?(QueryBuilder) - return queries.build - end - - unless queries.is_a?(Array) - return [] - end + def _flatten(value) + return value.build if value.is_a?(QueryBuilder) + return value.to_s if value.is_a?(Query) + return value unless value.is_a?(Array) out = [] - queries.each do |item| - if item.is_a?(QueryBuilder) - out.concat(item.build) - elsif item.is_a?(Query) - out << item.to_s - elsif item.is_a?(Array) - out.concat(_normalize(item)) - else - out << item + changed = false + value.each do |item| + flat = _flatten(item) + if item.is_a?(QueryBuilder) || item.is_a?(Query) + if flat.is_a?(Array) + out.concat(flat) + else + out << flat + end + changed = true + next + end + if item.is_a?(Array) && flat.is_a?(Array) && flat.all? { |entry| entry.is_a?(String) } + out.concat(flat) + changed = true + next end + changed = true if flat != item + out << flat end - return out + changed ? out : value + end + + def _normalize(queries) + flat = _flatten(queries) + return flat.map(&:to_s) if flat.is_a?(Array) + return [flat] if flat.is_a?(String) + + [] end private diff --git a/templates/rust/src/client.rs.twig b/templates/rust/src/client.rs.twig index 9cd2f7e94e..6d26da9fc1 100644 --- a/templates/rust/src/client.rs.twig +++ b/templates/rust/src/client.rs.twig @@ -345,15 +345,29 @@ impl Client { result } - fn normalize_request_params(mut params: HashMap) -> HashMap { - if let Some(queries) = params.get("queries").cloned() { - let normalized = Query::normalize_value(&queries) - .into_iter() - .map(Value::String) - .collect::>(); - params.insert("queries".to_string(), Value::Array(normalized)); - } + fn normalize_request_params(params: HashMap) -> HashMap { params + .into_iter() + .map(|(key, value)| (key, Self::flatten_value(value))) + .collect() + } + + fn flatten_value(value: Value) -> Value { + match value { + Value::Array(items) + if items + .iter() + .all(|item| item.is_string() || item.is_array()) => + { + Value::Array( + Query::normalize_value(&Value::Array(items)) + .into_iter() + .map(Value::String) + .collect(), + ) + } + other => other, + } } pub async fn call( diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index ead0aa50ad..513c28c16a 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -324,11 +324,9 @@ open class Client { headers: [String: String] = [:], params: [String: Any?] = [:] ) throws -> HTTPClientRequest { - var params = params - if params.keys.contains("queries") { - params["queries"] = Query.normalize(params["queries"] as Any?) - } - let validParams = params.filter { $0.value != nil } + let validParams = params + .mapValues { Query.flatten($0) } + .filter { $0.value != nil } let queryParameters = method == "GET" && !validParams.isEmpty ? (path.contains("?") ? "&" : "?") + parametersToQueryString(params: validParams) diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index 6599a4148e..a64dd3d0e9 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -885,38 +885,46 @@ public struct Query : Codable, CustomStringConvertible { return QueryBuilder() } - static func normalize(_ queries: Any?) -> [String] { - guard let queries else { - return [] - } - if queries is NSNull { - return [] - } - if let builder = queries as? QueryBuilder { + static func flatten(_ value: Any?) -> Any? { + if let builder = value as? QueryBuilder { return builder.build() } - if let query = queries as? Query { - return [query.description] - } - if let string = queries as? String { - return [string] - } - if let strings = queries as? [String] { - return strings + if let query = value as? Query { + return query.description } - if let builders = queries as? [QueryBuilder] { - return builders.flatMap { $0.build() } - } - if let nested = queries as? [[String]] { - return nested.flatMap { $0 } - } - if let nestedAny = queries as? [Any] { - return nestedAny.flatMap { normalize($0) } + + let items: [Any] + if let typed = value as? [Any] { + items = typed + } else if let typed = value as? [Any?] { + items = typed.compactMap { $0 } + } else { + return value } - if let nestedOptional = queries as? [Any?] { - return nestedOptional.flatMap { normalize($0) } + + var out: [Any] = [] + var changed = false + for item in items { + let flat = flatten(item) + if item is QueryBuilder || item is Query { + if let list = flat as? [Any] { + out.append(contentsOf: list) + } else if let flat { + out.append(flat) + } + changed = true + continue + } + if let list = flat as? [Any], (item is [Any] || item is [String]), list.allSatisfy({ $0 is String }) { + out.append(contentsOf: list) + changed = true + continue + } + if let flat { + out.append(flat) + } } - return [] + return changed ? out : value } private static func joinQuery(_ method: String, table: String, left: String, right: String, operator op: String, alias: String) -> String { diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 93300fc338..ab0d395488 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1,6 +1,6 @@ import { Models } from './models'; import { Channel, ActionableChannel, ResolvedChannel } from './channel'; -import { normalizeQueries, type QueryInput } from './query'; +import { flattenParam, normalizeQueries, type QueryInput } from './query'; import { ID } from './id'; import JSONbigModule from 'json-bigint'; const JSONbigParser = JSONbigModule({ storeAsString: false }); @@ -825,8 +825,8 @@ class Client { prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { method = method.toUpperCase(); - if (Object.prototype.hasOwnProperty.call(params, 'queries')) { - params = { ...params, queries: normalizeQueries(params.queries) }; + for (const key of Object.keys(params)) { + params[key] = flattenParam(params[key]); } headers = Object.assign({}, this.headers, headers); diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index 6ac5754ebc..4bd6a5e8bf 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -616,42 +616,55 @@ export class Query { static countDistinct = (attribute: string, alias: string = ""): string => new Query("countDistinct", attribute, alias !== "" ? alias : undefined).toString(); + /** Sum values of `attribute`, optionally aliased. */ static sum = (attribute: string, alias: string = ""): string => new Query("sum", attribute, alias !== "" ? alias : undefined).toString(); + /** Average values of `attribute`, optionally aliased. */ static avg = (attribute: string, alias: string = ""): string => new Query("avg", attribute, alias !== "" ? alias : undefined).toString(); + /** Minimum value of `attribute`, optionally aliased. */ static min = (attribute: string, alias: string = ""): string => new Query("min", attribute, alias !== "" ? alias : undefined).toString(); + /** Maximum value of `attribute`, optionally aliased. */ static max = (attribute: string, alias: string = ""): string => new Query("max", attribute, alias !== "" ? alias : undefined).toString(); + /** Standard deviation of `attribute`, optionally aliased. */ static stddev = (attribute: string, alias: string = ""): string => new Query("stddev", attribute, alias !== "" ? alias : undefined).toString(); + /** Population standard deviation of `attribute`, optionally aliased. */ static stddevPop = (attribute: string, alias: string = ""): string => new Query("stddevPop", attribute, alias !== "" ? alias : undefined).toString(); + /** Sample standard deviation of `attribute`, optionally aliased. */ static stddevSamp = (attribute: string, alias: string = ""): string => new Query("stddevSamp", attribute, alias !== "" ? alias : undefined).toString(); + /** Variance of `attribute`, optionally aliased. */ static variance = (attribute: string, alias: string = ""): string => new Query("variance", attribute, alias !== "" ? alias : undefined).toString(); + /** Population variance of `attribute`, optionally aliased. */ static varPop = (attribute: string, alias: string = ""): string => new Query("varPop", attribute, alias !== "" ? alias : undefined).toString(); + /** Sample variance of `attribute`, optionally aliased. */ static varSamp = (attribute: string, alias: string = ""): string => new Query("varSamp", attribute, alias !== "" ? alias : undefined).toString(); + /** Bitwise AND of `attribute`, optionally aliased. */ static bitAnd = (attribute: string, alias: string = ""): string => new Query("bitAnd", attribute, alias !== "" ? alias : undefined).toString(); + /** Bitwise OR of `attribute`, optionally aliased. */ static bitOr = (attribute: string, alias: string = ""): string => new Query("bitOr", attribute, alias !== "" ? alias : undefined).toString(); + /** Bitwise XOR of `attribute`, optionally aliased. */ static bitXor = (attribute: string, alias: string = ""): string => new Query("bitXor", attribute, alias !== "" ? alias : undefined).toString(); @@ -671,6 +684,7 @@ export class Query { queries.map((query) => JSONbig.parse(query)) ).toString(); + /** Select distinct rows. */ static distinct = (): string => new Query("distinct").toString(); @@ -696,6 +710,7 @@ export class Query { ): string => Query.createJoin("join", table, leftOrAliasOrOn, rightOrOn, operator, alias); + /** Left join. `table` is the joined collection/table ID. */ static leftJoin = ( table: string, leftOrAliasOrOn: string | string[], @@ -705,6 +720,7 @@ export class Query { ): string => Query.createJoin("leftJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); + /** Right join. `table` is the joined collection/table ID. */ static rightJoin = ( table: string, leftOrAliasOrOn: string | string[], @@ -714,6 +730,7 @@ export class Query { ): string => Query.createJoin("rightJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); + /** Full outer join. `table` is the joined collection/table ID. */ static fullOuterJoin = ( table: string, leftOrAliasOrOn: string | string[], @@ -723,18 +740,23 @@ export class Query { ): string => Query.createJoin("fullOuterJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); + /** Cross join. `table` is the joined collection/table ID. */ static crossJoin = (table: string, alias: string = ""): string => new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); + /** Filter resources where `attribute` covers the given geometry. */ static covers = (attribute: string, values: any[]): string => new Query("covers", attribute, [values]).toString(); + /** Filter resources where `attribute` does not cover the given geometry. */ static notCovers = (attribute: string, values: any[]): string => new Query("notCovers", attribute, [values]).toString(); + /** Filter resources where `attribute` is spatially equal to the given geometry. */ static spatialEquals = (attribute: string, values: any[]): string => new Query("spatialEquals", attribute, [values]).toString(); + /** Filter resources where `attribute` is not spatially equal to the given geometry. */ static notSpatialEquals = (attribute: string, values: any[]): string => new Query("notSpatialEquals", attribute, [values]).toString(); @@ -751,6 +773,7 @@ export class Query { return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; }; + /** Start a fluent query builder. */ static builder = (): QueryBuilder => new QueryBuilder(); private static createJoin( @@ -1175,3 +1198,23 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { } return out; } + +function isQueryList(value: unknown[]): boolean { + return value.every( + (item) => + item instanceof QueryBuilder || + item instanceof Query || + typeof item === "string" || + (Array.isArray(item) && isQueryList(item)) + ); +} + +export function flattenParam(value: unknown): unknown { + if (value instanceof QueryBuilder || value instanceof Query) { + return normalizeQueries(value as QueryInput); + } + if (Array.isArray(value) && isQueryList(value)) { + return normalizeQueries(value as QueryInput); + } + return value; +} From 844e3e1f8e66a3768a2e6b3ff3fae01d18f2e359 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 19:44:38 +1200 Subject: [PATCH 07/42] fix: treat oneOf string enums as closed wire enums Live specs encode closed enums as type:string plus oneOf of single-value string schemas. Parsing that as an object made path params Any, JSON keys the enum title, and Ruby docs crash on non-JSON examples. Keep model JSON keys as the property name, type signatures from the collapsed enum, and flatten Dart request maps as Map. --- src/SDK/Language.php | 81 +++++++++++++++- src/SDK/Language/Dart.php | 3 +- src/SDK/Language/DotNet.php | 3 +- src/SDK/Language/Kotlin.php | 3 +- src/SDK/Language/PHP.php | 7 +- src/SDK/Language/Python.php | 3 +- src/SDK/Language/Ruby.php | 6 +- src/SDK/Language/Rust.php | 3 +- src/SDK/Language/Swift.php | 3 +- src/SDK/SDK.php | 6 +- templates/apple/Package.swift.twig | 4 + templates/dart/lib/src/client_mixin.dart.twig | 4 +- .../dart/test/src/models/model_test.dart.twig | 2 +- .../flutter/lib/src/client_mixin.dart.twig | 4 +- templates/swift/Package.swift.twig | 4 + tests/e2e/Base.php | 92 +++++++++++++++++++ tests/resources/spec-openapi3.json | 44 +++++++++ 17 files changed, 245 insertions(+), 27 deletions(-) diff --git a/src/SDK/Language.php b/src/SDK/Language.php index 34bebe7bdb..0ec47009ea 100644 --- a/src/SDK/Language.php +++ b/src/SDK/Language.php @@ -7,6 +7,7 @@ use Utopia\OpenAPI\Model\ArraySchema; use Utopia\OpenAPI\Model\BooleanSchema; use Utopia\OpenAPI\Model\CompositeSchema; +use Utopia\OpenAPI\Model\Composition; use Utopia\OpenAPI\Model\IntegerSchema; use Utopia\OpenAPI\Model\NumberSchema; use Utopia\OpenAPI\Model\ObjectSchema; @@ -141,6 +142,21 @@ public function keepsOpenEnumType(): bool return false; } + /** + * Enum schema used in generated signatures. Open string enums are untyped + * except in languages that keep the documented enum as a type. + */ + protected function getTypedEnumSchema(Schema|Parameter $value): ?Schema + { + if ($this->isOpenStringEnum($value) && !$this->keepsOpenEnumType()) { + return null; + } + + $enumSchema = $this->getEnumSchema($value); + + return $enumSchema->enum === [] ? null : $enumSchema; + } + /** * Language specific functions. */ @@ -272,7 +288,7 @@ protected function getSchemaType(Schema|Parameter $value): string return match (true) { $schema instanceof StringSchema && $schema->format === 'binary' => self::TYPE_FILE, $schema instanceof StringSchema, - $schema instanceof CompositeSchema && $this->isOpenStringEnum($schema) => self::TYPE_STRING, + $schema instanceof CompositeSchema && $this->getEnumSchema($schema) instanceof StringSchema => self::TYPE_STRING, $schema instanceof IntegerSchema => self::TYPE_INTEGER, $schema instanceof NumberSchema => self::TYPE_NUMBER, $schema instanceof BooleanSchema => self::TYPE_BOOLEAN, @@ -367,9 +383,66 @@ public function getEnumSchema(Schema|Parameter $value): Schema $schema = $this->getSchema($value); $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - return $enumSchema instanceof CompositeSchema - ? ($enumSchema->openStringEnumBranch() ?? $enumSchema) - : $enumSchema; + if (!$enumSchema instanceof CompositeSchema) { + return $enumSchema; + } + + return $enumSchema->openStringEnumBranch() + ?? $this->closedStringEnumFromComposite($enumSchema) + ?? $enumSchema; + } + + /** + * Live specs encode closed string enums as type:string plus oneOf of + * single-value string schemas, which is not an open string enum. + */ + protected function closedStringEnumFromComposite(CompositeSchema $schema): ?StringSchema + { + if ( + ($schema->composition !== Composition::ONE_OF && $schema->composition !== Composition::ANY_OF) + || $schema->not instanceof Schema + ) { + return null; + } + + $values = []; + $keys = []; + foreach ($schema->schemas as $member) { + if (!$member instanceof StringSchema || $member->enum === []) { + return null; + } + foreach ($member->enum as $index => $value) { + if (\in_array($value, $values, true)) { + continue; + } + $values[] = $value; + $key = $member->enumKeys[$index] ?? null; + $keys[] = \is_string($key) && $key !== '' + ? $key + : (string) ($member->title ?? $value); + } + } + + if ($values === []) { + return null; + } + + return new StringSchema( + title: $schema->title, + description: $schema->description, + nullable: $schema->nullable, + default: $schema->default, + enum: $values, + format: $schema->format, + readOnly: $schema->readOnly, + writeOnly: $schema->writeOnly, + deprecated: $schema->deprecated, + example: $schema->example, + extensions: $schema->extensions, + enumName: $schema->title, + enumKeys: $keys, + open: false, + ); } public function isOpenStringEnum(Schema|Parameter $value): bool diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index 3c6c472b98..caa8bf5e3e 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -166,8 +166,7 @@ public function getArrayOf(string $elements): string public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = null): string { $schema = $this->getSchema($parameter); - $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - if ($enumSchema->enum !== []) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema) { $type = 'enums.' . $this->toPascalCase($this->getSchemaEnumName($parameter, $spec)); return $schema instanceof ArraySchema ? 'List<' . $type . '>' : $type; } diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index 29a67bd737..fa62dec398 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -193,8 +193,7 @@ public function getPropertyOverrides(): array public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = null): string { $schema = $this->getSchema($parameter); - $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - if ($enumSchema->enum !== []) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema) { $type = 'Appwrite.Enums.' . $this->toPascalCase($this->getSchemaEnumName($parameter, $spec)); return $schema instanceof ArraySchema ? 'List<' . $type . '>' : $type; } diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index a9570caa0b..da48017785 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -117,8 +117,7 @@ public function getArrayOf(string $elements): string public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = null): string { $schema = $this->getSchema($parameter); - $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - if ($enumSchema->enum !== []) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema) { $type = 'io.appwrite.enums.' . $this->toPascalCase($this->getSchemaEnumName($parameter, $spec)); return $schema instanceof ArraySchema ? 'List<' . $type . '>' : $type; } diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index f889223604..34cba12c02 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -338,7 +338,7 @@ public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = if ($schema instanceof ArraySchema) { return 'array'; } - if ($schema->enum !== []) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema) { return $this->applyIdentifierOverride($this->toPascalCase($this->getSchemaEnumName($parameter, $spec))); } @@ -551,8 +551,9 @@ protected function getMockPropertyValue(string $name, Schema $property, Specific } return $this->getMockDefinitionPayload($model, $spec, $indentLevel); } - if ($property->enum !== []) { - return '"' . $this->escapePhpString((string) $property->enum[0]) . '"'; + $enumSchema = $this->getTypedEnumSchema($property); + if ($enumSchema instanceof Schema) { + return '"' . $this->escapePhpString((string) $enumSchema->enum[0]) . '"'; } $example = $this->getSchemaExample($property); diff --git a/src/SDK/Language/Python.php b/src/SDK/Language/Python.php index 79134a12da..37f0889d32 100644 --- a/src/SDK/Language/Python.php +++ b/src/SDK/Language/Python.php @@ -353,8 +353,7 @@ public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = protected function getBaseTypeName(Schema|Parameter $parameter, ?Specification $spec = null): string { $schema = $this->getSchema($parameter); - $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - if ($enumSchema->enum !== []) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema) { $typeName = $this->toPascalCase($this->getSchemaEnumName($parameter, $spec)); if ($schema instanceof ArraySchema) { $typeName = 'List[' . $typeName . ']'; diff --git a/src/SDK/Language/Ruby.php b/src/SDK/Language/Ruby.php index e21d9a465f..f684606548 100644 --- a/src/SDK/Language/Ruby.php +++ b/src/SDK/Language/Ruby.php @@ -217,8 +217,7 @@ public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = // An array of enum members is still just an Array to Ruby; only a // scalar enum names its generated class. - $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - if ($enumSchema->enum !== [] && $type !== self::TYPE_ARRAY) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema && $type !== self::TYPE_ARRAY) { return \ucfirst($this->getSchemaEnumName($parameter, $spec)); } @@ -323,7 +322,8 @@ public function getParamExample(Schema|Parameter $param, string $lang = ''): str $output .= $this->isPermissionString($example) ? $this->getPermissionExample($example) : $example; break; case self::TYPE_OBJECT: - $output .= $this->jsonToHash(json_decode((string) $example, true)); + $decoded = \is_array($example) ? $example : json_decode((string) $example, true); + $output .= $this->jsonToHash(\is_array($decoded) ? $decoded : []); break; case self::TYPE_BOOLEAN: $output .= ($example) ? 'true' : 'false'; diff --git a/src/SDK/Language/Rust.php b/src/SDK/Language/Rust.php index db3744dd5a..b363159def 100644 --- a/src/SDK/Language/Rust.php +++ b/src/SDK/Language/Rust.php @@ -275,8 +275,7 @@ public function getFiles(): array public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = null): string { $schema = $this->getSchema($parameter); - $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - if ($enumSchema->enum !== []) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema) { $type = 'crate::enums::' . $this->toPascalCase($this->getSchemaEnumName($parameter, $spec)); return $schema instanceof ArraySchema ? 'Vec<' . $type . '>' : $type; } diff --git a/src/SDK/Language/Swift.php b/src/SDK/Language/Swift.php index 643fa8fdd5..9951fdb15e 100644 --- a/src/SDK/Language/Swift.php +++ b/src/SDK/Language/Swift.php @@ -323,8 +323,7 @@ public function getTypeName(Schema|Parameter $parameter, ?Specification $spec = $schema = $this->getSchema($parameter); $items = $this->getArraySchema($parameter); $prefix = $spec?->info->title ?? ''; - $enumSchema = $schema instanceof ArraySchema ? $schema->items : $schema; - if ($enumSchema->enum !== []) { + if ($this->getTypedEnumSchema($parameter) instanceof Schema) { $type = $prefix . 'Enums.' . $this->toPascalCase($this->getSchemaEnumName($parameter, $spec)); return $schema instanceof ArraySchema ? '[' . $type . ']' : $type; } diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index cdd49032a9..a19e5b8cbb 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -1549,7 +1549,7 @@ protected function getSchemaType(Schema|Parameter $value): string return match (true) { $schema instanceof StringSchema && $schema->format === 'binary' => 'file', $schema instanceof StringSchema, - $schema instanceof CompositeSchema && $this->language->isOpenStringEnum($schema) => 'string', + $schema instanceof CompositeSchema && $this->language->getEnumSchema($schema) instanceof StringSchema => 'string', $schema instanceof IntegerSchema => 'integer', $schema instanceof NumberSchema => 'number', $schema instanceof BooleanSchema => 'boolean', @@ -1560,7 +1560,7 @@ protected function getSchemaType(Schema|Parameter $value): string protected function getSchemaName(Schema $schema): string { - return $schema->title ?? $this->schemaNames[\spl_object_id($schema)] ?? ''; + return $this->schemaNames[\spl_object_id($schema)] ?? $schema->title ?? ''; } protected function getSchemaModel(Schema|Parameter $value): string @@ -1634,6 +1634,7 @@ protected function getSchemaDependencies(Schema $schema): array protected function getEnumName(Schema|Parameter $value): string { + $original = $this->getSchema($value); $enumSchema = $this->language->getEnumSchema($value); if ($enumSchema->enum === []) { return ''; @@ -1641,6 +1642,7 @@ protected function getEnumName(Schema|Parameter $value): string return (string) (($enumSchema instanceof StringSchema ? $enumSchema->enumName : null) ?? $enumSchema->title ?? $this->schemaEnumNames[\spl_object_id($enumSchema)] + ?? $this->schemaEnumNames[\spl_object_id($original)] ?? ($value instanceof Parameter ? $value->name : '')); } diff --git a/templates/apple/Package.swift.twig b/templates/apple/Package.swift.twig index 710507d2e2..0462a21aa6 100644 --- a/templates/apple/Package.swift.twig +++ b/templates/apple/Package.swift.twig @@ -15,8 +15,12 @@ let package = Package( name: "{{spec.info.title | caseUcfirst}}", targets: [ "{{spec.info.title | caseUcfirst}}", + {%~ if allEnums is not empty %} "{{spec.info.title | caseUcfirst}}Enums", + {%~ endif %} + {%~ if definitions is not empty %} "{{spec.info.title | caseUcfirst}}Models", + {%~ endif %} "JSONCodable" ] ), diff --git a/templates/dart/lib/src/client_mixin.dart.twig b/templates/dart/lib/src/client_mixin.dart.twig index e1269b1a1f..2b24e9676c 100644 --- a/templates/dart/lib/src/client_mixin.dart.twig +++ b/templates/dart/lib/src/client_mixin.dart.twig @@ -12,7 +12,9 @@ mixin ClientMixin { required Map headers, required Map params, }) { - params = params.map((key, value) => MapEntry(key, _flattenValue(value))); + params = { + for (final entry in params.entries) entry.key: _flattenValue(entry.value), + }; http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { diff --git a/templates/dart/test/src/models/model_test.dart.twig b/templates/dart/test/src/models/model_test.dart.twig index c4d99cb706..f58adf62eb 100644 --- a/templates/dart/test/src/models/model_test.dart.twig +++ b/templates/dart/test/src/models/model_test.dart.twig @@ -1,5 +1,5 @@ {% macro sub_schema(definitions, property, indent = ' ', closeIndent = ' ') %}{% if (property | schemaModel) %}{% if (property | schemaType) == 'array' %}List<{{ (property | schemaModel) | caseUcfirst | overrideIdentifier }}>{% else %}{% set modelDefinition = definitions[(property | schemaModel)] ?? null %}{% set requiredProperties = modelDefinition ? modelDefinition.properties | filter(p => p | schemaRequired) : [] %}{{ (property | schemaModel) | caseUcfirst | overrideIdentifier }}({% if requiredProperties|length == 0 %}){% else %}{{ "\n" -}} -{% for property in requiredProperties %}{{ indent }}{{ (property | schemaName) | escapeKeyword }}: {% if (property | schemaType) == 'array' %}[]{% elseif (property | schemaType) == 'object' and ((property | schemaModel) == 'prefs' or (property | schemaModel) == 'preferences') %}Preferences(data: {}){% elseif (property | schemaType) == 'object' and (property | schemaModel) %}{{ _self.sub_schema(definitions, property, indent ~ ' ', indent) }}{% elseif (property | enumValues) is not empty %}{{ (property | enumName) | caseUcfirst }}.{{ ((property | enumKeys)[0] ?? (property | enumValues)[0]) | caseEnumKey }}{% elseif (property | schemaType) == 'string' %}{{ (property | schemaExample) | dartString }}{% elseif (property | schemaType) == 'boolean' %}true{% else %}{{ property | schemaExample }}{% endif %}, +{% for property in requiredProperties %}{{ indent }}{{ (property | schemaName) | escapeKeyword }}: {% if (property | schemaType) == 'array' %}[]{% elseif (property | schemaType) == 'object' and ((property | schemaModel) == 'prefs' or (property | schemaModel) == 'preferences') %}Preferences(data: {}){% elseif (property | schemaType) == 'object' and (property | schemaModel) %}{{ _self.sub_schema(definitions, property, indent ~ ' ', indent) }}{% elseif (property | schemaType) == 'object' %}{}{% elseif (property | enumValues) is not empty %}{{ (property | enumName) | caseUcfirst }}.{{ ((property | enumKeys)[0] ?? (property | enumValues)[0]) | caseEnumKey }}{% elseif (property | schemaType) == 'string' %}{{ (property | schemaExample) | dartString }}{% elseif (property | schemaType) == 'boolean' %}true{% else %}{{ property | schemaExample }}{% endif %}, {% endfor %}{{ closeIndent }}){% endif %}{% endif %}{% else %}{% if (property | schemaType) == 'object' and property.additionalProperties %}Map{% else %}{{ property | typeName }}{% endif %}{% endif %}{% endmacro %} import 'package:{{ language.params.packageName }}/models.dart'; {% if (definition.properties | filter(p => (p | enumValues) is not empty and (p | schemaType) != 'array') | length > 0) or (definition.properties | filter(p => (p | schemaRequired) and (p | schemaModel) and definitions[(p | schemaModel)] and (definitions[(p | schemaModel)].properties | filter(sp => (sp | schemaRequired) and (sp | enumValues) is not empty and (sp | schemaType) != 'array') | length > 0)) | length > 0) %} diff --git a/templates/flutter/lib/src/client_mixin.dart.twig b/templates/flutter/lib/src/client_mixin.dart.twig index 5562aac126..50ef16dd3e 100644 --- a/templates/flutter/lib/src/client_mixin.dart.twig +++ b/templates/flutter/lib/src/client_mixin.dart.twig @@ -12,7 +12,9 @@ mixin ClientMixin { required Map headers, required Map params, }) { - params = params.map((key, value) => MapEntry(key, _flattenValue(value))); + params = { + for (final entry in params.entries) entry.key: _flattenValue(entry.value), + }; http.BaseRequest request = http.Request(method.name(), uri); if (headers['content-type'] == 'multipart/form-data') { diff --git a/templates/swift/Package.swift.twig b/templates/swift/Package.swift.twig index 710507d2e2..0462a21aa6 100644 --- a/templates/swift/Package.swift.twig +++ b/templates/swift/Package.swift.twig @@ -15,8 +15,12 @@ let package = Package( name: "{{spec.info.title | caseUcfirst}}", targets: [ "{{spec.info.title | caseUcfirst}}", + {%~ if allEnums is not empty %} "{{spec.info.title | caseUcfirst}}Enums", + {%~ endif %} + {%~ if definitions is not empty %} "{{spec.info.title | caseUcfirst}}Models", + {%~ endif %} "JSONCodable" ] ), diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 8e1c0e2960..21f3ab3c73 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -508,6 +508,7 @@ public function testHTTPSuccess(): void } $this->assertOpenEnumsAllowAnyString(); + $this->assertClosedOneOfStringEnumsAreEnums(); $sdk = new SDK($this->getLanguage(), Parser::parse($spec)); @@ -550,6 +551,7 @@ public function testHTTPSuccess(): void $sdk->generate(__DIR__ . '/sdks/' . $this->language); $this->assertOpenEnumSuggestionsGenerated($dir); + $this->assertOneOfStringEnumsUseWireNames($dir); $this->assertExcludedFixtureWasRemoved($dir); $this->assertCommentsAreNotHtmlEscaped($dir); @@ -608,6 +610,96 @@ public function testHTTPSuccess(): void } } + private function assertClosedOneOfStringEnumsAreEnums(): void + { + $specification = Parser::parse([ + 'openapi' => '3.0.0', + 'info' => ['title' => 'test', 'version' => '1.0.0'], + 'paths' => ['/items/{kind}' => ['get' => [ + 'operationId' => 'getItem', + 'parameters' => [[ + 'name' => 'kind', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'string', + 'example' => 'alpha', + 'title' => 'MockKind', + 'oneOf' => [ + ['type' => 'string', 'enum' => ['alpha'], 'title' => 'alpha'], + ['type' => 'string', 'enum' => ['beta'], 'title' => 'beta'], + ], + ], + ]], + 'responses' => ['200' => ['description' => 'ok']], + ]]], + ]); + $language = $this->getLanguage(); + $parameter = $specification->paths['/items/{kind}']->operations['get']->parameters[0]; + $plainString = Parser::parse([ + 'openapi' => '3.0.0', + 'info' => ['title' => 'test', 'version' => '1.0.0'], + 'paths' => ['/test' => ['get' => [ + 'operationId' => 'test', + 'parameters' => [ + ['name' => 'plainScalar', 'in' => 'query', 'schema' => ['type' => 'string']], + ['name' => 'plainObject', 'in' => 'query', 'schema' => ['type' => 'object']], + ], + 'responses' => ['200' => ['description' => 'ok']], + ]]], + ])->paths['/test']->operations['get']->parameters; + [$plainScalar, $plainObject] = $plainString; + + $this->assertNotSame( + $language->getTypeName($plainObject, $specification), + $language->getTypeName($parameter, $specification), + 'A oneOf of string enums must not type as a generic object.' + ); + $this->assertNotSame( + $language->getTypeName($plainScalar, $specification), + $language->getTypeName($plainObject, $specification) + ); + $enumSchema = $language->getEnumSchema($parameter); + $this->assertSame(['alpha', 'beta'], $enumSchema->enum); + } + + private function assertOneOfStringEnumsUseWireNames(string $dir): void + { + $expectations = [ + 'php' => [ + $dir . '/src/Appwrite/Models/Mock.php', + "array_key_exists('kind'", + "array_key_exists('MockKind'", + ], + 'python' => [ + $dir . '/appwrite/models/mock.py', + "alias='kind'", + "alias='MockKind'", + ], + 'kotlin' => [ + $dir . '/src/main/kotlin/io/appwrite/models/Mock.kt', + '@SerializedName("kind")', + '@SerializedName("MockKind")', + ], + 'android' => [ + $dir . '/library/src/main/java/io/appwrite/models/Mock.kt', + '@SerializedName("kind")', + '@SerializedName("MockKind")', + ], + ]; + + if (!isset($expectations[$this->language])) { + return; + } + + [$modelPath, $wireName, $titleName] = $expectations[$this->language]; + $this->assertFileExists($modelPath); + $contents = file_get_contents($modelPath); + $this->assertIsString($contents); + $this->assertStringContainsString($wireName, $contents); + $this->assertStringNotContainsString($titleName, $contents); + } + private function assertOpenEnumsAllowAnyString(): void { $enumBranch = [ diff --git a/tests/resources/spec-openapi3.json b/tests/resources/spec-openapi3.json index 88de466914..e61b9e3cc3 100644 --- a/tests/resources/spec-openapi3.json +++ b/tests/resources/spec-openapi3.json @@ -1597,6 +1597,28 @@ "type": "string" } ] + }, + "kind": { + "description": "Mock kind encoded as a oneOf of string enums.", + "example": "alpha", + "title": "MockKind", + "type": "string", + "oneOf": [ + { + "type": "string", + "enum": [ + "alpha" + ], + "title": "alpha" + }, + { + "type": "string", + "enum": [ + "beta" + ], + "title": "beta" + } + ] } }, "required": [ @@ -2666,6 +2688,28 @@ ], "x-enum-name": "MockStatus" }, + "kind": { + "description": "Mock kind encoded as a oneOf of string enums.", + "example": "alpha", + "title": "MockKind", + "type": "string", + "oneOf": [ + { + "type": "string", + "enum": [ + "alpha" + ], + "title": "alpha" + }, + { + "type": "string", + "enum": [ + "beta" + ], + "title": "beta" + } + ] + }, "relatedMock": { "type": "object", "description": "Optional nested mock.", From 4d81290e4afcde779f597517657ca5b0162efe80 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 19:50:29 +1200 Subject: [PATCH 08/42] fix: name untitled oneOf enums from their parent property Collapsed oneOf string enums with no title produced empty type names (`val status: ,`, `resourceType: ;`) and invalid PHP properties. --- src/SDK/Language.php | 10 ++++++++-- tests/e2e/Base.php | 36 +++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/SDK/Language.php b/src/SDK/Language.php index 0ec47009ea..10cff811b9 100644 --- a/src/SDK/Language.php +++ b/src/SDK/Language.php @@ -462,17 +462,23 @@ protected function getSchemaEnumName(Schema|Parameter $value, ?Specification $sp return $name; } + $source = $this->getSchema($value); + $source = $source instanceof ArraySchema ? $source->items : $source; + foreach ($spec?->schemas ?? [] as $modelName => $model) { if (!$model instanceof ObjectSchema) { continue; } foreach ($model->properties as $propertyName => $property) { - if ($this->getEnumSchema($property) === $enumSchema) { + $propertySource = $this->getSchema($property); + $propertySource = $propertySource instanceof ArraySchema ? $propertySource->items : $propertySource; + if ($propertySource === $source) { return \ucfirst($modelName) . \ucfirst($propertyName); } } } - return ''; + + return $value instanceof Parameter ? $value->name : ''; } protected function getSpecificationSchemaName(Schema $schema, Specification $spec): string diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 21f3ab3c73..3a349feac0 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -612,6 +612,14 @@ public function testHTTPSuccess(): void private function assertClosedOneOfStringEnumsAreEnums(): void { + $oneOfKind = [ + 'type' => 'string', + 'example' => 'alpha', + 'oneOf' => [ + ['type' => 'string', 'enum' => ['alpha'], 'title' => 'alpha'], + ['type' => 'string', 'enum' => ['beta'], 'title' => 'beta'], + ], + ]; $specification = Parser::parse([ 'openapi' => '3.0.0', 'info' => ['title' => 'test', 'version' => '1.0.0'], @@ -621,18 +629,19 @@ private function assertClosedOneOfStringEnumsAreEnums(): void 'name' => 'kind', 'in' => 'path', 'required' => true, - 'schema' => [ - 'type' => 'string', - 'example' => 'alpha', - 'title' => 'MockKind', - 'oneOf' => [ - ['type' => 'string', 'enum' => ['alpha'], 'title' => 'alpha'], - ['type' => 'string', 'enum' => ['beta'], 'title' => 'beta'], - ], - ], + 'schema' => $oneOfKind + ['title' => 'MockKind'], ]], 'responses' => ['200' => ['description' => 'ok']], ]]], + 'components' => ['schemas' => [ + 'widget' => [ + 'type' => 'object', + 'required' => ['kind'], + 'properties' => [ + 'kind' => $oneOfKind, + ], + ], + ]], ]); $language = $this->getLanguage(); $parameter = $specification->paths['/items/{kind}']->operations['get']->parameters[0]; @@ -661,6 +670,15 @@ private function assertClosedOneOfStringEnumsAreEnums(): void ); $enumSchema = $language->getEnumSchema($parameter); $this->assertSame(['alpha', 'beta'], $enumSchema->enum); + + $untitled = $specification->schemas['widget']->properties['kind']; + $untitledType = $language->getTypeName($untitled, $specification); + $this->assertNotSame('', $untitledType); + $this->assertNotSame( + $language->getTypeName($plainObject, $specification), + $untitledType, + 'An untitled oneOf string enum must still produce a concrete type name.' + ); } private function assertOneOfStringEnumsUseWireNames(string $dir): void From f8897cda4a2886c9c4a904936a33a4d96038f7df Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 20:10:18 +1200 Subject: [PATCH 09/42] refactor: nest query builders as Query.Builder Move QueryBuilder into Query/Builder files and nested types where the language allows it. Alias QueryBuilder at import for PHP and the TypeScript public export. --- src/SDK/Language/Apple.php | 5 + src/SDK/Language/Dart.php | 5 + src/SDK/Language/DotNet.php | 5 + src/SDK/Language/Flutter.php | 5 + src/SDK/Language/Go.php | 5 + src/SDK/Language/PHP.php | 4 +- src/SDK/Language/Ruby.php | 5 + src/SDK/Language/Rust.php | 9 +- src/SDK/Language/Swift.php | 5 + .../src/main/java/io/package/Query.kt.twig | 200 +++---- templates/dart/lib/package.dart.twig | 1 + templates/dart/lib/query.dart.twig | 304 +---------- templates/dart/lib/query/builder.dart.twig | 303 +++++++++++ templates/deno/src/query.ts.twig | 19 +- templates/dotnet/Package/Query.cs.twig | 508 +----------------- .../dotnet/Package/Query/Builder.cs.twig | 507 +++++++++++++++++ templates/flutter/lib/package.dart.twig | 1 + templates/go/client.go.twig | 6 +- templates/go/query.go.twig | 371 +------------ templates/go/query/builder.go.twig | 368 +++++++++++++ templates/go/query_test.go.twig | 8 +- .../src/main/kotlin/io/appwrite/Query.kt.twig | 200 +++---- templates/php/src/Client.php.twig | 2 + templates/php/src/Query.php.twig | 2 + .../Builder.php.twig} | 6 +- templates/python/package/query.py.twig | 360 ++++++------- templates/react-native/src/query.ts.twig | 19 +- templates/ruby/lib/container/query.rb.twig | 360 +------------ .../ruby/lib/container/query/builder.rb.twig | 357 ++++++++++++ templates/rust/src/query/builder.rs.twig | 501 +++++++++++++++++ .../src/{query.rs.twig => query/mod.rs.twig} | 504 +---------------- templates/skills/dotnet.md.twig | 4 +- templates/skills/go.md.twig | 2 +- templates/swift/Sources/Query.swift.twig | 505 +---------------- .../swift/Sources/Query/Builder.swift.twig | 501 +++++++++++++++++ templates/web/src/query.ts.twig | 19 +- tests/e2e/languages/dotnet/Tests.cs | 2 +- tests/e2e/languages/php/test.php | 2 +- tests/e2e/languages/unity/Tests.cs | 2 +- 39 files changed, 3039 insertions(+), 2953 deletions(-) create mode 100644 templates/dart/lib/query/builder.dart.twig create mode 100644 templates/dotnet/Package/Query/Builder.cs.twig create mode 100644 templates/go/query/builder.go.twig rename templates/php/src/{QueryBuilder.php.twig => Query/Builder.php.twig} (99%) create mode 100644 templates/ruby/lib/container/query/builder.rb.twig create mode 100644 templates/rust/src/query/builder.rs.twig rename templates/rust/src/{query.rs.twig => query/mod.rs.twig} (75%) create mode 100644 templates/swift/Sources/Query/Builder.swift.twig diff --git a/src/SDK/Language/Apple.php b/src/SDK/Language/Apple.php index d698bc1504..4b5ffa3da2 100644 --- a/src/SDK/Language/Apple.php +++ b/src/SDK/Language/Apple.php @@ -85,6 +85,11 @@ public function getFiles(): array 'destination' => '/Sources/{{ spec.info.title | caseUcfirst}}/Query.swift', 'template' => 'swift/Sources/Query.swift.twig', ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.info.title | caseUcfirst}}/Query/Builder.swift', + 'template' => 'swift/Sources/Query/Builder.swift.twig', + ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.info.title | caseUcfirst}}/Operator.swift', diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index caa8bf5e3e..c77b5499c2 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -382,6 +382,11 @@ public function getFiles(): array 'destination' => '/lib/query.dart', 'template' => 'dart/lib/query.dart.twig', ], + [ + 'scope' => 'default', + 'destination' => '/lib/query/builder.dart', + 'template' => 'dart/lib/query/builder.dart.twig', + ], [ 'scope' => 'default', 'destination' => '/lib/operator.dart', diff --git a/src/SDK/Language/DotNet.php b/src/SDK/Language/DotNet.php index fa62dec398..380044290a 100644 --- a/src/SDK/Language/DotNet.php +++ b/src/SDK/Language/DotNet.php @@ -411,6 +411,11 @@ public function getFiles(): array 'destination' => '{{ spec.info.title | caseUcfirst }}/Query.cs', 'template' => 'dotnet/Package/Query.cs.twig', ], + [ + 'scope' => 'default', + 'destination' => '{{ spec.info.title | caseUcfirst }}/Query/Builder.cs', + 'template' => 'dotnet/Package/Query/Builder.cs.twig', + ], [ 'scope' => 'default', 'destination' => '{{ spec.info.title | caseUcfirst }}/Operator.cs', diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index 94942c9c48..87819bc463 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -91,6 +91,11 @@ public function getFiles(): array 'destination' => '/lib/query.dart', 'template' => 'dart/lib/query.dart.twig', ], + [ + 'scope' => 'default', + 'destination' => '/lib/query/builder.dart', + 'template' => 'dart/lib/query/builder.dart.twig', + ], [ 'scope' => 'default', 'destination' => '/lib/operator.dart', diff --git a/src/SDK/Language/Go.php b/src/SDK/Language/Go.php index f7c7c51103..b2f33c4559 100644 --- a/src/SDK/Language/Go.php +++ b/src/SDK/Language/Go.php @@ -108,6 +108,11 @@ public function getFiles(): array 'destination' => 'query/query.go', 'template' => 'go/query.go.twig', ], + [ + 'scope' => 'default', + 'destination' => 'query/builder.go', + 'template' => 'go/query/builder.go.twig', + ], [ 'scope' => 'default', 'destination' => 'operator/operator.go', diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 34cba12c02..ac7b82becf 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -239,8 +239,8 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'src/{{ namespace | caseNamespacePath }}/QueryBuilder.php', - 'template' => 'php/src/QueryBuilder.php.twig', + 'destination' => 'src/{{ namespace | caseNamespacePath }}/Query/Builder.php', + 'template' => 'php/src/Query/Builder.php.twig', ], [ 'scope' => 'default', diff --git a/src/SDK/Language/Ruby.php b/src/SDK/Language/Ruby.php index f684606548..31db7bf8db 100644 --- a/src/SDK/Language/Ruby.php +++ b/src/SDK/Language/Ruby.php @@ -157,6 +157,11 @@ public function getFiles(): array 'destination' => 'lib/{{ spec.info.title | caseDash }}/query.rb', 'template' => 'ruby/lib/container/query.rb.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/{{ spec.info.title | caseDash }}/query/builder.rb', + 'template' => 'ruby/lib/container/query/builder.rb.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/{{ spec.info.title | caseDash }}/operator.rb', diff --git a/src/SDK/Language/Rust.php b/src/SDK/Language/Rust.php index b363159def..64f9870e0f 100644 --- a/src/SDK/Language/Rust.php +++ b/src/SDK/Language/Rust.php @@ -190,8 +190,13 @@ public function getFiles(): array ], [ "scope" => "default", - "destination" => "src/query.rs", - "template" => "rust/src/query.rs.twig", + "destination" => "src/query/mod.rs", + "template" => "rust/src/query/mod.rs.twig", + ], + [ + "scope" => "default", + "destination" => "src/query/builder.rs", + "template" => "rust/src/query/builder.rs.twig", ], [ "scope" => "default", diff --git a/src/SDK/Language/Swift.php b/src/SDK/Language/Swift.php index 9951fdb15e..e327a94774 100644 --- a/src/SDK/Language/Swift.php +++ b/src/SDK/Language/Swift.php @@ -185,6 +185,11 @@ public function getFiles(): array 'destination' => '/Sources/{{ spec.info.title | caseUcfirst}}/Query.swift', 'template' => 'swift/Sources/Query.swift.twig', ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.info.title | caseUcfirst}}/Query/Builder.swift', + 'template' => 'swift/Sources/Query/Builder.swift.twig', + ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.info.title | caseUcfirst}}/Operator.swift', 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 808222fbea..ee827e8380 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 @@ -867,10 +867,10 @@ class Query( * * @returns A new query builder. */ - fun builder() = QueryBuilder() + fun builder() = Builder() internal fun flatten(value: Any?): Any? { - if (value is QueryBuilder) { + if (value is Builder) { return value.build() } if (value is Query) { @@ -887,7 +887,7 @@ class Query( var changed = false for (item in items) { val flat = flatten(item) - if (item is QueryBuilder || item is Query) { + if (item is Builder || item is Query) { if (flat is Collection<*>) { out.addAll(flat) } else { @@ -936,196 +936,196 @@ class Query( } } } -} -class QueryBuilder { - private val queries = mutableListOf() + class Builder { + private val queries = mutableListOf() - fun build(): List = queries.toList() + fun build(): List = queries.toList() - fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) + fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) - fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) + fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) - fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) + fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) - fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) + fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) - fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) + fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) - fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) + fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) - fun search(attribute: String, value: String) = push(Query.search(attribute, value)) + fun search(attribute: String, value: String) = push(Query.search(attribute, value)) - fun isNull(attribute: String) = push(Query.isNull(attribute)) + fun isNull(attribute: String) = push(Query.isNull(attribute)) - fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) + fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) - fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) + fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) - fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) + fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) - fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) + fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) - fun select(attributes: List) = push(Query.select(attributes)) + fun select(attributes: List) = push(Query.select(attributes)) - fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) + fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) - fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) + fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) - fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) + fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) - fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) + fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) - fun orderRandom() = push(Query.orderRandom()) + fun orderRandom() = push(Query.orderRandom()) - fun limit(limit: Int) = push(Query.limit(limit)) + fun limit(limit: Int) = push(Query.limit(limit)) - fun offset(offset: Int) = push(Query.offset(offset)) + fun offset(offset: Int) = push(Query.offset(offset)) - fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) + fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) - fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) + fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) - fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) + fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) - fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) + fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) - fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) + fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) - fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) + fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) - fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) + fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) - fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) + fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) - fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) + fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) - fun exists(attributes: List) = push(Query.exists(attributes)) + fun exists(attributes: List) = push(Query.exists(attributes)) - fun notExists(attributes: List) = push(Query.notExists(attributes)) + fun notExists(attributes: List) = push(Query.notExists(attributes)) - fun createdBefore(value: String) = push(Query.createdBefore(value)) + fun createdBefore(value: String) = push(Query.createdBefore(value)) - fun createdAfter(value: String) = push(Query.createdAfter(value)) + fun createdAfter(value: String) = push(Query.createdAfter(value)) - fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) + fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) - fun updatedBefore(value: String) = push(Query.updatedBefore(value)) + fun updatedBefore(value: String) = push(Query.updatedBefore(value)) - fun updatedAfter(value: String) = push(Query.updatedAfter(value)) + fun updatedAfter(value: String) = push(Query.updatedAfter(value)) - fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) + fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) - fun or(queries: List) = push(Query.or(queries)) + fun or(queries: List) = push(Query.or(queries)) - fun and(queries: List) = push(Query.and(queries)) + fun and(queries: List) = push(Query.and(queries)) - fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) + fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) - fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) + fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) - fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) + fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) - fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) + fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) - fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) + fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) - fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) + fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) - fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) + fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) - fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) + fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) - fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) + fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) - fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) + fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) - fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) + fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) - fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) + fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) - fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) + fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) - fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) + fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) - fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) + fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) - fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) + fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) - fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) + fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) - fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) + fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) - fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) + fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) - fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) + fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) - fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) + fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) - fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) + fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) - fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) + fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) - fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) + fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) - fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) + fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) - fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) + fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) - fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) + fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) - fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) + fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) - fun groupBy(attributes: List) = push(Query.groupBy(attributes)) + fun groupBy(attributes: List) = push(Query.groupBy(attributes)) - fun having(queries: List) = push(Query.having(queries)) + fun having(queries: List) = push(Query.having(queries)) - fun distinct() = push(Query.distinct()) + fun distinct() = push(Query.distinct()) - fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) - fun join(table: String, on: List) = push(Query.join(table, on)) + fun join(table: String, on: List) = push(Query.join(table, on)) - fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) + fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) - fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) - fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) + fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) - fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) + fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) - fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) - fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) + fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) - fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) + fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) - fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) - fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) + fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) - fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) + fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) - fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) - fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) + fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) - fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) + fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) - fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) + fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) - fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) + fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) - fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) + fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) - fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) + fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) - fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) + fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) - fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) + fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) - private fun push(query: String) = apply { queries.add(query) } + private fun push(query: String) = apply { queries.add(query) } - private fun push(query: List) = apply { queries.addAll(query) } + private fun push(query: List) = apply { queries.addAll(query) } + } } diff --git a/templates/dart/lib/package.dart.twig b/templates/dart/lib/package.dart.twig index 6054663361..bd4610a35e 100644 --- a/templates/dart/lib/package.dart.twig +++ b/templates/dart/lib/package.dart.twig @@ -24,6 +24,7 @@ export 'src/input_file.dart'; export 'src/upload_progress.dart'; part 'query.dart'; +part 'query/builder.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index e3b15b1a78..efcb0b873b 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -466,7 +466,7 @@ class Query { } /// Start a fluent query builder. - static QueryBuilder builder() => QueryBuilder(); + static Builder builder() => Builder(); static String _createJoin( String method, @@ -526,305 +526,3 @@ class Query { return Query._(method, table, values).toString(); } } - -/// Fluent collector of query strings. Call [build] to produce the list -/// expected by list methods. -class QueryBuilder { - final List _queries = []; - - List build() => List.from(_queries); - - QueryBuilder equal(String attribute, dynamic value) => - _push(Query.equal(attribute, value)); - - QueryBuilder notEqual(String attribute, dynamic value) => - _push(Query.notEqual(attribute, value)); - - QueryBuilder lessThan(String attribute, dynamic value) => - _push(Query.lessThan(attribute, value)); - - QueryBuilder lessThanEqual(String attribute, dynamic value) => - _push(Query.lessThanEqual(attribute, value)); - - QueryBuilder greaterThan(String attribute, dynamic value) => - _push(Query.greaterThan(attribute, value)); - - QueryBuilder greaterThanEqual(String attribute, dynamic value) => - _push(Query.greaterThanEqual(attribute, value)); - - QueryBuilder search(String attribute, String value) => - _push(Query.search(attribute, value)); - - QueryBuilder isNull(String attribute) => _push(Query.isNull(attribute)); - - QueryBuilder isNotNull(String attribute) => - _push(Query.isNotNull(attribute)); - - QueryBuilder between(String attribute, dynamic start, dynamic end) => - _push(Query.between(attribute, start, end)); - - QueryBuilder startsWith(String attribute, String value) => - _push(Query.startsWith(attribute, value)); - - QueryBuilder endsWith(String attribute, String value) => - _push(Query.endsWith(attribute, value)); - - QueryBuilder select(List attributes) => - _push(Query.select(attributes)); - - QueryBuilder cursorAfter(String documentId) => - _push(Query.cursorAfter(documentId)); - - QueryBuilder cursorBefore(String documentId) => - _push(Query.cursorBefore(documentId)); - - QueryBuilder orderAsc(String attribute) => - _push(Query.orderAsc(attribute)); - - QueryBuilder orderDesc(String attribute) => - _push(Query.orderDesc(attribute)); - - QueryBuilder orderRandom() => _push(Query.orderRandom()); - - QueryBuilder limit(int limit) => _push(Query.limit(limit)); - - QueryBuilder offset(int offset) => _push(Query.offset(offset)); - - QueryBuilder contains(String attribute, dynamic value) => - _push(Query.contains(attribute, value)); - - QueryBuilder containsAny(String attribute, List value) => - _push(Query.containsAny(attribute, value)); - - QueryBuilder containsAll(String attribute, List value) => - _push(Query.containsAll(attribute, value)); - - QueryBuilder notContains(String attribute, dynamic value) => - _push(Query.notContains(attribute, value)); - - QueryBuilder notSearch(String attribute, String value) => - _push(Query.notSearch(attribute, value)); - - QueryBuilder notBetween(String attribute, dynamic start, dynamic end) => - _push(Query.notBetween(attribute, start, end)); - - QueryBuilder notStartsWith(String attribute, String value) => - _push(Query.notStartsWith(attribute, value)); - - QueryBuilder notEndsWith(String attribute, String value) => - _push(Query.notEndsWith(attribute, value)); - - QueryBuilder regex(String attribute, String pattern) => - _push(Query.regex(attribute, pattern)); - - QueryBuilder exists(List attributes) => - _push(Query.exists(attributes)); - - QueryBuilder notExists(List attributes) => - _push(Query.notExists(attributes)); - - QueryBuilder createdBefore(String value) => - _push(Query.createdBefore(value)); - - QueryBuilder createdAfter(String value) => - _push(Query.createdAfter(value)); - - QueryBuilder createdBetween(String start, String end) => - _push(Query.createdBetween(start, end)); - - QueryBuilder updatedBefore(String value) => - _push(Query.updatedBefore(value)); - - QueryBuilder updatedAfter(String value) => - _push(Query.updatedAfter(value)); - - QueryBuilder updatedBetween(String start, String end) => - _push(Query.updatedBetween(start, end)); - - QueryBuilder or(List queries) => _push(Query.or(queries)); - - QueryBuilder and(List queries) => _push(Query.and(queries)); - - QueryBuilder elemMatch(String attribute, List queries) => - _push(Query.elemMatch(attribute, queries)); - - QueryBuilder distanceEqual( - String attribute, - List values, - num distance, [ - bool meters = true, - ]) => - _push(Query.distanceEqual(attribute, values, distance, meters)); - - QueryBuilder distanceNotEqual( - String attribute, - List values, - num distance, [ - bool meters = true, - ]) => - _push(Query.distanceNotEqual(attribute, values, distance, meters)); - - QueryBuilder distanceGreaterThan( - String attribute, - List values, - num distance, [ - bool meters = true, - ]) => - _push(Query.distanceGreaterThan(attribute, values, distance, meters)); - - QueryBuilder distanceLessThan( - String attribute, - List values, - num distance, [ - bool meters = true, - ]) => - _push(Query.distanceLessThan(attribute, values, distance, meters)); - - QueryBuilder intersects(String attribute, List values) => - _push(Query.intersects(attribute, values)); - - QueryBuilder notIntersects(String attribute, List values) => - _push(Query.notIntersects(attribute, values)); - - QueryBuilder crosses(String attribute, List values) => - _push(Query.crosses(attribute, values)); - - QueryBuilder notCrosses(String attribute, List values) => - _push(Query.notCrosses(attribute, values)); - - QueryBuilder overlaps(String attribute, List values) => - _push(Query.overlaps(attribute, values)); - - QueryBuilder notOverlaps(String attribute, List values) => - _push(Query.notOverlaps(attribute, values)); - - QueryBuilder touches(String attribute, List values) => - _push(Query.touches(attribute, values)); - - QueryBuilder notTouches(String attribute, List values) => - _push(Query.notTouches(attribute, values)); - - QueryBuilder count([String attribute = '*', String alias = '']) => - _push(Query.count(attribute, alias)); - - QueryBuilder countDistinct(String attribute, [String alias = '']) => - _push(Query.countDistinct(attribute, alias)); - - QueryBuilder sum(String attribute, [String alias = '']) => - _push(Query.sum(attribute, alias)); - - QueryBuilder avg(String attribute, [String alias = '']) => - _push(Query.avg(attribute, alias)); - - QueryBuilder min(String attribute, [String alias = '']) => - _push(Query.min(attribute, alias)); - - QueryBuilder max(String attribute, [String alias = '']) => - _push(Query.max(attribute, alias)); - - QueryBuilder stddev(String attribute, [String alias = '']) => - _push(Query.stddev(attribute, alias)); - - QueryBuilder stddevPop(String attribute, [String alias = '']) => - _push(Query.stddevPop(attribute, alias)); - - QueryBuilder stddevSamp(String attribute, [String alias = '']) => - _push(Query.stddevSamp(attribute, alias)); - - QueryBuilder variance(String attribute, [String alias = '']) => - _push(Query.variance(attribute, alias)); - - QueryBuilder varPop(String attribute, [String alias = '']) => - _push(Query.varPop(attribute, alias)); - - QueryBuilder varSamp(String attribute, [String alias = '']) => - _push(Query.varSamp(attribute, alias)); - - QueryBuilder bitAnd(String attribute, [String alias = '']) => - _push(Query.bitAnd(attribute, alias)); - - QueryBuilder bitOr(String attribute, [String alias = '']) => - _push(Query.bitOr(attribute, alias)); - - QueryBuilder bitXor(String attribute, [String alias = '']) => - _push(Query.bitXor(attribute, alias)); - - QueryBuilder groupBy(List attributes) => - _push(Query.groupBy(attributes)); - - QueryBuilder having(List queries) => _push(Query.having(queries)); - - QueryBuilder distinct() => _push(Query.distinct()); - - QueryBuilder join( - String table, - Object leftOrAliasOrOn, [ - Object rightOrOn = '', - String operator = '=', - String alias = '', - ]) => - _push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - - QueryBuilder leftJoin( - String table, - Object leftOrAliasOrOn, [ - Object rightOrOn = '', - String operator = '=', - String alias = '', - ]) => - _push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - - QueryBuilder rightJoin( - String table, - Object leftOrAliasOrOn, [ - Object rightOrOn = '', - String operator = '=', - String alias = '', - ]) => - _push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - - QueryBuilder fullOuterJoin( - String table, - Object leftOrAliasOrOn, [ - Object rightOrOn = '', - String operator = '=', - String alias = '', - ]) => - _push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - - QueryBuilder crossJoin(String table, [String alias = '']) => - _push(Query.crossJoin(table, alias)); - - QueryBuilder vectorDot(String attribute, List vector) => - _push(Query.vectorDot(attribute, vector)); - - QueryBuilder vectorCosine(String attribute, List vector) => - _push(Query.vectorCosine(attribute, vector)); - - QueryBuilder vectorEuclidean(String attribute, List vector) => - _push(Query.vectorEuclidean(attribute, vector)); - - QueryBuilder covers(String attribute, List values) => - _push(Query.covers(attribute, values)); - - QueryBuilder notCovers(String attribute, List values) => - _push(Query.notCovers(attribute, values)); - - QueryBuilder spatialEquals(String attribute, List values) => - _push(Query.spatialEquals(attribute, values)); - - QueryBuilder notSpatialEquals(String attribute, List values) => - _push(Query.notSpatialEquals(attribute, values)); - - QueryBuilder page(int page, [int perPage = 25]) => - _push(Query.page(page, perPage)); - - QueryBuilder _push(Object query) { - if (query is List) { - _queries.addAll(query.cast()); - } else { - _queries.add(query as String); - } - return this; - } -} diff --git a/templates/dart/lib/query/builder.dart.twig b/templates/dart/lib/query/builder.dart.twig new file mode 100644 index 0000000000..6613319ef6 --- /dev/null +++ b/templates/dart/lib/query/builder.dart.twig @@ -0,0 +1,303 @@ +part of '{{ language.params.packageName }}.dart'; + +/// Fluent collector of query strings. Call [build] to produce the list +/// expected by list methods. +class Builder { + final List _queries = []; + + List build() => List.from(_queries); + + Builder equal(String attribute, dynamic value) => + _push(Query.equal(attribute, value)); + + Builder notEqual(String attribute, dynamic value) => + _push(Query.notEqual(attribute, value)); + + Builder lessThan(String attribute, dynamic value) => + _push(Query.lessThan(attribute, value)); + + Builder lessThanEqual(String attribute, dynamic value) => + _push(Query.lessThanEqual(attribute, value)); + + Builder greaterThan(String attribute, dynamic value) => + _push(Query.greaterThan(attribute, value)); + + Builder greaterThanEqual(String attribute, dynamic value) => + _push(Query.greaterThanEqual(attribute, value)); + + Builder search(String attribute, String value) => + _push(Query.search(attribute, value)); + + Builder isNull(String attribute) => _push(Query.isNull(attribute)); + + Builder isNotNull(String attribute) => + _push(Query.isNotNull(attribute)); + + Builder between(String attribute, dynamic start, dynamic end) => + _push(Query.between(attribute, start, end)); + + Builder startsWith(String attribute, String value) => + _push(Query.startsWith(attribute, value)); + + Builder endsWith(String attribute, String value) => + _push(Query.endsWith(attribute, value)); + + Builder select(List attributes) => + _push(Query.select(attributes)); + + Builder cursorAfter(String documentId) => + _push(Query.cursorAfter(documentId)); + + Builder cursorBefore(String documentId) => + _push(Query.cursorBefore(documentId)); + + Builder orderAsc(String attribute) => + _push(Query.orderAsc(attribute)); + + Builder orderDesc(String attribute) => + _push(Query.orderDesc(attribute)); + + Builder orderRandom() => _push(Query.orderRandom()); + + Builder limit(int limit) => _push(Query.limit(limit)); + + Builder offset(int offset) => _push(Query.offset(offset)); + + Builder contains(String attribute, dynamic value) => + _push(Query.contains(attribute, value)); + + Builder containsAny(String attribute, List value) => + _push(Query.containsAny(attribute, value)); + + Builder containsAll(String attribute, List value) => + _push(Query.containsAll(attribute, value)); + + Builder notContains(String attribute, dynamic value) => + _push(Query.notContains(attribute, value)); + + Builder notSearch(String attribute, String value) => + _push(Query.notSearch(attribute, value)); + + Builder notBetween(String attribute, dynamic start, dynamic end) => + _push(Query.notBetween(attribute, start, end)); + + Builder notStartsWith(String attribute, String value) => + _push(Query.notStartsWith(attribute, value)); + + Builder notEndsWith(String attribute, String value) => + _push(Query.notEndsWith(attribute, value)); + + Builder regex(String attribute, String pattern) => + _push(Query.regex(attribute, pattern)); + + Builder exists(List attributes) => + _push(Query.exists(attributes)); + + Builder notExists(List attributes) => + _push(Query.notExists(attributes)); + + Builder createdBefore(String value) => + _push(Query.createdBefore(value)); + + Builder createdAfter(String value) => + _push(Query.createdAfter(value)); + + Builder createdBetween(String start, String end) => + _push(Query.createdBetween(start, end)); + + Builder updatedBefore(String value) => + _push(Query.updatedBefore(value)); + + Builder updatedAfter(String value) => + _push(Query.updatedAfter(value)); + + Builder updatedBetween(String start, String end) => + _push(Query.updatedBetween(start, end)); + + Builder or(List queries) => _push(Query.or(queries)); + + Builder and(List queries) => _push(Query.and(queries)); + + Builder elemMatch(String attribute, List queries) => + _push(Query.elemMatch(attribute, queries)); + + Builder distanceEqual( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceEqual(attribute, values, distance, meters)); + + Builder distanceNotEqual( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceNotEqual(attribute, values, distance, meters)); + + Builder distanceGreaterThan( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceGreaterThan(attribute, values, distance, meters)); + + Builder distanceLessThan( + String attribute, + List values, + num distance, [ + bool meters = true, + ]) => + _push(Query.distanceLessThan(attribute, values, distance, meters)); + + Builder intersects(String attribute, List values) => + _push(Query.intersects(attribute, values)); + + Builder notIntersects(String attribute, List values) => + _push(Query.notIntersects(attribute, values)); + + Builder crosses(String attribute, List values) => + _push(Query.crosses(attribute, values)); + + Builder notCrosses(String attribute, List values) => + _push(Query.notCrosses(attribute, values)); + + Builder overlaps(String attribute, List values) => + _push(Query.overlaps(attribute, values)); + + Builder notOverlaps(String attribute, List values) => + _push(Query.notOverlaps(attribute, values)); + + Builder touches(String attribute, List values) => + _push(Query.touches(attribute, values)); + + Builder notTouches(String attribute, List values) => + _push(Query.notTouches(attribute, values)); + + Builder count([String attribute = '*', String alias = '']) => + _push(Query.count(attribute, alias)); + + Builder countDistinct(String attribute, [String alias = '']) => + _push(Query.countDistinct(attribute, alias)); + + Builder sum(String attribute, [String alias = '']) => + _push(Query.sum(attribute, alias)); + + Builder avg(String attribute, [String alias = '']) => + _push(Query.avg(attribute, alias)); + + Builder min(String attribute, [String alias = '']) => + _push(Query.min(attribute, alias)); + + Builder max(String attribute, [String alias = '']) => + _push(Query.max(attribute, alias)); + + Builder stddev(String attribute, [String alias = '']) => + _push(Query.stddev(attribute, alias)); + + Builder stddevPop(String attribute, [String alias = '']) => + _push(Query.stddevPop(attribute, alias)); + + Builder stddevSamp(String attribute, [String alias = '']) => + _push(Query.stddevSamp(attribute, alias)); + + Builder variance(String attribute, [String alias = '']) => + _push(Query.variance(attribute, alias)); + + Builder varPop(String attribute, [String alias = '']) => + _push(Query.varPop(attribute, alias)); + + Builder varSamp(String attribute, [String alias = '']) => + _push(Query.varSamp(attribute, alias)); + + Builder bitAnd(String attribute, [String alias = '']) => + _push(Query.bitAnd(attribute, alias)); + + Builder bitOr(String attribute, [String alias = '']) => + _push(Query.bitOr(attribute, alias)); + + Builder bitXor(String attribute, [String alias = '']) => + _push(Query.bitXor(attribute, alias)); + + Builder groupBy(List attributes) => + _push(Query.groupBy(attributes)); + + Builder having(List queries) => _push(Query.having(queries)); + + Builder distinct() => _push(Query.distinct()); + + Builder join( + String table, + Object leftOrAliasOrOn, [ + Object rightOrOn = '', + String operator = '=', + String alias = '', + ]) => + _push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); + + Builder leftJoin( + String table, + Object leftOrAliasOrOn, [ + Object rightOrOn = '', + String operator = '=', + String alias = '', + ]) => + _push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); + + Builder rightJoin( + String table, + Object leftOrAliasOrOn, [ + Object rightOrOn = '', + String operator = '=', + String alias = '', + ]) => + _push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); + + Builder fullOuterJoin( + String table, + Object leftOrAliasOrOn, [ + Object rightOrOn = '', + String operator = '=', + String alias = '', + ]) => + _push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); + + Builder crossJoin(String table, [String alias = '']) => + _push(Query.crossJoin(table, alias)); + + Builder vectorDot(String attribute, List vector) => + _push(Query.vectorDot(attribute, vector)); + + Builder vectorCosine(String attribute, List vector) => + _push(Query.vectorCosine(attribute, vector)); + + Builder vectorEuclidean(String attribute, List vector) => + _push(Query.vectorEuclidean(attribute, vector)); + + Builder covers(String attribute, List values) => + _push(Query.covers(attribute, values)); + + Builder notCovers(String attribute, List values) => + _push(Query.notCovers(attribute, values)); + + Builder spatialEquals(String attribute, List values) => + _push(Query.spatialEquals(attribute, values)); + + Builder notSpatialEquals(String attribute, List values) => + _push(Query.notSpatialEquals(attribute, values)); + + Builder page(int page, [int perPage = 25]) => + _push(Query.page(page, perPage)); + + Builder _push(Object query) { + if (query is List) { + _queries.addAll(query.cast()); + } else { + _queries.add(query as String); + } + return this; + } +} diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index 50456bc0db..3cade2a6d6 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -593,7 +593,7 @@ export class Query { return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; }; - static builder = (): QueryBuilder => new QueryBuilder(); + static builder = (): Query.Builder => new Query.Builder(); private static createJoin( method: string, @@ -638,14 +638,15 @@ export class Query { export type QueryInput = | string | Query - | QueryBuilder + | Query.Builder | QueryInput[]; /** - * Fluent collector of Query fragments. Call {@link QueryBuilder.build} to + * Fluent collector of Query fragments. Call {@link Query.Builder.build} to * produce the `string[]` expected by list/createQuery methods. */ -export class QueryBuilder { +export namespace Query { + export class Builder { private readonly queries: string[] = []; build(): string[] { @@ -993,12 +994,13 @@ export class QueryBuilder { return this; } } +} export function normalizeQueries(queries?: QueryInput | null): string[] { if (queries == null) { return []; } - if (queries instanceof QueryBuilder) { + if (queries instanceof Query.Builder) { return queries.build(); } if (!Array.isArray(queries)) { @@ -1021,7 +1023,7 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { function isQueryList(value: unknown[]): boolean { return value.every( (item) => - item instanceof QueryBuilder || + item instanceof Query.Builder || item instanceof Query || typeof item === "string" || (Array.isArray(item) && isQueryList(item)) @@ -1029,7 +1031,7 @@ function isQueryList(value: unknown[]): boolean { } export function flattenParam(value: unknown): unknown { - if (value instanceof QueryBuilder || value instanceof Query) { + if (value instanceof Query.Builder || value instanceof Query) { return normalizeQueries(value as QueryInput); } if (Array.isArray(value) && isQueryList(value)) { @@ -1037,3 +1039,6 @@ export function flattenParam(value: unknown): unknown { } return value; } + +export const QueryBuilder = Query.Builder; +export type QueryBuilder = Query.Builder; diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 97bdee8ffb..c2130b4168 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; namespace {{ spec.info.title | caseUcfirst }} { - public class Query + public partial class Query { [JsonPropertyName("method")] public string Method { get; set; } = string.Empty; @@ -550,14 +550,9 @@ namespace {{ spec.info.title | caseUcfirst }} return new List { Limit(perPage), Offset((page - 1) * perPage) }; } - public static QueryBuilder Builder() - { - return new QueryBuilder(); - } - internal static object? Flatten(object? value) { - if (value is QueryBuilder builder) + if (value is Builder builder) { return builder.Build(); } @@ -576,7 +571,7 @@ namespace {{ spec.info.title | caseUcfirst }} foreach (var item in items) { var flat = Flatten(item); - if (item is QueryBuilder || item is Query) + if (item is Builder || item is Query) { if (flat is IEnumerable nested && flat is not string) { @@ -665,501 +660,4 @@ namespace {{ spec.info.title | caseUcfirst }} return new Query(method, table, values).ToString(); } } - - public class QueryBuilder - { - private readonly List queries = new List(); - - public List Build() - { - return new List(queries); - } - - public QueryBuilder Equal(string attribute, object value) - { - return Push(Query.Equal(attribute, value)); - } - - public QueryBuilder NotEqual(string attribute, object value) - { - return Push(Query.NotEqual(attribute, value)); - } - - public QueryBuilder LessThan(string attribute, object value) - { - return Push(Query.LessThan(attribute, value)); - } - - public QueryBuilder LessThanEqual(string attribute, object value) - { - return Push(Query.LessThanEqual(attribute, value)); - } - - public QueryBuilder GreaterThan(string attribute, object value) - { - return Push(Query.GreaterThan(attribute, value)); - } - - public QueryBuilder GreaterThanEqual(string attribute, object value) - { - return Push(Query.GreaterThanEqual(attribute, value)); - } - - public QueryBuilder Search(string attribute, string value) - { - return Push(Query.Search(attribute, value)); - } - - public QueryBuilder IsNull(string attribute) - { - return Push(Query.IsNull(attribute)); - } - - public QueryBuilder IsNotNull(string attribute) - { - return Push(Query.IsNotNull(attribute)); - } - - public QueryBuilder Between(string attribute, string start, string end) - { - return Push(Query.Between(attribute, start, end)); - } - - public QueryBuilder Between(string attribute, int start, int end) - { - return Push(Query.Between(attribute, start, end)); - } - - public QueryBuilder Between(string attribute, double start, double end) - { - return Push(Query.Between(attribute, start, end)); - } - - public QueryBuilder StartsWith(string attribute, string value) - { - return Push(Query.StartsWith(attribute, value)); - } - - public QueryBuilder EndsWith(string attribute, string value) - { - return Push(Query.EndsWith(attribute, value)); - } - - public QueryBuilder Select(List attributes) - { - return Push(Query.Select(attributes)); - } - - public QueryBuilder CursorAfter(string documentId) - { - return Push(Query.CursorAfter(documentId)); - } - - public QueryBuilder CursorBefore(string documentId) - { - return Push(Query.CursorBefore(documentId)); - } - - public QueryBuilder OrderAsc(string attribute) - { - return Push(Query.OrderAsc(attribute)); - } - - public QueryBuilder OrderDesc(string attribute) - { - return Push(Query.OrderDesc(attribute)); - } - - public QueryBuilder OrderRandom() - { - return Push(Query.OrderRandom()); - } - - public QueryBuilder Limit(int limit) - { - return Push(Query.Limit(limit)); - } - - public QueryBuilder Offset(int offset) - { - return Push(Query.Offset(offset)); - } - - public QueryBuilder Contains(string attribute, object value) - { - return Push(Query.Contains(attribute, value)); - } - - public QueryBuilder ContainsAny(string attribute, IList value) - { - return Push(Query.ContainsAny(attribute, value)); - } - - public QueryBuilder ContainsAll(string attribute, IList value) - { - return Push(Query.ContainsAll(attribute, value)); - } - - public QueryBuilder NotContains(string attribute, object value) - { - return Push(Query.NotContains(attribute, value)); - } - - public QueryBuilder NotSearch(string attribute, string value) - { - return Push(Query.NotSearch(attribute, value)); - } - - public QueryBuilder NotBetween(string attribute, string start, string end) - { - return Push(Query.NotBetween(attribute, start, end)); - } - - public QueryBuilder NotBetween(string attribute, int start, int end) - { - return Push(Query.NotBetween(attribute, start, end)); - } - - public QueryBuilder NotBetween(string attribute, double start, double end) - { - return Push(Query.NotBetween(attribute, start, end)); - } - - public QueryBuilder NotStartsWith(string attribute, string value) - { - return Push(Query.NotStartsWith(attribute, value)); - } - - public QueryBuilder NotEndsWith(string attribute, string value) - { - return Push(Query.NotEndsWith(attribute, value)); - } - - public QueryBuilder Regex(string attribute, string pattern) - { - return Push(Query.Regex(attribute, pattern)); - } - - public QueryBuilder Exists(List attributes) - { - return Push(Query.Exists(attributes)); - } - - public QueryBuilder NotExists(List attributes) - { - return Push(Query.NotExists(attributes)); - } - - public QueryBuilder CreatedBefore(string value) - { - return Push(Query.CreatedBefore(value)); - } - - public QueryBuilder CreatedAfter(string value) - { - return Push(Query.CreatedAfter(value)); - } - - public QueryBuilder CreatedBetween(string start, string end) - { - return Push(Query.CreatedBetween(start, end)); - } - - public QueryBuilder UpdatedBefore(string value) - { - return Push(Query.UpdatedBefore(value)); - } - - public QueryBuilder UpdatedAfter(string value) - { - return Push(Query.UpdatedAfter(value)); - } - - public QueryBuilder UpdatedBetween(string start, string end) - { - return Push(Query.UpdatedBetween(start, end)); - } - - public QueryBuilder Or(List queries) - { - return Push(Query.Or(queries)); - } - - public QueryBuilder And(List queries) - { - return Push(Query.And(queries)); - } - - public QueryBuilder ElemMatch(string attribute, List queries) - { - return Push(Query.ElemMatch(attribute, queries)); - } - - public QueryBuilder DistanceEqual(string attribute, object values, double distance, bool meters = true) - { - return Push(Query.DistanceEqual(attribute, values, distance, meters)); - } - - public QueryBuilder DistanceNotEqual(string attribute, object values, double distance, bool meters = true) - { - return Push(Query.DistanceNotEqual(attribute, values, distance, meters)); - } - - public QueryBuilder DistanceGreaterThan(string attribute, object values, double distance, bool meters = true) - { - return Push(Query.DistanceGreaterThan(attribute, values, distance, meters)); - } - - public QueryBuilder DistanceLessThan(string attribute, object values, double distance, bool meters = true) - { - return Push(Query.DistanceLessThan(attribute, values, distance, meters)); - } - - public QueryBuilder Intersects(string attribute, object values) - { - return Push(Query.Intersects(attribute, values)); - } - - public QueryBuilder NotIntersects(string attribute, object values) - { - return Push(Query.NotIntersects(attribute, values)); - } - - public QueryBuilder Crosses(string attribute, object values) - { - return Push(Query.Crosses(attribute, values)); - } - - public QueryBuilder NotCrosses(string attribute, object values) - { - return Push(Query.NotCrosses(attribute, values)); - } - - public QueryBuilder Overlaps(string attribute, object values) - { - return Push(Query.Overlaps(attribute, values)); - } - - public QueryBuilder NotOverlaps(string attribute, object values) - { - return Push(Query.NotOverlaps(attribute, values)); - } - - public QueryBuilder Touches(string attribute, object values) - { - return Push(Query.Touches(attribute, values)); - } - - public QueryBuilder NotTouches(string attribute, object values) - { - return Push(Query.NotTouches(attribute, values)); - } - - public QueryBuilder Count(string attribute = "*", string alias = "") - { - return Push(Query.Count(attribute, alias)); - } - - public QueryBuilder CountDistinct(string attribute, string alias = "") - { - return Push(Query.CountDistinct(attribute, alias)); - } - - public QueryBuilder Sum(string attribute, string alias = "") - { - return Push(Query.Sum(attribute, alias)); - } - - public QueryBuilder Avg(string attribute, string alias = "") - { - return Push(Query.Avg(attribute, alias)); - } - - public QueryBuilder Min(string attribute, string alias = "") - { - return Push(Query.Min(attribute, alias)); - } - - public QueryBuilder Max(string attribute, string alias = "") - { - return Push(Query.Max(attribute, alias)); - } - - public QueryBuilder Stddev(string attribute, string alias = "") - { - return Push(Query.Stddev(attribute, alias)); - } - - public QueryBuilder StddevPop(string attribute, string alias = "") - { - return Push(Query.StddevPop(attribute, alias)); - } - - public QueryBuilder StddevSamp(string attribute, string alias = "") - { - return Push(Query.StddevSamp(attribute, alias)); - } - - public QueryBuilder Variance(string attribute, string alias = "") - { - return Push(Query.Variance(attribute, alias)); - } - - public QueryBuilder VarPop(string attribute, string alias = "") - { - return Push(Query.VarPop(attribute, alias)); - } - - public QueryBuilder VarSamp(string attribute, string alias = "") - { - return Push(Query.VarSamp(attribute, alias)); - } - - public QueryBuilder BitAnd(string attribute, string alias = "") - { - return Push(Query.BitAnd(attribute, alias)); - } - - public QueryBuilder BitOr(string attribute, string alias = "") - { - return Push(Query.BitOr(attribute, alias)); - } - - public QueryBuilder BitXor(string attribute, string alias = "") - { - return Push(Query.BitXor(attribute, alias)); - } - - public QueryBuilder GroupBy(List attributes) - { - return Push(Query.GroupBy(attributes)); - } - - public QueryBuilder Having(List queries) - { - return Push(Query.Having(queries)); - } - - public QueryBuilder Distinct() - { - return Push(Query.Distinct()); - } - - public QueryBuilder Join(string table, string left, string right, string @operator = "=", string alias = "") - { - return Push(Query.Join(table, left, right, @operator, alias)); - } - - public QueryBuilder Join(string table, List on) - { - return Push(Query.Join(table, on)); - } - - public QueryBuilder Join(string table, string alias, List on) - { - return Push(Query.Join(table, alias, on)); - } - - public QueryBuilder LeftJoin(string table, string left, string right, string @operator = "=", string alias = "") - { - return Push(Query.LeftJoin(table, left, right, @operator, alias)); - } - - public QueryBuilder LeftJoin(string table, List on) - { - return Push(Query.LeftJoin(table, on)); - } - - public QueryBuilder LeftJoin(string table, string alias, List on) - { - return Push(Query.LeftJoin(table, alias, on)); - } - - public QueryBuilder RightJoin(string table, string left, string right, string @operator = "=", string alias = "") - { - return Push(Query.RightJoin(table, left, right, @operator, alias)); - } - - public QueryBuilder RightJoin(string table, List on) - { - return Push(Query.RightJoin(table, on)); - } - - public QueryBuilder RightJoin(string table, string alias, List on) - { - return Push(Query.RightJoin(table, alias, on)); - } - - public QueryBuilder FullOuterJoin(string table, string left, string right, string @operator = "=", string alias = "") - { - return Push(Query.FullOuterJoin(table, left, right, @operator, alias)); - } - - public QueryBuilder FullOuterJoin(string table, List on) - { - return Push(Query.FullOuterJoin(table, on)); - } - - public QueryBuilder FullOuterJoin(string table, string alias, List on) - { - return Push(Query.FullOuterJoin(table, alias, on)); - } - - public QueryBuilder CrossJoin(string table, string alias = "") - { - return Push(Query.CrossJoin(table, alias)); - } - - public QueryBuilder VectorDot(string attribute, List vector) - { - return Push(Query.VectorDot(attribute, vector)); - } - - public QueryBuilder VectorCosine(string attribute, List vector) - { - return Push(Query.VectorCosine(attribute, vector)); - } - - public QueryBuilder VectorEuclidean(string attribute, List vector) - { - return Push(Query.VectorEuclidean(attribute, vector)); - } - - public QueryBuilder Covers(string attribute, object values) - { - return Push(Query.Covers(attribute, values)); - } - - public QueryBuilder NotCovers(string attribute, object values) - { - return Push(Query.NotCovers(attribute, values)); - } - - public QueryBuilder SpatialEquals(string attribute, object values) - { - return Push(Query.SpatialEquals(attribute, values)); - } - - public QueryBuilder NotSpatialEquals(string attribute, object values) - { - return Push(Query.NotSpatialEquals(attribute, values)); - } - - public QueryBuilder Page(int page, int perPage = 25) - { - return Push(Query.Page(page, perPage)); - } - - private QueryBuilder Push(string query) - { - queries.Add(query); - return this; - } - - private QueryBuilder Push(IEnumerable query) - { - queries.AddRange(query); - return this; - } - } } diff --git a/templates/dotnet/Package/Query/Builder.cs.twig b/templates/dotnet/Package/Query/Builder.cs.twig new file mode 100644 index 0000000000..00fb5f0857 --- /dev/null +++ b/templates/dotnet/Package/Query/Builder.cs.twig @@ -0,0 +1,507 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace {{ spec.info.title | caseUcfirst }} +{ + public partial class Query + { + public class Builder + { + private readonly List queries = new List(); + + public List Build() + { + return new List(queries); + } + + public Builder Equal(string attribute, object value) + { + return Push(Query.Equal(attribute, value)); + } + + public Builder NotEqual(string attribute, object value) + { + return Push(Query.NotEqual(attribute, value)); + } + + public Builder LessThan(string attribute, object value) + { + return Push(Query.LessThan(attribute, value)); + } + + public Builder LessThanEqual(string attribute, object value) + { + return Push(Query.LessThanEqual(attribute, value)); + } + + public Builder GreaterThan(string attribute, object value) + { + return Push(Query.GreaterThan(attribute, value)); + } + + public Builder GreaterThanEqual(string attribute, object value) + { + return Push(Query.GreaterThanEqual(attribute, value)); + } + + public Builder Search(string attribute, string value) + { + return Push(Query.Search(attribute, value)); + } + + public Builder IsNull(string attribute) + { + return Push(Query.IsNull(attribute)); + } + + public Builder IsNotNull(string attribute) + { + return Push(Query.IsNotNull(attribute)); + } + + public Builder Between(string attribute, string start, string end) + { + return Push(Query.Between(attribute, start, end)); + } + + public Builder Between(string attribute, int start, int end) + { + return Push(Query.Between(attribute, start, end)); + } + + public Builder Between(string attribute, double start, double end) + { + return Push(Query.Between(attribute, start, end)); + } + + public Builder StartsWith(string attribute, string value) + { + return Push(Query.StartsWith(attribute, value)); + } + + public Builder EndsWith(string attribute, string value) + { + return Push(Query.EndsWith(attribute, value)); + } + + public Builder Select(List attributes) + { + return Push(Query.Select(attributes)); + } + + public Builder CursorAfter(string documentId) + { + return Push(Query.CursorAfter(documentId)); + } + + public Builder CursorBefore(string documentId) + { + return Push(Query.CursorBefore(documentId)); + } + + public Builder OrderAsc(string attribute) + { + return Push(Query.OrderAsc(attribute)); + } + + public Builder OrderDesc(string attribute) + { + return Push(Query.OrderDesc(attribute)); + } + + public Builder OrderRandom() + { + return Push(Query.OrderRandom()); + } + + public Builder Limit(int limit) + { + return Push(Query.Limit(limit)); + } + + public Builder Offset(int offset) + { + return Push(Query.Offset(offset)); + } + + public Builder Contains(string attribute, object value) + { + return Push(Query.Contains(attribute, value)); + } + + public Builder ContainsAny(string attribute, IList value) + { + return Push(Query.ContainsAny(attribute, value)); + } + + public Builder ContainsAll(string attribute, IList value) + { + return Push(Query.ContainsAll(attribute, value)); + } + + public Builder NotContains(string attribute, object value) + { + return Push(Query.NotContains(attribute, value)); + } + + public Builder NotSearch(string attribute, string value) + { + return Push(Query.NotSearch(attribute, value)); + } + + public Builder NotBetween(string attribute, string start, string end) + { + return Push(Query.NotBetween(attribute, start, end)); + } + + public Builder NotBetween(string attribute, int start, int end) + { + return Push(Query.NotBetween(attribute, start, end)); + } + + public Builder NotBetween(string attribute, double start, double end) + { + return Push(Query.NotBetween(attribute, start, end)); + } + + public Builder NotStartsWith(string attribute, string value) + { + return Push(Query.NotStartsWith(attribute, value)); + } + + public Builder NotEndsWith(string attribute, string value) + { + return Push(Query.NotEndsWith(attribute, value)); + } + + public Builder Regex(string attribute, string pattern) + { + return Push(Query.Regex(attribute, pattern)); + } + + public Builder Exists(List attributes) + { + return Push(Query.Exists(attributes)); + } + + public Builder NotExists(List attributes) + { + return Push(Query.NotExists(attributes)); + } + + public Builder CreatedBefore(string value) + { + return Push(Query.CreatedBefore(value)); + } + + public Builder CreatedAfter(string value) + { + return Push(Query.CreatedAfter(value)); + } + + public Builder CreatedBetween(string start, string end) + { + return Push(Query.CreatedBetween(start, end)); + } + + public Builder UpdatedBefore(string value) + { + return Push(Query.UpdatedBefore(value)); + } + + public Builder UpdatedAfter(string value) + { + return Push(Query.UpdatedAfter(value)); + } + + public Builder UpdatedBetween(string start, string end) + { + return Push(Query.UpdatedBetween(start, end)); + } + + public Builder Or(List queries) + { + return Push(Query.Or(queries)); + } + + public Builder And(List queries) + { + return Push(Query.And(queries)); + } + + public Builder ElemMatch(string attribute, List queries) + { + return Push(Query.ElemMatch(attribute, queries)); + } + + public Builder DistanceEqual(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceEqual(attribute, values, distance, meters)); + } + + public Builder DistanceNotEqual(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceNotEqual(attribute, values, distance, meters)); + } + + public Builder DistanceGreaterThan(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceGreaterThan(attribute, values, distance, meters)); + } + + public Builder DistanceLessThan(string attribute, object values, double distance, bool meters = true) + { + return Push(Query.DistanceLessThan(attribute, values, distance, meters)); + } + + public Builder Intersects(string attribute, object values) + { + return Push(Query.Intersects(attribute, values)); + } + + public Builder NotIntersects(string attribute, object values) + { + return Push(Query.NotIntersects(attribute, values)); + } + + public Builder Crosses(string attribute, object values) + { + return Push(Query.Crosses(attribute, values)); + } + + public Builder NotCrosses(string attribute, object values) + { + return Push(Query.NotCrosses(attribute, values)); + } + + public Builder Overlaps(string attribute, object values) + { + return Push(Query.Overlaps(attribute, values)); + } + + public Builder NotOverlaps(string attribute, object values) + { + return Push(Query.NotOverlaps(attribute, values)); + } + + public Builder Touches(string attribute, object values) + { + return Push(Query.Touches(attribute, values)); + } + + public Builder NotTouches(string attribute, object values) + { + return Push(Query.NotTouches(attribute, values)); + } + + public Builder Count(string attribute = "*", string alias = "") + { + return Push(Query.Count(attribute, alias)); + } + + public Builder CountDistinct(string attribute, string alias = "") + { + return Push(Query.CountDistinct(attribute, alias)); + } + + public Builder Sum(string attribute, string alias = "") + { + return Push(Query.Sum(attribute, alias)); + } + + public Builder Avg(string attribute, string alias = "") + { + return Push(Query.Avg(attribute, alias)); + } + + public Builder Min(string attribute, string alias = "") + { + return Push(Query.Min(attribute, alias)); + } + + public Builder Max(string attribute, string alias = "") + { + return Push(Query.Max(attribute, alias)); + } + + public Builder Stddev(string attribute, string alias = "") + { + return Push(Query.Stddev(attribute, alias)); + } + + public Builder StddevPop(string attribute, string alias = "") + { + return Push(Query.StddevPop(attribute, alias)); + } + + public Builder StddevSamp(string attribute, string alias = "") + { + return Push(Query.StddevSamp(attribute, alias)); + } + + public Builder Variance(string attribute, string alias = "") + { + return Push(Query.Variance(attribute, alias)); + } + + public Builder VarPop(string attribute, string alias = "") + { + return Push(Query.VarPop(attribute, alias)); + } + + public Builder VarSamp(string attribute, string alias = "") + { + return Push(Query.VarSamp(attribute, alias)); + } + + public Builder BitAnd(string attribute, string alias = "") + { + return Push(Query.BitAnd(attribute, alias)); + } + + public Builder BitOr(string attribute, string alias = "") + { + return Push(Query.BitOr(attribute, alias)); + } + + public Builder BitXor(string attribute, string alias = "") + { + return Push(Query.BitXor(attribute, alias)); + } + + public Builder GroupBy(List attributes) + { + return Push(Query.GroupBy(attributes)); + } + + public Builder Having(List queries) + { + return Push(Query.Having(queries)); + } + + public Builder Distinct() + { + return Push(Query.Distinct()); + } + + public Builder Join(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.Join(table, left, right, @operator, alias)); + } + + public Builder Join(string table, List on) + { + return Push(Query.Join(table, on)); + } + + public Builder Join(string table, string alias, List on) + { + return Push(Query.Join(table, alias, on)); + } + + public Builder LeftJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.LeftJoin(table, left, right, @operator, alias)); + } + + public Builder LeftJoin(string table, List on) + { + return Push(Query.LeftJoin(table, on)); + } + + public Builder LeftJoin(string table, string alias, List on) + { + return Push(Query.LeftJoin(table, alias, on)); + } + + public Builder RightJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.RightJoin(table, left, right, @operator, alias)); + } + + public Builder RightJoin(string table, List on) + { + return Push(Query.RightJoin(table, on)); + } + + public Builder RightJoin(string table, string alias, List on) + { + return Push(Query.RightJoin(table, alias, on)); + } + + public Builder FullOuterJoin(string table, string left, string right, string @operator = "=", string alias = "") + { + return Push(Query.FullOuterJoin(table, left, right, @operator, alias)); + } + + public Builder FullOuterJoin(string table, List on) + { + return Push(Query.FullOuterJoin(table, on)); + } + + public Builder FullOuterJoin(string table, string alias, List on) + { + return Push(Query.FullOuterJoin(table, alias, on)); + } + + public Builder CrossJoin(string table, string alias = "") + { + return Push(Query.CrossJoin(table, alias)); + } + + public Builder VectorDot(string attribute, List vector) + { + return Push(Query.VectorDot(attribute, vector)); + } + + public Builder VectorCosine(string attribute, List vector) + { + return Push(Query.VectorCosine(attribute, vector)); + } + + public Builder VectorEuclidean(string attribute, List vector) + { + return Push(Query.VectorEuclidean(attribute, vector)); + } + + public Builder Covers(string attribute, object values) + { + return Push(Query.Covers(attribute, values)); + } + + public Builder NotCovers(string attribute, object values) + { + return Push(Query.NotCovers(attribute, values)); + } + + public Builder SpatialEquals(string attribute, object values) + { + return Push(Query.SpatialEquals(attribute, values)); + } + + public Builder NotSpatialEquals(string attribute, object values) + { + return Push(Query.NotSpatialEquals(attribute, values)); + } + + public Builder Page(int page, int perPage = 25) + { + return Push(Query.Page(page, perPage)); + } + + private Builder Push(string query) + { + queries.Add(query); + return this; + } + + private Builder Push(IEnumerable query) + { + queries.AddRange(query); + return this; + } + } + } +} diff --git a/templates/flutter/lib/package.dart.twig b/templates/flutter/lib/package.dart.twig index 44e1b3cd2d..83f19cb0a0 100644 --- a/templates/flutter/lib/package.dart.twig +++ b/templates/flutter/lib/package.dart.twig @@ -27,6 +27,7 @@ export 'src/realtime_message.dart'; export 'src/input_file.dart'; part 'query.dart'; +part 'query/builder.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index 746605cf96..4ce5836ecc 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -767,12 +767,12 @@ func flattenValue(value interface{}) interface{} { return nil } switch v := value.(type) { - case *query.QueryBuilder: + case *query.Builder: if v == nil { return value } return v.Build() - case query.QueryBuilder: + case query.Builder: return v.Build() } @@ -787,7 +787,7 @@ func flattenValue(value interface{}) interface{} { item := reflected.Index(i).Interface() flat := flattenValue(item) switch item.(type) { - case *query.QueryBuilder, query.QueryBuilder: + case *query.Builder, query.Builder: if list, ok := flat.([]string); ok { for _, entry := range list { out = append(out, entry) diff --git a/templates/go/query.go.twig b/templates/go/query.go.twig index 4f82acc60a..ea094cb9fd 100644 --- a/templates/go/query.go.twig +++ b/templates/go/query.go.twig @@ -746,385 +746,18 @@ func Page(page, perPage int) []string { return []string{Limit(perPage), Offset((page - 1) * perPage)} } -type QueryBuilder struct { - queries []string -} - -func Builder() *QueryBuilder { - return &QueryBuilder{} -} - -func (b *QueryBuilder) Build() []string { - result := make([]string, len(b.queries)) - copy(result, b.queries) - return result -} - -func (b *QueryBuilder) push(queries ...string) *QueryBuilder { - b.queries = append(b.queries, queries...) - return b -} - -func (b *QueryBuilder) Equal(attribute string, value interface{}) *QueryBuilder { - return b.push(Equal(attribute, value)) -} - -func (b *QueryBuilder) NotEqual(attribute string, value interface{}) *QueryBuilder { - return b.push(NotEqual(attribute, value)) -} - -func (b *QueryBuilder) LessThan(attribute string, value interface{}) *QueryBuilder { - return b.push(LessThan(attribute, value)) -} - -func (b *QueryBuilder) LessThanEqual(attribute string, value interface{}) *QueryBuilder { - return b.push(LessThanEqual(attribute, value)) -} - -func (b *QueryBuilder) GreaterThan(attribute string, value interface{}) *QueryBuilder { - return b.push(GreaterThan(attribute, value)) -} - -func (b *QueryBuilder) GreaterThanEqual(attribute string, value interface{}) *QueryBuilder { - return b.push(GreaterThanEqual(attribute, value)) -} - -func (b *QueryBuilder) Search(attribute string, value interface{}) *QueryBuilder { - return b.push(Search(attribute, value)) -} - -func (b *QueryBuilder) IsNull(attribute string) *QueryBuilder { - return b.push(IsNull(attribute)) -} - -func (b *QueryBuilder) IsNotNull(attribute string) *QueryBuilder { - return b.push(IsNotNull(attribute)) -} - -func (b *QueryBuilder) Between(attribute string, start, end interface{}) *QueryBuilder { - return b.push(Between(attribute, start, end)) -} - -func (b *QueryBuilder) StartsWith(attribute string, value interface{}) *QueryBuilder { - return b.push(StartsWith(attribute, value)) -} - -func (b *QueryBuilder) EndsWith(attribute string, value interface{}) *QueryBuilder { - return b.push(EndsWith(attribute, value)) -} - -func (b *QueryBuilder) Select(attributes interface{}) *QueryBuilder { - return b.push(Select(attributes)) -} - -func (b *QueryBuilder) CursorAfter(documentId string) *QueryBuilder { - return b.push(CursorAfter(documentId)) -} - -func (b *QueryBuilder) CursorBefore(documentId interface{}) *QueryBuilder { - return b.push(CursorBefore(documentId)) -} - -func (b *QueryBuilder) OrderAsc(attribute string) *QueryBuilder { - return b.push(OrderAsc(attribute)) -} - -func (b *QueryBuilder) OrderDesc(attribute string) *QueryBuilder { - return b.push(OrderDesc(attribute)) -} - -func (b *QueryBuilder) OrderRandom() *QueryBuilder { - return b.push(OrderRandom()) -} - -func (b *QueryBuilder) Limit(limit int) *QueryBuilder { - return b.push(Limit(limit)) -} - -func (b *QueryBuilder) Offset(offset int) *QueryBuilder { - return b.push(Offset(offset)) -} - -func (b *QueryBuilder) Contains(attribute string, value interface{}) *QueryBuilder { - return b.push(Contains(attribute, value)) -} - -func (b *QueryBuilder) ContainsAny(attribute string, value []interface{}) *QueryBuilder { - return b.push(ContainsAny(attribute, value)) -} - -func (b *QueryBuilder) ContainsAll(attribute string, value []interface{}) *QueryBuilder { - return b.push(ContainsAll(attribute, value)) -} - -func (b *QueryBuilder) NotContains(attribute string, value interface{}) *QueryBuilder { - return b.push(NotContains(attribute, value)) -} - -func (b *QueryBuilder) NotSearch(attribute string, value interface{}) *QueryBuilder { - return b.push(NotSearch(attribute, value)) -} - -func (b *QueryBuilder) NotBetween(attribute string, start, end interface{}) *QueryBuilder { - return b.push(NotBetween(attribute, start, end)) -} - -func (b *QueryBuilder) NotStartsWith(attribute string, value interface{}) *QueryBuilder { - return b.push(NotStartsWith(attribute, value)) -} - -func (b *QueryBuilder) NotEndsWith(attribute string, value interface{}) *QueryBuilder { - return b.push(NotEndsWith(attribute, value)) -} - -func (b *QueryBuilder) Regex(attribute string, pattern string) *QueryBuilder { - return b.push(Regex(attribute, pattern)) -} - -func (b *QueryBuilder) Exists(attributes []interface{}) *QueryBuilder { - return b.push(Exists(attributes)) -} - -func (b *QueryBuilder) NotExists(attributes []interface{}) *QueryBuilder { - return b.push(NotExists(attributes)) -} - -func (b *QueryBuilder) CreatedBefore(value interface{}) *QueryBuilder { - return b.push(CreatedBefore(value)) -} - -func (b *QueryBuilder) CreatedAfter(value interface{}) *QueryBuilder { - return b.push(CreatedAfter(value)) -} - -func (b *QueryBuilder) CreatedBetween(start, end interface{}) *QueryBuilder { - return b.push(CreatedBetween(start, end)) -} - -func (b *QueryBuilder) UpdatedBefore(value interface{}) *QueryBuilder { - return b.push(UpdatedBefore(value)) -} - -func (b *QueryBuilder) UpdatedAfter(value interface{}) *QueryBuilder { - return b.push(UpdatedAfter(value)) -} - -func (b *QueryBuilder) UpdatedBetween(start, end interface{}) *QueryBuilder { - return b.push(UpdatedBetween(start, end)) -} - -func (b *QueryBuilder) Or(queries []string) *QueryBuilder { - return b.push(Or(queries)) -} - -func (b *QueryBuilder) And(queries []string) *QueryBuilder { - return b.push(And(queries)) -} - -func (b *QueryBuilder) ElemMatch(attribute string, queries []string) *QueryBuilder { - return b.push(ElemMatch(attribute, queries)) -} - -func (b *QueryBuilder) DistanceEqual(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { - return b.push(DistanceEqual(attribute, values, distance, meters)) -} - -func (b *QueryBuilder) DistanceNotEqual(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { - return b.push(DistanceNotEqual(attribute, values, distance, meters)) -} - -func (b *QueryBuilder) DistanceGreaterThan(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { - return b.push(DistanceGreaterThan(attribute, values, distance, meters)) -} - -func (b *QueryBuilder) DistanceLessThan(attribute string, values []interface{}, distance float64, meters bool) *QueryBuilder { - return b.push(DistanceLessThan(attribute, values, distance, meters)) -} - -func (b *QueryBuilder) Intersects(attribute string, values []interface{}) *QueryBuilder { - return b.push(Intersects(attribute, values)) -} - -func (b *QueryBuilder) NotIntersects(attribute string, values []interface{}) *QueryBuilder { - return b.push(NotIntersects(attribute, values)) -} - -func (b *QueryBuilder) Crosses(attribute string, values []interface{}) *QueryBuilder { - return b.push(Crosses(attribute, values)) -} - -func (b *QueryBuilder) NotCrosses(attribute string, values []interface{}) *QueryBuilder { - return b.push(NotCrosses(attribute, values)) -} - -func (b *QueryBuilder) Overlaps(attribute string, values []interface{}) *QueryBuilder { - return b.push(Overlaps(attribute, values)) -} - -func (b *QueryBuilder) NotOverlaps(attribute string, values []interface{}) *QueryBuilder { - return b.push(NotOverlaps(attribute, values)) -} - -func (b *QueryBuilder) Touches(attribute string, values []interface{}) *QueryBuilder { - return b.push(Touches(attribute, values)) -} - -func (b *QueryBuilder) NotTouches(attribute string, values []interface{}) *QueryBuilder { - return b.push(NotTouches(attribute, values)) -} - -func (b *QueryBuilder) Count(attribute, alias string) *QueryBuilder { - return b.push(Count(attribute, alias)) -} - -func (b *QueryBuilder) CountDistinct(attribute, alias string) *QueryBuilder { - return b.push(CountDistinct(attribute, alias)) -} - -func (b *QueryBuilder) Sum(attribute, alias string) *QueryBuilder { - return b.push(Sum(attribute, alias)) -} - -func (b *QueryBuilder) Avg(attribute, alias string) *QueryBuilder { - return b.push(Avg(attribute, alias)) -} - -func (b *QueryBuilder) Min(attribute, alias string) *QueryBuilder { - return b.push(Min(attribute, alias)) -} - -func (b *QueryBuilder) Max(attribute, alias string) *QueryBuilder { - return b.push(Max(attribute, alias)) -} - -func (b *QueryBuilder) Stddev(attribute, alias string) *QueryBuilder { - return b.push(Stddev(attribute, alias)) -} - -func (b *QueryBuilder) StddevPop(attribute, alias string) *QueryBuilder { - return b.push(StddevPop(attribute, alias)) -} - -func (b *QueryBuilder) StddevSamp(attribute, alias string) *QueryBuilder { - return b.push(StddevSamp(attribute, alias)) -} - -func (b *QueryBuilder) Variance(attribute, alias string) *QueryBuilder { - return b.push(Variance(attribute, alias)) -} - -func (b *QueryBuilder) VarPop(attribute, alias string) *QueryBuilder { - return b.push(VarPop(attribute, alias)) -} - -func (b *QueryBuilder) VarSamp(attribute, alias string) *QueryBuilder { - return b.push(VarSamp(attribute, alias)) -} - -func (b *QueryBuilder) BitAnd(attribute, alias string) *QueryBuilder { - return b.push(BitAnd(attribute, alias)) -} - -func (b *QueryBuilder) BitOr(attribute, alias string) *QueryBuilder { - return b.push(BitOr(attribute, alias)) -} - -func (b *QueryBuilder) BitXor(attribute, alias string) *QueryBuilder { - return b.push(BitXor(attribute, alias)) -} - -func (b *QueryBuilder) GroupBy(attributes []interface{}) *QueryBuilder { - return b.push(GroupBy(attributes)) -} - -func (b *QueryBuilder) Having(queries []string) *QueryBuilder { - return b.push(Having(queries)) -} - -func (b *QueryBuilder) Distinct() *QueryBuilder { - return b.push(Distinct()) -} - -func (b *QueryBuilder) Join(table, left, right, operator, alias string) *QueryBuilder { - return b.push(Join(table, left, right, operator, alias)) -} - -func (b *QueryBuilder) LeftJoin(table, left, right, operator, alias string) *QueryBuilder { - return b.push(LeftJoin(table, left, right, operator, alias)) -} - -func (b *QueryBuilder) RightJoin(table, left, right, operator, alias string) *QueryBuilder { - return b.push(RightJoin(table, left, right, operator, alias)) -} - -func (b *QueryBuilder) FullOuterJoin(table, left, right, operator, alias string) *QueryBuilder { - return b.push(FullOuterJoin(table, left, right, operator, alias)) -} - -func (b *QueryBuilder) JoinOn(table, alias string, on []string) *QueryBuilder { - return b.push(JoinOn(table, alias, on)) -} - -func (b *QueryBuilder) LeftJoinOn(table, alias string, on []string) *QueryBuilder { - return b.push(LeftJoinOn(table, alias, on)) -} - -func (b *QueryBuilder) RightJoinOn(table, alias string, on []string) *QueryBuilder { - return b.push(RightJoinOn(table, alias, on)) -} - -func (b *QueryBuilder) FullOuterJoinOn(table, alias string, on []string) *QueryBuilder { - return b.push(FullOuterJoinOn(table, alias, on)) -} - -func (b *QueryBuilder) CrossJoin(table, alias string) *QueryBuilder { - return b.push(CrossJoin(table, alias)) -} - -func (b *QueryBuilder) VectorDot(attribute string, vector []float64) *QueryBuilder { - return b.push(VectorDot(attribute, vector)) -} - -func (b *QueryBuilder) VectorCosine(attribute string, vector []float64) *QueryBuilder { - return b.push(VectorCosine(attribute, vector)) -} - -func (b *QueryBuilder) VectorEuclidean(attribute string, vector []float64) *QueryBuilder { - return b.push(VectorEuclidean(attribute, vector)) -} - -func (b *QueryBuilder) Covers(attribute string, values []interface{}) *QueryBuilder { - return b.push(Covers(attribute, values)) -} - -func (b *QueryBuilder) NotCovers(attribute string, values []interface{}) *QueryBuilder { - return b.push(NotCovers(attribute, values)) -} - -func (b *QueryBuilder) SpatialEquals(attribute string, values []interface{}) *QueryBuilder { - return b.push(SpatialEquals(attribute, values)) -} - -func (b *QueryBuilder) NotSpatialEquals(attribute string, values []interface{}) *QueryBuilder { - return b.push(NotSpatialEquals(attribute, values)) -} - -func (b *QueryBuilder) Page(page, perPage int) *QueryBuilder { - return b.push(Page(page, perPage)...) -} - func normalize(queries interface{}) []string { if queries == nil { return []string{} } switch v := queries.(type) { - case *QueryBuilder: + case *Builder: if v == nil { return []string{} } return v.Build() - case QueryBuilder: + case Builder: return v.Build() case string: return []string{v} diff --git a/templates/go/query/builder.go.twig b/templates/go/query/builder.go.twig new file mode 100644 index 0000000000..ae7fcc7a4e --- /dev/null +++ b/templates/go/query/builder.go.twig @@ -0,0 +1,368 @@ +package query + +type Builder struct { + queries []string +} + +func NewBuilder() *Builder { + return &Builder{} +} + +func (b *Builder) Build() []string { + result := make([]string, len(b.queries)) + copy(result, b.queries) + return result +} + +func (b *Builder) push(queries ...string) *Builder { + b.queries = append(b.queries, queries...) + return b +} + +func (b *Builder) Equal(attribute string, value interface{}) *Builder { + return b.push(Equal(attribute, value)) +} + +func (b *Builder) NotEqual(attribute string, value interface{}) *Builder { + return b.push(NotEqual(attribute, value)) +} + +func (b *Builder) LessThan(attribute string, value interface{}) *Builder { + return b.push(LessThan(attribute, value)) +} + +func (b *Builder) LessThanEqual(attribute string, value interface{}) *Builder { + return b.push(LessThanEqual(attribute, value)) +} + +func (b *Builder) GreaterThan(attribute string, value interface{}) *Builder { + return b.push(GreaterThan(attribute, value)) +} + +func (b *Builder) GreaterThanEqual(attribute string, value interface{}) *Builder { + return b.push(GreaterThanEqual(attribute, value)) +} + +func (b *Builder) Search(attribute string, value interface{}) *Builder { + return b.push(Search(attribute, value)) +} + +func (b *Builder) IsNull(attribute string) *Builder { + return b.push(IsNull(attribute)) +} + +func (b *Builder) IsNotNull(attribute string) *Builder { + return b.push(IsNotNull(attribute)) +} + +func (b *Builder) Between(attribute string, start, end interface{}) *Builder { + return b.push(Between(attribute, start, end)) +} + +func (b *Builder) StartsWith(attribute string, value interface{}) *Builder { + return b.push(StartsWith(attribute, value)) +} + +func (b *Builder) EndsWith(attribute string, value interface{}) *Builder { + return b.push(EndsWith(attribute, value)) +} + +func (b *Builder) Select(attributes interface{}) *Builder { + return b.push(Select(attributes)) +} + +func (b *Builder) CursorAfter(documentId string) *Builder { + return b.push(CursorAfter(documentId)) +} + +func (b *Builder) CursorBefore(documentId interface{}) *Builder { + return b.push(CursorBefore(documentId)) +} + +func (b *Builder) OrderAsc(attribute string) *Builder { + return b.push(OrderAsc(attribute)) +} + +func (b *Builder) OrderDesc(attribute string) *Builder { + return b.push(OrderDesc(attribute)) +} + +func (b *Builder) OrderRandom() *Builder { + return b.push(OrderRandom()) +} + +func (b *Builder) Limit(limit int) *Builder { + return b.push(Limit(limit)) +} + +func (b *Builder) Offset(offset int) *Builder { + return b.push(Offset(offset)) +} + +func (b *Builder) Contains(attribute string, value interface{}) *Builder { + return b.push(Contains(attribute, value)) +} + +func (b *Builder) ContainsAny(attribute string, value []interface{}) *Builder { + return b.push(ContainsAny(attribute, value)) +} + +func (b *Builder) ContainsAll(attribute string, value []interface{}) *Builder { + return b.push(ContainsAll(attribute, value)) +} + +func (b *Builder) NotContains(attribute string, value interface{}) *Builder { + return b.push(NotContains(attribute, value)) +} + +func (b *Builder) NotSearch(attribute string, value interface{}) *Builder { + return b.push(NotSearch(attribute, value)) +} + +func (b *Builder) NotBetween(attribute string, start, end interface{}) *Builder { + return b.push(NotBetween(attribute, start, end)) +} + +func (b *Builder) NotStartsWith(attribute string, value interface{}) *Builder { + return b.push(NotStartsWith(attribute, value)) +} + +func (b *Builder) NotEndsWith(attribute string, value interface{}) *Builder { + return b.push(NotEndsWith(attribute, value)) +} + +func (b *Builder) Regex(attribute string, pattern string) *Builder { + return b.push(Regex(attribute, pattern)) +} + +func (b *Builder) Exists(attributes []interface{}) *Builder { + return b.push(Exists(attributes)) +} + +func (b *Builder) NotExists(attributes []interface{}) *Builder { + return b.push(NotExists(attributes)) +} + +func (b *Builder) CreatedBefore(value interface{}) *Builder { + return b.push(CreatedBefore(value)) +} + +func (b *Builder) CreatedAfter(value interface{}) *Builder { + return b.push(CreatedAfter(value)) +} + +func (b *Builder) CreatedBetween(start, end interface{}) *Builder { + return b.push(CreatedBetween(start, end)) +} + +func (b *Builder) UpdatedBefore(value interface{}) *Builder { + return b.push(UpdatedBefore(value)) +} + +func (b *Builder) UpdatedAfter(value interface{}) *Builder { + return b.push(UpdatedAfter(value)) +} + +func (b *Builder) UpdatedBetween(start, end interface{}) *Builder { + return b.push(UpdatedBetween(start, end)) +} + +func (b *Builder) Or(queries []string) *Builder { + return b.push(Or(queries)) +} + +func (b *Builder) And(queries []string) *Builder { + return b.push(And(queries)) +} + +func (b *Builder) ElemMatch(attribute string, queries []string) *Builder { + return b.push(ElemMatch(attribute, queries)) +} + +func (b *Builder) DistanceEqual(attribute string, values []interface{}, distance float64, meters bool) *Builder { + return b.push(DistanceEqual(attribute, values, distance, meters)) +} + +func (b *Builder) DistanceNotEqual(attribute string, values []interface{}, distance float64, meters bool) *Builder { + return b.push(DistanceNotEqual(attribute, values, distance, meters)) +} + +func (b *Builder) DistanceGreaterThan(attribute string, values []interface{}, distance float64, meters bool) *Builder { + return b.push(DistanceGreaterThan(attribute, values, distance, meters)) +} + +func (b *Builder) DistanceLessThan(attribute string, values []interface{}, distance float64, meters bool) *Builder { + return b.push(DistanceLessThan(attribute, values, distance, meters)) +} + +func (b *Builder) Intersects(attribute string, values []interface{}) *Builder { + return b.push(Intersects(attribute, values)) +} + +func (b *Builder) NotIntersects(attribute string, values []interface{}) *Builder { + return b.push(NotIntersects(attribute, values)) +} + +func (b *Builder) Crosses(attribute string, values []interface{}) *Builder { + return b.push(Crosses(attribute, values)) +} + +func (b *Builder) NotCrosses(attribute string, values []interface{}) *Builder { + return b.push(NotCrosses(attribute, values)) +} + +func (b *Builder) Overlaps(attribute string, values []interface{}) *Builder { + return b.push(Overlaps(attribute, values)) +} + +func (b *Builder) NotOverlaps(attribute string, values []interface{}) *Builder { + return b.push(NotOverlaps(attribute, values)) +} + +func (b *Builder) Touches(attribute string, values []interface{}) *Builder { + return b.push(Touches(attribute, values)) +} + +func (b *Builder) NotTouches(attribute string, values []interface{}) *Builder { + return b.push(NotTouches(attribute, values)) +} + +func (b *Builder) Count(attribute, alias string) *Builder { + return b.push(Count(attribute, alias)) +} + +func (b *Builder) CountDistinct(attribute, alias string) *Builder { + return b.push(CountDistinct(attribute, alias)) +} + +func (b *Builder) Sum(attribute, alias string) *Builder { + return b.push(Sum(attribute, alias)) +} + +func (b *Builder) Avg(attribute, alias string) *Builder { + return b.push(Avg(attribute, alias)) +} + +func (b *Builder) Min(attribute, alias string) *Builder { + return b.push(Min(attribute, alias)) +} + +func (b *Builder) Max(attribute, alias string) *Builder { + return b.push(Max(attribute, alias)) +} + +func (b *Builder) Stddev(attribute, alias string) *Builder { + return b.push(Stddev(attribute, alias)) +} + +func (b *Builder) StddevPop(attribute, alias string) *Builder { + return b.push(StddevPop(attribute, alias)) +} + +func (b *Builder) StddevSamp(attribute, alias string) *Builder { + return b.push(StddevSamp(attribute, alias)) +} + +func (b *Builder) Variance(attribute, alias string) *Builder { + return b.push(Variance(attribute, alias)) +} + +func (b *Builder) VarPop(attribute, alias string) *Builder { + return b.push(VarPop(attribute, alias)) +} + +func (b *Builder) VarSamp(attribute, alias string) *Builder { + return b.push(VarSamp(attribute, alias)) +} + +func (b *Builder) BitAnd(attribute, alias string) *Builder { + return b.push(BitAnd(attribute, alias)) +} + +func (b *Builder) BitOr(attribute, alias string) *Builder { + return b.push(BitOr(attribute, alias)) +} + +func (b *Builder) BitXor(attribute, alias string) *Builder { + return b.push(BitXor(attribute, alias)) +} + +func (b *Builder) GroupBy(attributes []interface{}) *Builder { + return b.push(GroupBy(attributes)) +} + +func (b *Builder) Having(queries []string) *Builder { + return b.push(Having(queries)) +} + +func (b *Builder) Distinct() *Builder { + return b.push(Distinct()) +} + +func (b *Builder) Join(table, left, right, operator, alias string) *Builder { + return b.push(Join(table, left, right, operator, alias)) +} + +func (b *Builder) LeftJoin(table, left, right, operator, alias string) *Builder { + return b.push(LeftJoin(table, left, right, operator, alias)) +} + +func (b *Builder) RightJoin(table, left, right, operator, alias string) *Builder { + return b.push(RightJoin(table, left, right, operator, alias)) +} + +func (b *Builder) FullOuterJoin(table, left, right, operator, alias string) *Builder { + return b.push(FullOuterJoin(table, left, right, operator, alias)) +} + +func (b *Builder) JoinOn(table, alias string, on []string) *Builder { + return b.push(JoinOn(table, alias, on)) +} + +func (b *Builder) LeftJoinOn(table, alias string, on []string) *Builder { + return b.push(LeftJoinOn(table, alias, on)) +} + +func (b *Builder) RightJoinOn(table, alias string, on []string) *Builder { + return b.push(RightJoinOn(table, alias, on)) +} + +func (b *Builder) FullOuterJoinOn(table, alias string, on []string) *Builder { + return b.push(FullOuterJoinOn(table, alias, on)) +} + +func (b *Builder) CrossJoin(table, alias string) *Builder { + return b.push(CrossJoin(table, alias)) +} + +func (b *Builder) VectorDot(attribute string, vector []float64) *Builder { + return b.push(VectorDot(attribute, vector)) +} + +func (b *Builder) VectorCosine(attribute string, vector []float64) *Builder { + return b.push(VectorCosine(attribute, vector)) +} + +func (b *Builder) VectorEuclidean(attribute string, vector []float64) *Builder { + return b.push(VectorEuclidean(attribute, vector)) +} + +func (b *Builder) Covers(attribute string, values []interface{}) *Builder { + return b.push(Covers(attribute, values)) +} + +func (b *Builder) NotCovers(attribute string, values []interface{}) *Builder { + return b.push(NotCovers(attribute, values)) +} + +func (b *Builder) SpatialEquals(attribute string, values []interface{}) *Builder { + return b.push(SpatialEquals(attribute, values)) +} + +func (b *Builder) NotSpatialEquals(attribute string, values []interface{}) *Builder { + return b.push(NotSpatialEquals(attribute, values)) +} + +func (b *Builder) Page(page, perPage int) *Builder { + return b.push(Page(page, perPage)...) +} diff --git a/templates/go/query_test.go.twig b/templates/go/query_test.go.twig index 294bf2d88f..48af42d692 100644 --- a/templates/go/query_test.go.twig +++ b/templates/go/query_test.go.twig @@ -448,7 +448,7 @@ func TestBuilderBuild(t *testing.T) { Limit(25), } - built := Builder(). + built := NewBuilder(). Equal("status", "published"). LeftJoin("orders", "$id", "customerId", "=", "ord"). Count("*", "total"). @@ -479,17 +479,17 @@ func assertQueriesEqual(t *testing.T, got, expected []string) { } func TestNormalizeBuilderDirect(t *testing.T) { - builder := Builder().Equal("status", "active").Limit(10) + builder := NewBuilder().Equal("status", "active").Limit(10) assertQueriesEqual(t, normalize(builder), builder.Build()) } func TestNormalizeBuilderWrappedInArray(t *testing.T) { - builder := Builder().Equal("status", "active").Limit(10) + builder := NewBuilder().Equal("status", "active").Limit(10) assertQueriesEqual(t, normalize([]interface{}{builder}), builder.Build()) } func TestNormalizeBuildResultWrappedInArray(t *testing.T) { - built := Builder().Equal("status", "active").Limit(10).Build() + built := NewBuilder().Equal("status", "active").Limit(10).Build() assertQueriesEqual(t, normalize([]interface{}{built}), built) } 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 d5f7001fca..8f7dd7e213 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -575,10 +575,10 @@ class Query( * * @returns A new query builder. */ - fun builder() = QueryBuilder() + fun builder() = Builder() internal fun flatten(value: Any?): Any? { - if (value is QueryBuilder) { + if (value is Builder) { return value.build() } if (value is Query) { @@ -595,7 +595,7 @@ class Query( var changed = false for (item in items) { val flat = flatten(item) - if (item is QueryBuilder || item is Query) { + if (item is Builder || item is Query) { if (flat is Collection<*>) { out.addAll(flat) } else { @@ -638,196 +638,196 @@ class Query( } } } -} -class QueryBuilder { - private val queries = mutableListOf() + class Builder { + private val queries = mutableListOf() - fun build(): List = queries.toList() + fun build(): List = queries.toList() - fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) + fun equal(attribute: String, value: Any) = push(Query.equal(attribute, value)) - fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) + fun notEqual(attribute: String, value: Any) = push(Query.notEqual(attribute, value)) - fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) + fun lessThan(attribute: String, value: Any) = push(Query.lessThan(attribute, value)) - fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) + fun lessThanEqual(attribute: String, value: Any) = push(Query.lessThanEqual(attribute, value)) - fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) + fun greaterThan(attribute: String, value: Any) = push(Query.greaterThan(attribute, value)) - fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) + fun greaterThanEqual(attribute: String, value: Any) = push(Query.greaterThanEqual(attribute, value)) - fun search(attribute: String, value: String) = push(Query.search(attribute, value)) + fun search(attribute: String, value: String) = push(Query.search(attribute, value)) - fun isNull(attribute: String) = push(Query.isNull(attribute)) + fun isNull(attribute: String) = push(Query.isNull(attribute)) - fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) + fun isNotNull(attribute: String) = push(Query.isNotNull(attribute)) - fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) + fun between(attribute: String, start: Any, end: Any) = push(Query.between(attribute, start, end)) - fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) + fun startsWith(attribute: String, value: String) = push(Query.startsWith(attribute, value)) - fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) + fun endsWith(attribute: String, value: String) = push(Query.endsWith(attribute, value)) - fun select(attributes: List) = push(Query.select(attributes)) + fun select(attributes: List) = push(Query.select(attributes)) - fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) + fun cursorAfter(documentId: String) = push(Query.cursorAfter(documentId)) - fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) + fun cursorBefore(documentId: String) = push(Query.cursorBefore(documentId)) - fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) + fun orderAsc(attribute: String) = push(Query.orderAsc(attribute)) - fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) + fun orderDesc(attribute: String) = push(Query.orderDesc(attribute)) - fun orderRandom() = push(Query.orderRandom()) + fun orderRandom() = push(Query.orderRandom()) - fun limit(limit: Int) = push(Query.limit(limit)) + fun limit(limit: Int) = push(Query.limit(limit)) - fun offset(offset: Int) = push(Query.offset(offset)) + fun offset(offset: Int) = push(Query.offset(offset)) - fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) + fun contains(attribute: String, value: Any) = push(Query.contains(attribute, value)) - fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) + fun containsAny(attribute: String, value: List) = push(Query.containsAny(attribute, value)) - fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) + fun containsAll(attribute: String, value: List) = push(Query.containsAll(attribute, value)) - fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) + fun notContains(attribute: String, value: Any) = push(Query.notContains(attribute, value)) - fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) + fun notSearch(attribute: String, value: String) = push(Query.notSearch(attribute, value)) - fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) + fun notBetween(attribute: String, start: Any, end: Any) = push(Query.notBetween(attribute, start, end)) - fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) + fun notStartsWith(attribute: String, value: String) = push(Query.notStartsWith(attribute, value)) - fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) + fun notEndsWith(attribute: String, value: String) = push(Query.notEndsWith(attribute, value)) - fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) + fun regex(attribute: String, pattern: String) = push(Query.regex(attribute, pattern)) - fun exists(attributes: List) = push(Query.exists(attributes)) + fun exists(attributes: List) = push(Query.exists(attributes)) - fun notExists(attributes: List) = push(Query.notExists(attributes)) + fun notExists(attributes: List) = push(Query.notExists(attributes)) - fun createdBefore(value: String) = push(Query.createdBefore(value)) + fun createdBefore(value: String) = push(Query.createdBefore(value)) - fun createdAfter(value: String) = push(Query.createdAfter(value)) + fun createdAfter(value: String) = push(Query.createdAfter(value)) - fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) + fun createdBetween(start: String, end: String) = push(Query.createdBetween(start, end)) - fun updatedBefore(value: String) = push(Query.updatedBefore(value)) + fun updatedBefore(value: String) = push(Query.updatedBefore(value)) - fun updatedAfter(value: String) = push(Query.updatedAfter(value)) + fun updatedAfter(value: String) = push(Query.updatedAfter(value)) - fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) + fun updatedBetween(start: String, end: String) = push(Query.updatedBetween(start, end)) - fun or(queries: List) = push(Query.or(queries)) + fun or(queries: List) = push(Query.or(queries)) - fun and(queries: List) = push(Query.and(queries)) + fun and(queries: List) = push(Query.and(queries)) - fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) + fun elemMatch(attribute: String, queries: List) = push(Query.elemMatch(attribute, queries)) - fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) + fun distanceEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceEqual(attribute, values, distance, meters)) - fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) + fun distanceNotEqual(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceNotEqual(attribute, values, distance, meters)) - fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) + fun distanceGreaterThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceGreaterThan(attribute, values, distance, meters)) - fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) + fun distanceLessThan(attribute: String, values: List, distance: Number, meters: Boolean = true) = push(Query.distanceLessThan(attribute, values, distance, meters)) - fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) + fun intersects(attribute: String, values: List) = push(Query.intersects(attribute, values)) - fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) + fun notIntersects(attribute: String, values: List) = push(Query.notIntersects(attribute, values)) - fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) + fun crosses(attribute: String, values: List) = push(Query.crosses(attribute, values)) - fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) + fun notCrosses(attribute: String, values: List) = push(Query.notCrosses(attribute, values)) - fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) + fun overlaps(attribute: String, values: List) = push(Query.overlaps(attribute, values)) - fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) + fun notOverlaps(attribute: String, values: List) = push(Query.notOverlaps(attribute, values)) - fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) + fun touches(attribute: String, values: List) = push(Query.touches(attribute, values)) - fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) + fun notTouches(attribute: String, values: List) = push(Query.notTouches(attribute, values)) - fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) + fun count(attribute: String = "*", alias: String = "") = push(Query.count(attribute, alias)) - fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) + fun countDistinct(attribute: String, alias: String = "") = push(Query.countDistinct(attribute, alias)) - fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) + fun sum(attribute: String, alias: String = "") = push(Query.sum(attribute, alias)) - fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) + fun avg(attribute: String, alias: String = "") = push(Query.avg(attribute, alias)) - fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) + fun min(attribute: String, alias: String = "") = push(Query.min(attribute, alias)) - fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) + fun max(attribute: String, alias: String = "") = push(Query.max(attribute, alias)) - fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) + fun stddev(attribute: String, alias: String = "") = push(Query.stddev(attribute, alias)) - fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) + fun stddevPop(attribute: String, alias: String = "") = push(Query.stddevPop(attribute, alias)) - fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) + fun stddevSamp(attribute: String, alias: String = "") = push(Query.stddevSamp(attribute, alias)) - fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) + fun variance(attribute: String, alias: String = "") = push(Query.variance(attribute, alias)) - fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) + fun varPop(attribute: String, alias: String = "") = push(Query.varPop(attribute, alias)) - fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) + fun varSamp(attribute: String, alias: String = "") = push(Query.varSamp(attribute, alias)) - fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) + fun bitAnd(attribute: String, alias: String = "") = push(Query.bitAnd(attribute, alias)) - fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) + fun bitOr(attribute: String, alias: String = "") = push(Query.bitOr(attribute, alias)) - fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) + fun bitXor(attribute: String, alias: String = "") = push(Query.bitXor(attribute, alias)) - fun groupBy(attributes: List) = push(Query.groupBy(attributes)) + fun groupBy(attributes: List) = push(Query.groupBy(attributes)) - fun having(queries: List) = push(Query.having(queries)) + fun having(queries: List) = push(Query.having(queries)) - fun distinct() = push(Query.distinct()) + fun distinct() = push(Query.distinct()) - fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) + fun join(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.join(table, left, right, operator, alias)) - fun join(table: String, on: List) = push(Query.join(table, on)) + fun join(table: String, on: List) = push(Query.join(table, on)) - fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) + fun join(table: String, alias: String, on: List) = push(Query.join(table, alias, on)) - fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) + fun leftJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.leftJoin(table, left, right, operator, alias)) - fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) + fun leftJoin(table: String, on: List) = push(Query.leftJoin(table, on)) - fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) + fun leftJoin(table: String, alias: String, on: List) = push(Query.leftJoin(table, alias, on)) - fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) + fun rightJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.rightJoin(table, left, right, operator, alias)) - fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) + fun rightJoin(table: String, on: List) = push(Query.rightJoin(table, on)) - fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) + fun rightJoin(table: String, alias: String, on: List) = push(Query.rightJoin(table, alias, on)) - fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) + fun fullOuterJoin(table: String, left: String, right: String, operator: String = "=", alias: String = "") = push(Query.fullOuterJoin(table, left, right, operator, alias)) - fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) + fun fullOuterJoin(table: String, on: List) = push(Query.fullOuterJoin(table, on)) - fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) + fun fullOuterJoin(table: String, alias: String, on: List) = push(Query.fullOuterJoin(table, alias, on)) - fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) + fun crossJoin(table: String, alias: String = "") = push(Query.crossJoin(table, alias)) - fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) + fun vectorDot(attribute: String, vector: List) = push(Query.vectorDot(attribute, vector)) - fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) + fun vectorCosine(attribute: String, vector: List) = push(Query.vectorCosine(attribute, vector)) - fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) + fun vectorEuclidean(attribute: String, vector: List) = push(Query.vectorEuclidean(attribute, vector)) - fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) + fun covers(attribute: String, values: List) = push(Query.covers(attribute, values)) - fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) + fun notCovers(attribute: String, values: List) = push(Query.notCovers(attribute, values)) - fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) + fun spatialEquals(attribute: String, values: List) = push(Query.spatialEquals(attribute, values)) - fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) + fun notSpatialEquals(attribute: String, values: List) = push(Query.notSpatialEquals(attribute, values)) - fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) + fun page(page: Int, perPage: Int = 25) = push(Query.page(page, perPage)) - private fun push(query: String) = apply { queries.add(query) } + private fun push(query: String) = apply { queries.add(query) } - private fun push(query: List) = apply { queries.addAll(query) } + private fun push(query: List) = apply { queries.addAll(query) } + } } diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 0c5b241c99..aa53c49f9b 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -1,6 +1,8 @@ diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index c2896279f8..13763f8338 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -418,11 +418,11 @@ class Query(): @staticmethod def builder(): - return QueryBuilder() + return Query.Builder() @staticmethod def _flatten(value): - if isinstance(value, QueryBuilder): + if isinstance(value, Query.Builder): return value.build() if isinstance(value, Query): return str(value) @@ -433,7 +433,7 @@ class Query(): changed = False for item in value: flat = Query._flatten(item) - if isinstance(item, (QueryBuilder, Query)): + if isinstance(item, (Query.Builder, Query)): if isinstance(flat, list): out.extend(flat) else: @@ -483,266 +483,266 @@ class Query(): values.append(alias) return str(Query(method, table, values)) -class QueryBuilder: - def __init__(self): - self._queries = [] + class Builder: + def __init__(self): + self._queries = [] - def build(self): - return list(self._queries) + def build(self): + return list(self._queries) - def equal(self, attribute, value): - return self._push(Query.equal(attribute, value)) + def equal(self, attribute, value): + return self._push(Query.equal(attribute, value)) - def not_equal(self, attribute, value): - return self._push(Query.not_equal(attribute, value)) + def not_equal(self, attribute, value): + return self._push(Query.not_equal(attribute, value)) - def less_than(self, attribute, value): - return self._push(Query.less_than(attribute, value)) + def less_than(self, attribute, value): + return self._push(Query.less_than(attribute, value)) - def less_than_equal(self, attribute, value): - return self._push(Query.less_than_equal(attribute, value)) + def less_than_equal(self, attribute, value): + return self._push(Query.less_than_equal(attribute, value)) - def greater_than(self, attribute, value): - return self._push(Query.greater_than(attribute, value)) + def greater_than(self, attribute, value): + return self._push(Query.greater_than(attribute, value)) - def greater_than_equal(self, attribute, value): - return self._push(Query.greater_than_equal(attribute, value)) + def greater_than_equal(self, attribute, value): + return self._push(Query.greater_than_equal(attribute, value)) - def search(self, attribute, value): - return self._push(Query.search(attribute, value)) + def search(self, attribute, value): + return self._push(Query.search(attribute, value)) - def is_null(self, attribute): - return self._push(Query.is_null(attribute)) + def is_null(self, attribute): + return self._push(Query.is_null(attribute)) - def is_not_null(self, attribute): - return self._push(Query.is_not_null(attribute)) + def is_not_null(self, attribute): + return self._push(Query.is_not_null(attribute)) - def between(self, attribute, start, end): - return self._push(Query.between(attribute, start, end)) + def between(self, attribute, start, end): + return self._push(Query.between(attribute, start, end)) - def starts_with(self, attribute, value): - return self._push(Query.starts_with(attribute, value)) + def starts_with(self, attribute, value): + return self._push(Query.starts_with(attribute, value)) - def ends_with(self, attribute, value): - return self._push(Query.ends_with(attribute, value)) + def ends_with(self, attribute, value): + return self._push(Query.ends_with(attribute, value)) - def select(self, attributes): - return self._push(Query.select(attributes)) + def select(self, attributes): + return self._push(Query.select(attributes)) - def cursor_after(self, id): - return self._push(Query.cursor_after(id)) + def cursor_after(self, id): + return self._push(Query.cursor_after(id)) - def cursor_before(self, id): - return self._push(Query.cursor_before(id)) + def cursor_before(self, id): + return self._push(Query.cursor_before(id)) - def order_asc(self, attribute): - return self._push(Query.order_asc(attribute)) + def order_asc(self, attribute): + return self._push(Query.order_asc(attribute)) - def order_desc(self, attribute): - return self._push(Query.order_desc(attribute)) + def order_desc(self, attribute): + return self._push(Query.order_desc(attribute)) - def order_random(self): - return self._push(Query.order_random()) + def order_random(self): + return self._push(Query.order_random()) - def limit(self, limit): - return self._push(Query.limit(limit)) + def limit(self, limit): + return self._push(Query.limit(limit)) - def offset(self, offset): - return self._push(Query.offset(offset)) + def offset(self, offset): + return self._push(Query.offset(offset)) - def contains(self, attribute, value): - return self._push(Query.contains(attribute, value)) + def contains(self, attribute, value): + return self._push(Query.contains(attribute, value)) - def contains_any(self, attribute, value): - return self._push(Query.contains_any(attribute, value)) + def contains_any(self, attribute, value): + return self._push(Query.contains_any(attribute, value)) - def contains_all(self, attribute, value): - return self._push(Query.contains_all(attribute, value)) + def contains_all(self, attribute, value): + return self._push(Query.contains_all(attribute, value)) - def not_contains(self, attribute, value): - return self._push(Query.not_contains(attribute, value)) + def not_contains(self, attribute, value): + return self._push(Query.not_contains(attribute, value)) - def not_search(self, attribute, value): - return self._push(Query.not_search(attribute, value)) + def not_search(self, attribute, value): + return self._push(Query.not_search(attribute, value)) - def not_between(self, attribute, start, end): - return self._push(Query.not_between(attribute, start, end)) + def not_between(self, attribute, start, end): + return self._push(Query.not_between(attribute, start, end)) - def not_starts_with(self, attribute, value): - return self._push(Query.not_starts_with(attribute, value)) + def not_starts_with(self, attribute, value): + return self._push(Query.not_starts_with(attribute, value)) - def not_ends_with(self, attribute, value): - return self._push(Query.not_ends_with(attribute, value)) + def not_ends_with(self, attribute, value): + return self._push(Query.not_ends_with(attribute, value)) - def regex(self, attribute, pattern): - return self._push(Query.regex(attribute, pattern)) + def regex(self, attribute, pattern): + return self._push(Query.regex(attribute, pattern)) - def exists(self, attributes): - return self._push(Query.exists(attributes)) + def exists(self, attributes): + return self._push(Query.exists(attributes)) - def not_exists(self, attributes): - return self._push(Query.not_exists(attributes)) + def not_exists(self, attributes): + return self._push(Query.not_exists(attributes)) - def created_before(self, value): - return self._push(Query.created_before(value)) + def created_before(self, value): + return self._push(Query.created_before(value)) - def created_after(self, value): - return self._push(Query.created_after(value)) + def created_after(self, value): + return self._push(Query.created_after(value)) - def created_between(self, start, end): - return self._push(Query.created_between(start, end)) + def created_between(self, start, end): + return self._push(Query.created_between(start, end)) - def updated_before(self, value): - return self._push(Query.updated_before(value)) + def updated_before(self, value): + return self._push(Query.updated_before(value)) - def updated_after(self, value): - return self._push(Query.updated_after(value)) + def updated_after(self, value): + return self._push(Query.updated_after(value)) - def updated_between(self, start, end): - return self._push(Query.updated_between(start, end)) + def updated_between(self, start, end): + return self._push(Query.updated_between(start, end)) - def or_queries(self, queries): - return self._push(Query.or_queries(queries)) + def or_queries(self, queries): + return self._push(Query.or_queries(queries)) - def and_queries(self, queries): - return self._push(Query.and_queries(queries)) + def and_queries(self, queries): + return self._push(Query.and_queries(queries)) - def elem_match(self, attribute, queries): - return self._push(Query.elem_match(attribute, queries)) + def elem_match(self, attribute, queries): + return self._push(Query.elem_match(attribute, queries)) - def distance_equal(self, attribute, values, distance, meters=True): - return self._push(Query.distance_equal(attribute, values, distance, meters)) + def distance_equal(self, attribute, values, distance, meters=True): + return self._push(Query.distance_equal(attribute, values, distance, meters)) - def distance_not_equal(self, attribute, values, distance, meters=True): - return self._push(Query.distance_not_equal(attribute, values, distance, meters)) + def distance_not_equal(self, attribute, values, distance, meters=True): + return self._push(Query.distance_not_equal(attribute, values, distance, meters)) - def distance_greater_than(self, attribute, values, distance, meters=True): - return self._push(Query.distance_greater_than(attribute, values, distance, meters)) + def distance_greater_than(self, attribute, values, distance, meters=True): + return self._push(Query.distance_greater_than(attribute, values, distance, meters)) - def distance_less_than(self, attribute, values, distance, meters=True): - return self._push(Query.distance_less_than(attribute, values, distance, meters)) + def distance_less_than(self, attribute, values, distance, meters=True): + return self._push(Query.distance_less_than(attribute, values, distance, meters)) - def intersects(self, attribute, values): - return self._push(Query.intersects(attribute, values)) + def intersects(self, attribute, values): + return self._push(Query.intersects(attribute, values)) - def not_intersects(self, attribute, values): - return self._push(Query.not_intersects(attribute, values)) + def not_intersects(self, attribute, values): + return self._push(Query.not_intersects(attribute, values)) - def crosses(self, attribute, values): - return self._push(Query.crosses(attribute, values)) + def crosses(self, attribute, values): + return self._push(Query.crosses(attribute, values)) - def not_crosses(self, attribute, values): - return self._push(Query.not_crosses(attribute, values)) + def not_crosses(self, attribute, values): + return self._push(Query.not_crosses(attribute, values)) - def overlaps(self, attribute, values): - return self._push(Query.overlaps(attribute, values)) + def overlaps(self, attribute, values): + return self._push(Query.overlaps(attribute, values)) - def not_overlaps(self, attribute, values): - return self._push(Query.not_overlaps(attribute, values)) + def not_overlaps(self, attribute, values): + return self._push(Query.not_overlaps(attribute, values)) - def touches(self, attribute, values): - return self._push(Query.touches(attribute, values)) + def touches(self, attribute, values): + return self._push(Query.touches(attribute, values)) - def not_touches(self, attribute, values): - return self._push(Query.not_touches(attribute, values)) + def not_touches(self, attribute, values): + return self._push(Query.not_touches(attribute, values)) - def count(self, attribute='*', alias=''): - return self._push(Query.count(attribute, alias)) + def count(self, attribute='*', alias=''): + return self._push(Query.count(attribute, alias)) - def count_distinct(self, attribute, alias=''): - return self._push(Query.count_distinct(attribute, alias)) + def count_distinct(self, attribute, alias=''): + return self._push(Query.count_distinct(attribute, alias)) - def sum(self, attribute, alias=''): - return self._push(Query.sum(attribute, alias)) + def sum(self, attribute, alias=''): + return self._push(Query.sum(attribute, alias)) - def avg(self, attribute, alias=''): - return self._push(Query.avg(attribute, alias)) + def avg(self, attribute, alias=''): + return self._push(Query.avg(attribute, alias)) - def min(self, attribute, alias=''): - return self._push(Query.min(attribute, alias)) + def min(self, attribute, alias=''): + return self._push(Query.min(attribute, alias)) - def max(self, attribute, alias=''): - return self._push(Query.max(attribute, alias)) + def max(self, attribute, alias=''): + return self._push(Query.max(attribute, alias)) - def stddev(self, attribute, alias=''): - return self._push(Query.stddev(attribute, alias)) + def stddev(self, attribute, alias=''): + return self._push(Query.stddev(attribute, alias)) - def stddev_pop(self, attribute, alias=''): - return self._push(Query.stddev_pop(attribute, alias)) + def stddev_pop(self, attribute, alias=''): + return self._push(Query.stddev_pop(attribute, alias)) - def stddev_samp(self, attribute, alias=''): - return self._push(Query.stddev_samp(attribute, alias)) + def stddev_samp(self, attribute, alias=''): + return self._push(Query.stddev_samp(attribute, alias)) - def variance(self, attribute, alias=''): - return self._push(Query.variance(attribute, alias)) + def variance(self, attribute, alias=''): + return self._push(Query.variance(attribute, alias)) - def var_pop(self, attribute, alias=''): - return self._push(Query.var_pop(attribute, alias)) + def var_pop(self, attribute, alias=''): + return self._push(Query.var_pop(attribute, alias)) - def var_samp(self, attribute, alias=''): - return self._push(Query.var_samp(attribute, alias)) + def var_samp(self, attribute, alias=''): + return self._push(Query.var_samp(attribute, alias)) - def bit_and(self, attribute, alias=''): - return self._push(Query.bit_and(attribute, alias)) + def bit_and(self, attribute, alias=''): + return self._push(Query.bit_and(attribute, alias)) - def bit_or(self, attribute, alias=''): - return self._push(Query.bit_or(attribute, alias)) + def bit_or(self, attribute, alias=''): + return self._push(Query.bit_or(attribute, alias)) - def bit_xor(self, attribute, alias=''): - return self._push(Query.bit_xor(attribute, alias)) + def bit_xor(self, attribute, alias=''): + return self._push(Query.bit_xor(attribute, alias)) - def group_by(self, attributes): - return self._push(Query.group_by(attributes)) + def group_by(self, attributes): + return self._push(Query.group_by(attributes)) - def having(self, queries): - return self._push(Query.having(queries)) + def having(self, queries): + return self._push(Query.having(queries)) - def distinct(self): - return self._push(Query.distinct()) + def distinct(self): + return self._push(Query.distinct()) - def join(self, table, left, right='', operator='=', alias=''): - return self._push(Query.join(table, left, right, operator, alias)) + def join(self, table, left, right='', operator='=', alias=''): + return self._push(Query.join(table, left, right, operator, alias)) - def left_join(self, table, left, right='', operator='=', alias=''): - return self._push(Query.left_join(table, left, right, operator, alias)) + def left_join(self, table, left, right='', operator='=', alias=''): + return self._push(Query.left_join(table, left, right, operator, alias)) - def right_join(self, table, left, right='', operator='=', alias=''): - return self._push(Query.right_join(table, left, right, operator, alias)) + def right_join(self, table, left, right='', operator='=', alias=''): + return self._push(Query.right_join(table, left, right, operator, alias)) - def full_outer_join(self, table, left, right='', operator='=', alias=''): - return self._push(Query.full_outer_join(table, left, right, operator, alias)) + def full_outer_join(self, table, left, right='', operator='=', alias=''): + return self._push(Query.full_outer_join(table, left, right, operator, alias)) - def cross_join(self, table, alias=''): - return self._push(Query.cross_join(table, alias)) + def cross_join(self, table, alias=''): + return self._push(Query.cross_join(table, alias)) - def vector_dot(self, attribute, vector): - return self._push(Query.vector_dot(attribute, vector)) + def vector_dot(self, attribute, vector): + return self._push(Query.vector_dot(attribute, vector)) - def vector_cosine(self, attribute, vector): - return self._push(Query.vector_cosine(attribute, vector)) + def vector_cosine(self, attribute, vector): + return self._push(Query.vector_cosine(attribute, vector)) - def vector_euclidean(self, attribute, vector): - return self._push(Query.vector_euclidean(attribute, vector)) + def vector_euclidean(self, attribute, vector): + return self._push(Query.vector_euclidean(attribute, vector)) - def covers(self, attribute, values): - return self._push(Query.covers(attribute, values)) + def covers(self, attribute, values): + return self._push(Query.covers(attribute, values)) - def not_covers(self, attribute, values): - return self._push(Query.not_covers(attribute, values)) + def not_covers(self, attribute, values): + return self._push(Query.not_covers(attribute, values)) - def spatial_equals(self, attribute, values): - return self._push(Query.spatial_equals(attribute, values)) + def spatial_equals(self, attribute, values): + return self._push(Query.spatial_equals(attribute, values)) - def not_spatial_equals(self, attribute, values): - return self._push(Query.not_spatial_equals(attribute, values)) + def not_spatial_equals(self, attribute, values): + return self._push(Query.not_spatial_equals(attribute, values)) - def page(self, page, per_page=25): - return self._push(Query.page(page, per_page)) + def page(self, page, per_page=25): + return self._push(Query.page(page, per_page)) - def _push(self, query): - if isinstance(query, list): - self._queries.extend(query) - else: - self._queries.append(query) + def _push(self, query): + if isinstance(query, list): + self._queries.extend(query) + else: + self._queries.append(query) - return self + return self diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index b510a11214..c5fa21f715 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -593,7 +593,7 @@ export class Query { return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; }; - static builder = (): QueryBuilder => new QueryBuilder(); + static builder = (): Query.Builder => new Query.Builder(); private static createJoin( method: string, @@ -638,14 +638,15 @@ export class Query { export type QueryInput = | string | Query - | QueryBuilder + | Query.Builder | QueryInput[]; /** - * Fluent collector of Query fragments. Call {@link QueryBuilder.build} to + * Fluent collector of Query fragments. Call {@link Query.Builder.build} to * produce the `string[]` expected by list/createQuery methods. */ -export class QueryBuilder { +export namespace Query { + export class Builder { private readonly queries: string[] = []; build(): string[] { @@ -993,12 +994,13 @@ export class QueryBuilder { return this; } } +} export function normalizeQueries(queries?: QueryInput | null): string[] { if (queries == null) { return []; } - if (queries instanceof QueryBuilder) { + if (queries instanceof Query.Builder) { return queries.build(); } if (!Array.isArray(queries)) { @@ -1021,7 +1023,7 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { function isQueryList(value: unknown[]): boolean { return value.every( (item) => - item instanceof QueryBuilder || + item instanceof Query.Builder || item instanceof Query || typeof item === "string" || (Array.isArray(item) && isQueryList(item)) @@ -1029,7 +1031,7 @@ function isQueryList(value: unknown[]): boolean { } export function flattenParam(value: unknown): unknown { - if (value instanceof QueryBuilder || value instanceof Query) { + if (value instanceof Query.Builder || value instanceof Query) { return normalizeQueries(value as QueryInput); } if (Array.isArray(value) && isQueryList(value)) { @@ -1037,3 +1039,6 @@ export function flattenParam(value: unknown): unknown { } return value; } + +export const QueryBuilder = Query.Builder; +export type QueryBuilder = Query.Builder; diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index 3f32636f32..303c1a298f 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -403,11 +403,11 @@ module {{spec.info.title | caseUcfirst}} end def builder - return QueryBuilder.new + return Builder.new end def _flatten(value) - return value.build if value.is_a?(QueryBuilder) + return value.build if value.is_a?(Builder) return value.to_s if value.is_a?(Query) return value unless value.is_a?(Array) @@ -415,7 +415,7 @@ module {{spec.info.title | caseUcfirst}} changed = false value.each do |item| flat = _flatten(item) - if item.is_a?(QueryBuilder) || item.is_a?(Query) + if item.is_a?(Builder) || item.is_a?(Query) if flat.is_a?(Array) out.concat(flat) else @@ -470,357 +470,5 @@ module {{spec.info.title | caseUcfirst}} end end - class QueryBuilder - def initialize - @queries = [] - end - - def build - return @queries.dup - end - - def equal(attribute, value) - return push(Query.equal(attribute, value)) - end - - def not_equal(attribute, value) - return push(Query.not_equal(attribute, value)) - end - - def less_than(attribute, value) - return push(Query.less_than(attribute, value)) - end - - def less_than_equal(attribute, value) - return push(Query.less_than_equal(attribute, value)) - end - - def greater_than(attribute, value) - return push(Query.greater_than(attribute, value)) - end - - def greater_than_equal(attribute, value) - return push(Query.greater_than_equal(attribute, value)) - end - - def search(attribute, value) - return push(Query.search(attribute, value)) - end - - def is_null(attribute) - return push(Query.is_null(attribute)) - end - - def is_not_null(attribute) - return push(Query.is_not_null(attribute)) - end - - def between(attribute, start, ending) - return push(Query.between(attribute, start, ending)) - end - - def starts_with(attribute, value) - return push(Query.starts_with(attribute, value)) - end - - def ends_with(attribute, value) - return push(Query.ends_with(attribute, value)) - end - - def select(attributes) - return push(Query.select(attributes)) - end - - def cursor_after(id) - return push(Query.cursor_after(id)) - end - - def cursor_before(id) - return push(Query.cursor_before(id)) - end - - def order_asc(attribute) - return push(Query.order_asc(attribute)) - end - - def order_desc(attribute) - return push(Query.order_desc(attribute)) - end - - def order_random() - return push(Query.order_random()) - end - - def limit(limit) - return push(Query.limit(limit)) - end - - def offset(offset) - return push(Query.offset(offset)) - end - - def contains(attribute, value) - return push(Query.contains(attribute, value)) - end - - def contains_any(attribute, value) - return push(Query.contains_any(attribute, value)) - end - - def contains_all(attribute, value) - return push(Query.contains_all(attribute, value)) - end - - def not_contains(attribute, value) - return push(Query.not_contains(attribute, value)) - end - - def not_search(attribute, value) - return push(Query.not_search(attribute, value)) - end - - def not_between(attribute, start, ending) - return push(Query.not_between(attribute, start, ending)) - end - - def not_starts_with(attribute, value) - return push(Query.not_starts_with(attribute, value)) - end - - def not_ends_with(attribute, value) - return push(Query.not_ends_with(attribute, value)) - end - - def regex(attribute, pattern) - return push(Query.regex(attribute, pattern)) - end - - def exists(attributes) - return push(Query.exists(attributes)) - end - - def not_exists(attributes) - return push(Query.not_exists(attributes)) - end - - def created_before(value) - return push(Query.created_before(value)) - end - - def created_after(value) - return push(Query.created_after(value)) - end - - def created_between(start, ending) - return push(Query.created_between(start, ending)) - end - - def updated_before(value) - return push(Query.updated_before(value)) - end - - def updated_after(value) - return push(Query.updated_after(value)) - end - - def updated_between(start, ending) - return push(Query.updated_between(start, ending)) - end - - def or(queries) - return push(Query.or(queries)) - end - - def and(queries) - return push(Query.and(queries)) - end - - def elem_match(attribute, queries) - return push(Query.elem_match(attribute, queries)) - end - - def distance_equal(attribute, values, distance, meters = true) - return push(Query.distance_equal(attribute, values, distance, meters)) - end - - def distance_not_equal(attribute, values, distance, meters = true) - return push(Query.distance_not_equal(attribute, values, distance, meters)) - end - - def distance_greater_than(attribute, values, distance, meters = true) - return push(Query.distance_greater_than(attribute, values, distance, meters)) - end - - def distance_less_than(attribute, values, distance, meters = true) - return push(Query.distance_less_than(attribute, values, distance, meters)) - end - - def intersects(attribute, values) - return push(Query.intersects(attribute, values)) - end - - def not_intersects(attribute, values) - return push(Query.not_intersects(attribute, values)) - end - - def crosses(attribute, values) - return push(Query.crosses(attribute, values)) - end - - def not_crosses(attribute, values) - return push(Query.not_crosses(attribute, values)) - end - - def overlaps(attribute, values) - return push(Query.overlaps(attribute, values)) - end - - def not_overlaps(attribute, values) - return push(Query.not_overlaps(attribute, values)) - end - - def touches(attribute, values) - return push(Query.touches(attribute, values)) - end - - def not_touches(attribute, values) - return push(Query.not_touches(attribute, values)) - end - - def count(attribute = '*', alias_name = '') - return push(Query.count(attribute, alias_name)) - end - - def count_distinct(attribute, alias_name = '') - return push(Query.count_distinct(attribute, alias_name)) - end - - def sum(attribute, alias_name = '') - return push(Query.sum(attribute, alias_name)) - end - - def avg(attribute, alias_name = '') - return push(Query.avg(attribute, alias_name)) - end - - def min(attribute, alias_name = '') - return push(Query.min(attribute, alias_name)) - end - - def max(attribute, alias_name = '') - return push(Query.max(attribute, alias_name)) - end - - def stddev(attribute, alias_name = '') - return push(Query.stddev(attribute, alias_name)) - end - - def stddev_pop(attribute, alias_name = '') - return push(Query.stddev_pop(attribute, alias_name)) - end - - def stddev_samp(attribute, alias_name = '') - return push(Query.stddev_samp(attribute, alias_name)) - end - - def variance(attribute, alias_name = '') - return push(Query.variance(attribute, alias_name)) - end - - def var_pop(attribute, alias_name = '') - return push(Query.var_pop(attribute, alias_name)) - end - - def var_samp(attribute, alias_name = '') - return push(Query.var_samp(attribute, alias_name)) - end - - def bit_and(attribute, alias_name = '') - return push(Query.bit_and(attribute, alias_name)) - end - - def bit_or(attribute, alias_name = '') - return push(Query.bit_or(attribute, alias_name)) - end - - def bit_xor(attribute, alias_name = '') - return push(Query.bit_xor(attribute, alias_name)) - end - - def group_by(attributes) - return push(Query.group_by(attributes)) - end - - def having(queries) - return push(Query.having(queries)) - end - - def distinct() - return push(Query.distinct()) - end - - def join(table, left, right = '', operator = '=', alias_name = '') - return push(Query.join(table, left, right, operator, alias_name)) - end - - def left_join(table, left, right = '', operator = '=', alias_name = '') - return push(Query.left_join(table, left, right, operator, alias_name)) - end - - def right_join(table, left, right = '', operator = '=', alias_name = '') - return push(Query.right_join(table, left, right, operator, alias_name)) - end - - def full_outer_join(table, left, right = '', operator = '=', alias_name = '') - return push(Query.full_outer_join(table, left, right, operator, alias_name)) - end - - def cross_join(table, alias_name = '') - return push(Query.cross_join(table, alias_name)) - end - - def vector_dot(attribute, vector) - return push(Query.vector_dot(attribute, vector)) - end - - def vector_cosine(attribute, vector) - return push(Query.vector_cosine(attribute, vector)) - end - - def vector_euclidean(attribute, vector) - return push(Query.vector_euclidean(attribute, vector)) - end - - def covers(attribute, values) - return push(Query.covers(attribute, values)) - end - - def not_covers(attribute, values) - return push(Query.not_covers(attribute, values)) - end - - def spatial_equals(attribute, values) - return push(Query.spatial_equals(attribute, values)) - end - - def not_spatial_equals(attribute, values) - return push(Query.not_spatial_equals(attribute, values)) - end - - def page(page, per_page = 25) - return push(Query.page(page, per_page)) - end - - private - - def push(query) - if query.is_a?(Array) - @queries.concat(query) - else - @queries << query - end - - return self - end - end + require_relative 'query/builder' end diff --git a/templates/ruby/lib/container/query/builder.rb.twig b/templates/ruby/lib/container/query/builder.rb.twig new file mode 100644 index 0000000000..7690e5b38b --- /dev/null +++ b/templates/ruby/lib/container/query/builder.rb.twig @@ -0,0 +1,357 @@ +require 'json' + +module {{spec.info.title | caseUcfirst}} + class Query::Builder + def initialize + @queries = [] + end + + def build + return @queries.dup + end + + def equal(attribute, value) + return push(Query.equal(attribute, value)) + end + + def not_equal(attribute, value) + return push(Query.not_equal(attribute, value)) + end + + def less_than(attribute, value) + return push(Query.less_than(attribute, value)) + end + + def less_than_equal(attribute, value) + return push(Query.less_than_equal(attribute, value)) + end + + def greater_than(attribute, value) + return push(Query.greater_than(attribute, value)) + end + + def greater_than_equal(attribute, value) + return push(Query.greater_than_equal(attribute, value)) + end + + def search(attribute, value) + return push(Query.search(attribute, value)) + end + + def is_null(attribute) + return push(Query.is_null(attribute)) + end + + def is_not_null(attribute) + return push(Query.is_not_null(attribute)) + end + + def between(attribute, start, ending) + return push(Query.between(attribute, start, ending)) + end + + def starts_with(attribute, value) + return push(Query.starts_with(attribute, value)) + end + + def ends_with(attribute, value) + return push(Query.ends_with(attribute, value)) + end + + def select(attributes) + return push(Query.select(attributes)) + end + + def cursor_after(id) + return push(Query.cursor_after(id)) + end + + def cursor_before(id) + return push(Query.cursor_before(id)) + end + + def order_asc(attribute) + return push(Query.order_asc(attribute)) + end + + def order_desc(attribute) + return push(Query.order_desc(attribute)) + end + + def order_random() + return push(Query.order_random()) + end + + def limit(limit) + return push(Query.limit(limit)) + end + + def offset(offset) + return push(Query.offset(offset)) + end + + def contains(attribute, value) + return push(Query.contains(attribute, value)) + end + + def contains_any(attribute, value) + return push(Query.contains_any(attribute, value)) + end + + def contains_all(attribute, value) + return push(Query.contains_all(attribute, value)) + end + + def not_contains(attribute, value) + return push(Query.not_contains(attribute, value)) + end + + def not_search(attribute, value) + return push(Query.not_search(attribute, value)) + end + + def not_between(attribute, start, ending) + return push(Query.not_between(attribute, start, ending)) + end + + def not_starts_with(attribute, value) + return push(Query.not_starts_with(attribute, value)) + end + + def not_ends_with(attribute, value) + return push(Query.not_ends_with(attribute, value)) + end + + def regex(attribute, pattern) + return push(Query.regex(attribute, pattern)) + end + + def exists(attributes) + return push(Query.exists(attributes)) + end + + def not_exists(attributes) + return push(Query.not_exists(attributes)) + end + + def created_before(value) + return push(Query.created_before(value)) + end + + def created_after(value) + return push(Query.created_after(value)) + end + + def created_between(start, ending) + return push(Query.created_between(start, ending)) + end + + def updated_before(value) + return push(Query.updated_before(value)) + end + + def updated_after(value) + return push(Query.updated_after(value)) + end + + def updated_between(start, ending) + return push(Query.updated_between(start, ending)) + end + + def or(queries) + return push(Query.or(queries)) + end + + def and(queries) + return push(Query.and(queries)) + end + + def elem_match(attribute, queries) + return push(Query.elem_match(attribute, queries)) + end + + def distance_equal(attribute, values, distance, meters = true) + return push(Query.distance_equal(attribute, values, distance, meters)) + end + + def distance_not_equal(attribute, values, distance, meters = true) + return push(Query.distance_not_equal(attribute, values, distance, meters)) + end + + def distance_greater_than(attribute, values, distance, meters = true) + return push(Query.distance_greater_than(attribute, values, distance, meters)) + end + + def distance_less_than(attribute, values, distance, meters = true) + return push(Query.distance_less_than(attribute, values, distance, meters)) + end + + def intersects(attribute, values) + return push(Query.intersects(attribute, values)) + end + + def not_intersects(attribute, values) + return push(Query.not_intersects(attribute, values)) + end + + def crosses(attribute, values) + return push(Query.crosses(attribute, values)) + end + + def not_crosses(attribute, values) + return push(Query.not_crosses(attribute, values)) + end + + def overlaps(attribute, values) + return push(Query.overlaps(attribute, values)) + end + + def not_overlaps(attribute, values) + return push(Query.not_overlaps(attribute, values)) + end + + def touches(attribute, values) + return push(Query.touches(attribute, values)) + end + + def not_touches(attribute, values) + return push(Query.not_touches(attribute, values)) + end + + def count(attribute = '*', alias_name = '') + return push(Query.count(attribute, alias_name)) + end + + def count_distinct(attribute, alias_name = '') + return push(Query.count_distinct(attribute, alias_name)) + end + + def sum(attribute, alias_name = '') + return push(Query.sum(attribute, alias_name)) + end + + def avg(attribute, alias_name = '') + return push(Query.avg(attribute, alias_name)) + end + + def min(attribute, alias_name = '') + return push(Query.min(attribute, alias_name)) + end + + def max(attribute, alias_name = '') + return push(Query.max(attribute, alias_name)) + end + + def stddev(attribute, alias_name = '') + return push(Query.stddev(attribute, alias_name)) + end + + def stddev_pop(attribute, alias_name = '') + return push(Query.stddev_pop(attribute, alias_name)) + end + + def stddev_samp(attribute, alias_name = '') + return push(Query.stddev_samp(attribute, alias_name)) + end + + def variance(attribute, alias_name = '') + return push(Query.variance(attribute, alias_name)) + end + + def var_pop(attribute, alias_name = '') + return push(Query.var_pop(attribute, alias_name)) + end + + def var_samp(attribute, alias_name = '') + return push(Query.var_samp(attribute, alias_name)) + end + + def bit_and(attribute, alias_name = '') + return push(Query.bit_and(attribute, alias_name)) + end + + def bit_or(attribute, alias_name = '') + return push(Query.bit_or(attribute, alias_name)) + end + + def bit_xor(attribute, alias_name = '') + return push(Query.bit_xor(attribute, alias_name)) + end + + def group_by(attributes) + return push(Query.group_by(attributes)) + end + + def having(queries) + return push(Query.having(queries)) + end + + def distinct() + return push(Query.distinct()) + end + + def join(table, left, right = '', operator = '=', alias_name = '') + return push(Query.join(table, left, right, operator, alias_name)) + end + + def left_join(table, left, right = '', operator = '=', alias_name = '') + return push(Query.left_join(table, left, right, operator, alias_name)) + end + + def right_join(table, left, right = '', operator = '=', alias_name = '') + return push(Query.right_join(table, left, right, operator, alias_name)) + end + + def full_outer_join(table, left, right = '', operator = '=', alias_name = '') + return push(Query.full_outer_join(table, left, right, operator, alias_name)) + end + + def cross_join(table, alias_name = '') + return push(Query.cross_join(table, alias_name)) + end + + def vector_dot(attribute, vector) + return push(Query.vector_dot(attribute, vector)) + end + + def vector_cosine(attribute, vector) + return push(Query.vector_cosine(attribute, vector)) + end + + def vector_euclidean(attribute, vector) + return push(Query.vector_euclidean(attribute, vector)) + end + + def covers(attribute, values) + return push(Query.covers(attribute, values)) + end + + def not_covers(attribute, values) + return push(Query.not_covers(attribute, values)) + end + + def spatial_equals(attribute, values) + return push(Query.spatial_equals(attribute, values)) + end + + def not_spatial_equals(attribute, values) + return push(Query.not_spatial_equals(attribute, values)) + end + + def page(page, per_page = 25) + return push(Query.page(page, per_page)) + end + + private + + def push(query) + if query.is_a?(Array) + @queries.concat(query) + else + @queries << query + end + + return self + end + end +end diff --git a/templates/rust/src/query/builder.rs.twig b/templates/rust/src/query/builder.rs.twig new file mode 100644 index 0000000000..71562caafd --- /dev/null +++ b/templates/rust/src/query/builder.rs.twig @@ -0,0 +1,501 @@ +use serde::Serialize; +use serde_json::Value; + +use super::Query; + +#[derive(Debug, Clone, Default)] +pub struct Builder { + queries: Vec, +} + +impl Builder { + pub fn new() -> Self { + Self { queries: Vec::new() } + } + + pub fn build(&self) -> Vec { + self.queries.iter().map(|query| query.to_string()).collect() + } + + pub fn equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::equal(attribute, value)); + self + } + + pub fn not_equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_equal(attribute, value)); + self + } + + pub fn less_than, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::less_than(attribute, value)); + self + } + + pub fn less_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::less_than_equal(attribute, value)); + self + } + + pub fn greater_than, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::greater_than(attribute, value)); + self + } + + pub fn greater_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::greater_than_equal(attribute, value)); + self + } + + pub fn is_null>(mut self, attribute: S) -> Self { + self.queries.push(Query::is_null(attribute)); + self + } + + pub fn is_not_null>(mut self, attribute: S) -> Self { + self.queries.push(Query::is_not_null(attribute)); + self + } + + pub fn between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { + self.queries.push(Query::between(attribute, start, end)); + self + } + + pub fn starts_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::starts_with(attribute, value)); + self + } + + pub fn ends_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::ends_with(attribute, value)); + self + } + + pub fn select, S: Into>(mut self, attributes: I) -> Self { + self.queries.push(Query::select(attributes)); + self + } + + pub fn search, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::search(attribute, value)); + self + } + + pub fn not_search, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_search(attribute, value)); + self + } + + pub fn order_asc>(mut self, attribute: S) -> Self { + self.queries.push(Query::order_asc(attribute)); + self + } + + pub fn order_desc>(mut self, attribute: S) -> Self { + self.queries.push(Query::order_desc(attribute)); + self + } + + pub fn order_random(mut self) -> Self { + self.queries.push(Query::order_random()); + self + } + + pub fn cursor_after>(mut self, document_id: S) -> Self { + self.queries.push(Query::cursor_after(document_id)); + self + } + + pub fn cursor_before>(mut self, document_id: S) -> Self { + self.queries.push(Query::cursor_before(document_id)); + self + } + + pub fn limit(mut self, value: u32) -> Self { + self.queries.push(Query::limit(value)); + self + } + + pub fn offset(mut self, value: u32) -> Self { + self.queries.push(Query::offset(value)); + self + } + + pub fn contains, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::contains(attribute, value)); + self + } + + pub fn contains_any, V: Into>(mut self, attribute: S, values: Vec) -> Self { + self.queries.push(Query::contains_any(attribute, values)); + self + } + + pub fn contains_all, V: Into>(mut self, attribute: S, values: Vec) -> Self { + self.queries.push(Query::contains_all(attribute, values)); + self + } + + pub fn not_contains, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_contains(attribute, value)); + self + } + + pub fn not_between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { + self.queries.push(Query::not_between(attribute, start, end)); + self + } + + pub fn not_starts_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_starts_with(attribute, value)); + self + } + + pub fn not_ends_with, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::not_ends_with(attribute, value)); + self + } + + pub fn created_before>(mut self, value: V) -> Self { + self.queries.push(Query::created_before(value)); + self + } + + pub fn created_after>(mut self, value: V) -> Self { + self.queries.push(Query::created_after(value)); + self + } + + pub fn created_between>(mut self, start: V, end: V) -> Self { + self.queries.push(Query::created_between(start, end)); + self + } + + pub fn updated_before>(mut self, value: V) -> Self { + self.queries.push(Query::updated_before(value)); + self + } + + pub fn updated_after>(mut self, value: V) -> Self { + self.queries.push(Query::updated_after(value)); + self + } + + pub fn updated_between>(mut self, start: V, end: V) -> Self { + self.queries.push(Query::updated_between(start, end)); + self + } + + pub fn distance_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_equal(attribute, values, distance, meters)); + self + } + + pub fn distance_not_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_not_equal(attribute, values, distance, meters)); + self + } + + pub fn distance_greater_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_greater_than(attribute, values, distance, meters)); + self + } + + pub fn distance_less_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { + self.queries.push(Query::distance_less_than(attribute, values, distance, meters)); + self + } + + pub fn intersects, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::intersects(attribute, values)); + self + } + + pub fn not_intersects, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_intersects(attribute, values)); + self + } + + pub fn crosses, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::crosses(attribute, values)); + self + } + + pub fn not_crosses, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_crosses(attribute, values)); + self + } + + pub fn overlaps, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::overlaps(attribute, values)); + self + } + + pub fn not_overlaps, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_overlaps(attribute, values)); + self + } + + pub fn touches, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::touches(attribute, values)); + self + } + + pub fn not_touches, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_touches(attribute, values)); + self + } + + pub fn or(mut self, queries: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::or(queries)); + self + } + + pub fn and(mut self, queries: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::and(queries)); + self + } + + pub fn regex, V: Into>(mut self, attribute: S, value: V) -> Self { + self.queries.push(Query::regex(attribute, value)); + self + } + + pub fn exists>(mut self, attributes: Vec) -> Self { + self.queries.push(Query::exists(attributes)); + self + } + + pub fn not_exists>(mut self, attributes: Vec) -> Self { + self.queries.push(Query::not_exists(attributes)); + self + } + + pub fn elem_match(mut self, attribute: S, queries: I) -> Self + where + S: Into, + I: IntoIterator, + Q: AsRef, + { + self.queries.push(Query::elem_match(attribute, queries)); + self + } + + pub fn count(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::count(attribute, alias)); + self + } + + pub fn count_distinct(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::count_distinct(attribute, alias)); + self + } + + pub fn sum(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::sum(attribute, alias)); + self + } + + pub fn avg(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::avg(attribute, alias)); + self + } + + pub fn min(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::min(attribute, alias)); + self + } + + pub fn max(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::max(attribute, alias)); + self + } + + pub fn stddev(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::stddev(attribute, alias)); + self + } + + pub fn stddev_pop(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::stddev_pop(attribute, alias)); + self + } + + pub fn stddev_samp(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::stddev_samp(attribute, alias)); + self + } + + pub fn variance(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::variance(attribute, alias)); + self + } + + pub fn var_pop(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::var_pop(attribute, alias)); + self + } + + pub fn var_samp(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::var_samp(attribute, alias)); + self + } + + pub fn bit_and(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::bit_and(attribute, alias)); + self + } + + pub fn bit_or(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::bit_or(attribute, alias)); + self + } + + pub fn bit_xor(mut self, attribute: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::bit_xor(attribute, alias)); + self + } + + pub fn group_by(mut self, attributes: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.queries.push(Query::group_by(attributes)); + self + } + + pub fn having(mut self, queries: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::having(queries)); + self + } + + pub fn distinct(mut self) -> Self { + self.queries.push(Query::distinct()); + self + } + + pub fn join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::join(table, left, right, operator, alias)); + self + } + + pub fn left_join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::left_join(table, left, right, operator, alias)); + self + } + + pub fn left_join_on(mut self, table: impl Into, alias: impl Into, on: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + self.queries.push(Query::left_join_on(table, alias, on)); + self + } + + pub fn right_join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::right_join(table, left, right, operator, alias)); + self + } + + pub fn full_outer_join( + mut self, + table: impl Into, + left: impl Into, + right: impl Into, + operator: impl Into, + alias: impl Into, + ) -> Self { + self.queries.push(Query::full_outer_join(table, left, right, operator, alias)); + self + } + + pub fn cross_join(mut self, table: impl Into, alias: impl Into) -> Self { + self.queries.push(Query::cross_join(table, alias)); + self + } + + pub fn vector_dot, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); + self.queries.push(Query::vector_dot(attribute, values)); + self + } + + pub fn vector_cosine, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); + self.queries.push(Query::vector_cosine(attribute, values)); + self + } + + pub fn vector_euclidean, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); + self.queries.push(Query::vector_euclidean(attribute, values)); + self + } + + pub fn covers, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::covers(attribute, values)); + self + } + + pub fn not_covers, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_covers(attribute, values)); + self + } + + pub fn spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::spatial_equals(attribute, values)); + self + } + + pub fn not_spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { + self.queries.push(Query::not_spatial_equals(attribute, values)); + self + } + + pub fn page(mut self, page: u32, per_page: u32) -> Self { + self.queries.extend(Query::page(page, per_page)); + self + } +} + +impl Serialize for Builder { + fn serialize(&self, serializer: S) -> Result { + self.build().serialize(serializer) + } +} diff --git a/templates/rust/src/query.rs.twig b/templates/rust/src/query/mod.rs.twig similarity index 75% rename from templates/rust/src/query.rs.twig rename to templates/rust/src/query/mod.rs.twig index 90c961a74b..f93134e2d5 100644 --- a/templates/rust/src/query.rs.twig +++ b/templates/rust/src/query/mod.rs.twig @@ -3,6 +3,9 @@ use serde::Serialize; use serde_json::Value; +mod builder; +pub use builder::Builder; + /// Query builder for filtering and sorting database queries #[derive(Debug, Clone, Serialize)] pub struct Query { @@ -482,8 +485,8 @@ impl Query { vec![Self::limit(per_page), Self::offset((page - 1) * per_page)] } - pub fn builder() -> QueryBuilder { - QueryBuilder::new() + pub fn builder() -> Builder { + Builder::new() } pub(crate) fn normalize(value: impl Serialize) -> Vec { @@ -576,503 +579,6 @@ impl std::fmt::Display for Query { } } -#[derive(Debug, Clone, Default)] -pub struct QueryBuilder { - queries: Vec, -} - -impl QueryBuilder { - pub fn new() -> Self { - Self { queries: Vec::new() } - } - - pub fn build(&self) -> Vec { - self.queries.iter().map(|query| query.to_string()).collect() - } - - pub fn equal, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::equal(attribute, value)); - self - } - - pub fn not_equal, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::not_equal(attribute, value)); - self - } - - pub fn less_than, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::less_than(attribute, value)); - self - } - - pub fn less_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::less_than_equal(attribute, value)); - self - } - - pub fn greater_than, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::greater_than(attribute, value)); - self - } - - pub fn greater_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::greater_than_equal(attribute, value)); - self - } - - pub fn is_null>(mut self, attribute: S) -> Self { - self.queries.push(Query::is_null(attribute)); - self - } - - pub fn is_not_null>(mut self, attribute: S) -> Self { - self.queries.push(Query::is_not_null(attribute)); - self - } - - pub fn between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { - self.queries.push(Query::between(attribute, start, end)); - self - } - - pub fn starts_with, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::starts_with(attribute, value)); - self - } - - pub fn ends_with, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::ends_with(attribute, value)); - self - } - - pub fn select, S: Into>(mut self, attributes: I) -> Self { - self.queries.push(Query::select(attributes)); - self - } - - pub fn search, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::search(attribute, value)); - self - } - - pub fn not_search, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::not_search(attribute, value)); - self - } - - pub fn order_asc>(mut self, attribute: S) -> Self { - self.queries.push(Query::order_asc(attribute)); - self - } - - pub fn order_desc>(mut self, attribute: S) -> Self { - self.queries.push(Query::order_desc(attribute)); - self - } - - pub fn order_random(mut self) -> Self { - self.queries.push(Query::order_random()); - self - } - - pub fn cursor_after>(mut self, document_id: S) -> Self { - self.queries.push(Query::cursor_after(document_id)); - self - } - - pub fn cursor_before>(mut self, document_id: S) -> Self { - self.queries.push(Query::cursor_before(document_id)); - self - } - - pub fn limit(mut self, value: u32) -> Self { - self.queries.push(Query::limit(value)); - self - } - - pub fn offset(mut self, value: u32) -> Self { - self.queries.push(Query::offset(value)); - self - } - - pub fn contains, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::contains(attribute, value)); - self - } - - pub fn contains_any, V: Into>(mut self, attribute: S, values: Vec) -> Self { - self.queries.push(Query::contains_any(attribute, values)); - self - } - - pub fn contains_all, V: Into>(mut self, attribute: S, values: Vec) -> Self { - self.queries.push(Query::contains_all(attribute, values)); - self - } - - pub fn not_contains, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::not_contains(attribute, value)); - self - } - - pub fn not_between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { - self.queries.push(Query::not_between(attribute, start, end)); - self - } - - pub fn not_starts_with, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::not_starts_with(attribute, value)); - self - } - - pub fn not_ends_with, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::not_ends_with(attribute, value)); - self - } - - pub fn created_before>(mut self, value: V) -> Self { - self.queries.push(Query::created_before(value)); - self - } - - pub fn created_after>(mut self, value: V) -> Self { - self.queries.push(Query::created_after(value)); - self - } - - pub fn created_between>(mut self, start: V, end: V) -> Self { - self.queries.push(Query::created_between(start, end)); - self - } - - pub fn updated_before>(mut self, value: V) -> Self { - self.queries.push(Query::updated_before(value)); - self - } - - pub fn updated_after>(mut self, value: V) -> Self { - self.queries.push(Query::updated_after(value)); - self - } - - pub fn updated_between>(mut self, start: V, end: V) -> Self { - self.queries.push(Query::updated_between(start, end)); - self - } - - pub fn distance_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_equal(attribute, values, distance, meters)); - self - } - - pub fn distance_not_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_not_equal(attribute, values, distance, meters)); - self - } - - pub fn distance_greater_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_greater_than(attribute, values, distance, meters)); - self - } - - pub fn distance_less_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_less_than(attribute, values, distance, meters)); - self - } - - pub fn intersects, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::intersects(attribute, values)); - self - } - - pub fn not_intersects, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::not_intersects(attribute, values)); - self - } - - pub fn crosses, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::crosses(attribute, values)); - self - } - - pub fn not_crosses, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::not_crosses(attribute, values)); - self - } - - pub fn overlaps, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::overlaps(attribute, values)); - self - } - - pub fn not_overlaps, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::not_overlaps(attribute, values)); - self - } - - pub fn touches, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::touches(attribute, values)); - self - } - - pub fn not_touches, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::not_touches(attribute, values)); - self - } - - pub fn or(mut self, queries: I) -> Self - where - I: IntoIterator, - S: AsRef, - { - self.queries.push(Query::or(queries)); - self - } - - pub fn and(mut self, queries: I) -> Self - where - I: IntoIterator, - S: AsRef, - { - self.queries.push(Query::and(queries)); - self - } - - pub fn regex, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::regex(attribute, value)); - self - } - - pub fn exists>(mut self, attributes: Vec) -> Self { - self.queries.push(Query::exists(attributes)); - self - } - - pub fn not_exists>(mut self, attributes: Vec) -> Self { - self.queries.push(Query::not_exists(attributes)); - self - } - - pub fn elem_match(mut self, attribute: S, queries: I) -> Self - where - S: Into, - I: IntoIterator, - Q: AsRef, - { - self.queries.push(Query::elem_match(attribute, queries)); - self - } - - pub fn count(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::count(attribute, alias)); - self - } - - pub fn count_distinct(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::count_distinct(attribute, alias)); - self - } - - pub fn sum(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::sum(attribute, alias)); - self - } - - pub fn avg(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::avg(attribute, alias)); - self - } - - pub fn min(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::min(attribute, alias)); - self - } - - pub fn max(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::max(attribute, alias)); - self - } - - pub fn stddev(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::stddev(attribute, alias)); - self - } - - pub fn stddev_pop(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::stddev_pop(attribute, alias)); - self - } - - pub fn stddev_samp(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::stddev_samp(attribute, alias)); - self - } - - pub fn variance(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::variance(attribute, alias)); - self - } - - pub fn var_pop(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::var_pop(attribute, alias)); - self - } - - pub fn var_samp(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::var_samp(attribute, alias)); - self - } - - pub fn bit_and(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::bit_and(attribute, alias)); - self - } - - pub fn bit_or(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::bit_or(attribute, alias)); - self - } - - pub fn bit_xor(mut self, attribute: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::bit_xor(attribute, alias)); - self - } - - pub fn group_by(mut self, attributes: I) -> Self - where - I: IntoIterator, - S: Into, - { - self.queries.push(Query::group_by(attributes)); - self - } - - pub fn having(mut self, queries: I) -> Self - where - I: IntoIterator, - S: AsRef, - { - self.queries.push(Query::having(queries)); - self - } - - pub fn distinct(mut self) -> Self { - self.queries.push(Query::distinct()); - self - } - - pub fn join( - mut self, - table: impl Into, - left: impl Into, - right: impl Into, - operator: impl Into, - alias: impl Into, - ) -> Self { - self.queries.push(Query::join(table, left, right, operator, alias)); - self - } - - pub fn left_join( - mut self, - table: impl Into, - left: impl Into, - right: impl Into, - operator: impl Into, - alias: impl Into, - ) -> Self { - self.queries.push(Query::left_join(table, left, right, operator, alias)); - self - } - - pub fn left_join_on(mut self, table: impl Into, alias: impl Into, on: I) -> Self - where - I: IntoIterator, - S: AsRef, - { - self.queries.push(Query::left_join_on(table, alias, on)); - self - } - - pub fn right_join( - mut self, - table: impl Into, - left: impl Into, - right: impl Into, - operator: impl Into, - alias: impl Into, - ) -> Self { - self.queries.push(Query::right_join(table, left, right, operator, alias)); - self - } - - pub fn full_outer_join( - mut self, - table: impl Into, - left: impl Into, - right: impl Into, - operator: impl Into, - alias: impl Into, - ) -> Self { - self.queries.push(Query::full_outer_join(table, left, right, operator, alias)); - self - } - - pub fn cross_join(mut self, table: impl Into, alias: impl Into) -> Self { - self.queries.push(Query::cross_join(table, alias)); - self - } - - pub fn vector_dot, N: Into>(mut self, attribute: S, vector: Vec) -> Self { - let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); - self.queries.push(Query::vector_dot(attribute, values)); - self - } - - pub fn vector_cosine, N: Into>(mut self, attribute: S, vector: Vec) -> Self { - let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); - self.queries.push(Query::vector_cosine(attribute, values)); - self - } - - pub fn vector_euclidean, N: Into>(mut self, attribute: S, vector: Vec) -> Self { - let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); - self.queries.push(Query::vector_euclidean(attribute, values)); - self - } - - pub fn covers, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::covers(attribute, values)); - self - } - - pub fn not_covers, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::not_covers(attribute, values)); - self - } - - pub fn spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::spatial_equals(attribute, values)); - self - } - - pub fn not_spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::not_spatial_equals(attribute, values)); - self - } - - pub fn page(mut self, page: u32, per_page: u32) -> Self { - self.queries.extend(Query::page(page, per_page)); - self - } -} - -impl Serialize for QueryBuilder { - fn serialize(&self, serializer: S) -> Result { - self.build().serialize(serializer) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/templates/skills/dotnet.md.twig b/templates/skills/dotnet.md.twig index 5c61a2b153..56397be528 100644 --- a/templates/skills/dotnet.md.twig +++ b/templates/skills/dotnet.md.twig @@ -154,7 +154,7 @@ Query.LeftJoin("orders", "ord", new List { }) // Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. -var queries = Query.Builder() +var queries = new Query.Builder() .Equal("status", "published") .LeftJoin("orders", "$id", "customerId", "=", "ord") .Count("*", "total") @@ -163,7 +163,7 @@ var queries = Query.Builder() .Build(); await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", queries); -// await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", new[] { Query.Builder().Equal("status", "active") }); +// await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", new[] { new Query.Builder().Equal("status", "active") }); // POST CreateQuery is the large-payload path when it exists; ListRows still accepts the same query strings. await tablesDB.CreateQuery("[DATABASE_ID]", "[TABLE_ID]", queries); diff --git a/templates/skills/go.md.twig b/templates/skills/go.md.twig index 52fe57cb91..d737a1b1a6 100644 --- a/templates/skills/go.md.twig +++ b/templates/skills/go.md.twig @@ -179,7 +179,7 @@ query.LeftJoinOn("orders", "ord", []string{ }) // Fluent builder. Wrapping the builder in an extra slice is OK — the SDK flattens it. -queries := query.Builder(). +queries := query.NewBuilder(). Equal("status", "published"). LeftJoin("orders", "$id", "customerId", "=", "ord"). Count("*", "total"). diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index a64dd3d0e9..53eee30343 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -881,12 +881,12 @@ public struct Query : Codable, CustomStringConvertible { return [limit(perPage), offset((page - 1) * perPage)] } - public static func builder() -> QueryBuilder { - return QueryBuilder() + public static func builder() -> Builder { + return Builder() } static func flatten(_ value: Any?) -> Any? { - if let builder = value as? QueryBuilder { + if let builder = value as? Builder { return builder.build() } if let query = value as? Query { @@ -906,7 +906,7 @@ public struct Query : Codable, CustomStringConvertible { var changed = false for item in items { let flat = flatten(item) - if item is QueryBuilder || item is Query { + if item is Builder || item is Query { if let list = flat as? [Any] { out.append(contentsOf: list) } else if let flat { @@ -975,500 +975,3 @@ public struct Query : Codable, CustomStringConvertible { } } } - -public class QueryBuilder { - private var queries: [String] = [] - - public init() {} - - public func build() -> [String] { - return queries - } - - @discardableResult - public func equal(_ attribute: String, value: Any) -> QueryBuilder { - return push(Query.equal(attribute, value: value)) - } - - @discardableResult - public func notEqual(_ attribute: String, value: Any) -> QueryBuilder { - return push(Query.notEqual(attribute, value: value)) - } - - @discardableResult - public func lessThan(_ attribute: String, value: Any) -> QueryBuilder { - return push(Query.lessThan(attribute, value: value)) - } - - @discardableResult - public func lessThanEqual(attribute: String, value: Any) -> QueryBuilder { - return push(Query.lessThanEqual(attribute: attribute, value: value)) - } - - @discardableResult - public func greaterThan(_ attribute: String, value: Any) -> QueryBuilder { - return push(Query.greaterThan(attribute, value: value)) - } - - @discardableResult - public func greaterThanEqual(_ attribute: String, value: Any) -> QueryBuilder { - return push(Query.greaterThanEqual(attribute, value: value)) - } - - @discardableResult - public func search(_ attribute: String, value: String) -> QueryBuilder { - return push(Query.search(attribute, value: value)) - } - - @discardableResult - public func isNull(_ attribute: String) -> QueryBuilder { - return push(Query.isNull(attribute)) - } - - @discardableResult - public func isNotNull(_ attribute: String) -> QueryBuilder { - return push(Query.isNotNull(attribute)) - } - - @discardableResult - public func between(_ attribute: String, start: Int, end: Int) -> QueryBuilder { - return push(Query.between(attribute, start: start, end: end)) - } - - @discardableResult - public func between(_ attribute: String, start: Double, end: Double) -> QueryBuilder { - return push(Query.between(attribute, start: start, end: end)) - } - - @discardableResult - public func between(_ attribute: String, start: String, end: String) -> QueryBuilder { - return push(Query.between(attribute, start: start, end: end)) - } - - @discardableResult - public func startsWith(_ attribute: String, value: String) -> QueryBuilder { - return push(Query.startsWith(attribute, value: value)) - } - - @discardableResult - public func endsWith(_ attribute: String, value: String) -> QueryBuilder { - return push(Query.endsWith(attribute, value: value)) - } - - @discardableResult - public func select(_ attributes: [String]) -> QueryBuilder { - return push(Query.select(attributes)) - } - - @discardableResult - public func cursorAfter(_ id: String) -> QueryBuilder { - return push(Query.cursorAfter(id)) - } - - @discardableResult - public func cursorBefore(_ id: String) -> QueryBuilder { - return push(Query.cursorBefore(id)) - } - - @discardableResult - public func orderAsc(_ attribute: String) -> QueryBuilder { - return push(Query.orderAsc(attribute)) - } - - @discardableResult - public func orderDesc(_ attribute: String) -> QueryBuilder { - return push(Query.orderDesc(attribute)) - } - - @discardableResult - public func orderRandom() -> QueryBuilder { - return push(Query.orderRandom()) - } - - @discardableResult - public func limit(_ limit: Int) -> QueryBuilder { - return push(Query.limit(limit)) - } - - @discardableResult - public func offset(_ offset: Int) -> QueryBuilder { - return push(Query.offset(offset)) - } - - @discardableResult - public func contains(_ attribute: String, value: Any) -> QueryBuilder { - return push(Query.contains(attribute, value: value)) - } - - @discardableResult - public func containsAny(_ attribute: String, value: [Any]) -> QueryBuilder { - return push(Query.containsAny(attribute, value: value)) - } - - @discardableResult - public func containsAll(_ attribute: String, value: [Any]) -> QueryBuilder { - return push(Query.containsAll(attribute, value: value)) - } - - @discardableResult - public func notContains(_ attribute: String, value: Any) -> QueryBuilder { - return push(Query.notContains(attribute, value: value)) - } - - @discardableResult - public func notSearch(_ attribute: String, value: String) -> QueryBuilder { - return push(Query.notSearch(attribute, value: value)) - } - - @discardableResult - public func notBetween(_ attribute: String, start: Int, end: Int) -> QueryBuilder { - return push(Query.notBetween(attribute, start: start, end: end)) - } - - @discardableResult - public func notBetween(_ attribute: String, start: Double, end: Double) -> QueryBuilder { - return push(Query.notBetween(attribute, start: start, end: end)) - } - - @discardableResult - public func notBetween(_ attribute: String, start: String, end: String) -> QueryBuilder { - return push(Query.notBetween(attribute, start: start, end: end)) - } - - @discardableResult - public func notStartsWith(_ attribute: String, value: String) -> QueryBuilder { - return push(Query.notStartsWith(attribute, value: value)) - } - - @discardableResult - public func notEndsWith(_ attribute: String, value: String) -> QueryBuilder { - return push(Query.notEndsWith(attribute, value: value)) - } - - @discardableResult - public func regex(_ attribute: String, pattern: String) -> QueryBuilder { - return push(Query.regex(attribute, pattern: pattern)) - } - - @discardableResult - public func exists(_ attributes: [String]) -> QueryBuilder { - return push(Query.exists(attributes)) - } - - @discardableResult - public func notExists(_ attributes: [String]) -> QueryBuilder { - return push(Query.notExists(attributes)) - } - - @discardableResult - public func createdBefore(_ value: String) -> QueryBuilder { - return push(Query.createdBefore(value)) - } - - @discardableResult - public func createdAfter(_ value: String) -> QueryBuilder { - return push(Query.createdAfter(value)) - } - - @discardableResult - public func createdBetween(_ start: String, _ end: String) -> QueryBuilder { - return push(Query.createdBetween(start, end)) - } - - @discardableResult - public func updatedBefore(_ value: String) -> QueryBuilder { - return push(Query.updatedBefore(value)) - } - - @discardableResult - public func updatedAfter(_ value: String) -> QueryBuilder { - return push(Query.updatedAfter(value)) - } - - @discardableResult - public func updatedBetween(_ start: String, _ end: String) -> QueryBuilder { - return push(Query.updatedBetween(start, end)) - } - - @discardableResult - public func or(_ queries: [String]) -> QueryBuilder { - return push(Query.or(queries)) - } - - @discardableResult - public func and(_ queries: [String]) -> QueryBuilder { - return push(Query.and(queries)) - } - - @discardableResult - public func elemMatch(_ attribute: String, queries: [String]) -> QueryBuilder { - return push(Query.elemMatch(attribute, queries: queries)) - } - - @discardableResult - public func distanceEqual(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { - return push(Query.distanceEqual(attribute, values: values, distance: distance, meters: meters)) - } - - @discardableResult - public func distanceNotEqual(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { - return push(Query.distanceNotEqual(attribute, values: values, distance: distance, meters: meters)) - } - - @discardableResult - public func distanceGreaterThan(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { - return push(Query.distanceGreaterThan(attribute, values: values, distance: distance, meters: meters)) - } - - @discardableResult - public func distanceLessThan(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> QueryBuilder { - return push(Query.distanceLessThan(attribute, values: values, distance: distance, meters: meters)) - } - - @discardableResult - public func intersects(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.intersects(attribute, values: values)) - } - - @discardableResult - public func notIntersects(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.notIntersects(attribute, values: values)) - } - - @discardableResult - public func crosses(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.crosses(attribute, values: values)) - } - - @discardableResult - public func notCrosses(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.notCrosses(attribute, values: values)) - } - - @discardableResult - public func overlaps(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.overlaps(attribute, values: values)) - } - - @discardableResult - public func notOverlaps(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.notOverlaps(attribute, values: values)) - } - - @discardableResult - public func touches(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.touches(attribute, values: values)) - } - - @discardableResult - public func notTouches(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.notTouches(attribute, values: values)) - } - - @discardableResult - public func count(_ attribute: String = "*", alias: String = "") -> QueryBuilder { - return push(Query.count(attribute, alias: alias)) - } - - @discardableResult - public func countDistinct(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.countDistinct(attribute, alias: alias)) - } - - @discardableResult - public func sum(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.sum(attribute, alias: alias)) - } - - @discardableResult - public func avg(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.avg(attribute, alias: alias)) - } - - @discardableResult - public func min(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.min(attribute, alias: alias)) - } - - @discardableResult - public func max(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.max(attribute, alias: alias)) - } - - @discardableResult - public func stddev(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.stddev(attribute, alias: alias)) - } - - @discardableResult - public func stddevPop(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.stddevPop(attribute, alias: alias)) - } - - @discardableResult - public func stddevSamp(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.stddevSamp(attribute, alias: alias)) - } - - @discardableResult - public func variance(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.variance(attribute, alias: alias)) - } - - @discardableResult - public func varPop(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.varPop(attribute, alias: alias)) - } - - @discardableResult - public func varSamp(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.varSamp(attribute, alias: alias)) - } - - @discardableResult - public func bitAnd(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.bitAnd(attribute, alias: alias)) - } - - @discardableResult - public func bitOr(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.bitOr(attribute, alias: alias)) - } - - @discardableResult - public func bitXor(_ attribute: String, alias: String = "") -> QueryBuilder { - return push(Query.bitXor(attribute, alias: alias)) - } - - @discardableResult - public func groupBy(_ attributes: [String]) -> QueryBuilder { - return push(Query.groupBy(attributes)) - } - - @discardableResult - public func having(_ queries: [String]) -> QueryBuilder { - return push(Query.having(queries)) - } - - @discardableResult - public func distinct() -> QueryBuilder { - return push(Query.distinct()) - } - - @discardableResult - public func join(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { - return push(Query.join(table, left: left, right: right, operator: op, alias: alias)) - } - - @discardableResult - public func join(_ table: String, on: [String]) -> QueryBuilder { - return push(Query.join(table, on: on)) - } - - @discardableResult - public func join(_ table: String, alias: String, on: [String]) -> QueryBuilder { - return push(Query.join(table, alias: alias, on: on)) - } - - @discardableResult - public func leftJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { - return push(Query.leftJoin(table, left: left, right: right, operator: op, alias: alias)) - } - - @discardableResult - public func leftJoin(_ table: String, on: [String]) -> QueryBuilder { - return push(Query.leftJoin(table, on: on)) - } - - @discardableResult - public func leftJoin(_ table: String, alias: String, on: [String]) -> QueryBuilder { - return push(Query.leftJoin(table, alias: alias, on: on)) - } - - @discardableResult - public func rightJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { - return push(Query.rightJoin(table, left: left, right: right, operator: op, alias: alias)) - } - - @discardableResult - public func rightJoin(_ table: String, on: [String]) -> QueryBuilder { - return push(Query.rightJoin(table, on: on)) - } - - @discardableResult - public func rightJoin(_ table: String, alias: String, on: [String]) -> QueryBuilder { - return push(Query.rightJoin(table, alias: alias, on: on)) - } - - @discardableResult - public func fullOuterJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> QueryBuilder { - return push(Query.fullOuterJoin(table, left: left, right: right, operator: op, alias: alias)) - } - - @discardableResult - public func fullOuterJoin(_ table: String, on: [String]) -> QueryBuilder { - return push(Query.fullOuterJoin(table, on: on)) - } - - @discardableResult - public func fullOuterJoin(_ table: String, alias: String, on: [String]) -> QueryBuilder { - return push(Query.fullOuterJoin(table, alias: alias, on: on)) - } - - @discardableResult - public func crossJoin(_ table: String, alias: String = "") -> QueryBuilder { - return push(Query.crossJoin(table, alias: alias)) - } - - @discardableResult - public func vectorDot(_ attribute: String, vector: [Double]) -> QueryBuilder { - return push(Query.vectorDot(attribute, vector: vector)) - } - - @discardableResult - public func vectorCosine(_ attribute: String, vector: [Double]) -> QueryBuilder { - return push(Query.vectorCosine(attribute, vector: vector)) - } - - @discardableResult - public func vectorEuclidean(_ attribute: String, vector: [Double]) -> QueryBuilder { - return push(Query.vectorEuclidean(attribute, vector: vector)) - } - - @discardableResult - public func covers(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.covers(attribute, values: values)) - } - - @discardableResult - public func notCovers(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.notCovers(attribute, values: values)) - } - - @discardableResult - public func spatialEquals(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.spatialEquals(attribute, values: values)) - } - - @discardableResult - public func notSpatialEquals(_ attribute: String, values: [Any]) -> QueryBuilder { - return push(Query.notSpatialEquals(attribute, values: values)) - } - - @discardableResult - public func page(_ page: Int, perPage: Int = 25) -> QueryBuilder { - return push(Query.page(page, perPage: perPage)) - } - - @discardableResult - private func push(_ query: String) -> QueryBuilder { - queries.append(query) - return self - } - - @discardableResult - private func push(_ query: [String]) -> QueryBuilder { - queries.append(contentsOf: query) - return self - } -} diff --git a/templates/swift/Sources/Query/Builder.swift.twig b/templates/swift/Sources/Query/Builder.swift.twig new file mode 100644 index 0000000000..0d950f92ad --- /dev/null +++ b/templates/swift/Sources/Query/Builder.swift.twig @@ -0,0 +1,501 @@ +import Foundation + +extension Query { + + public class Builder { + private var queries: [String] = [] + + public init() {} + + public func build() -> [String] { + return queries + } + + @discardableResult + public func equal(_ attribute: String, value: Any) -> Builder { + return push(Query.equal(attribute, value: value)) + } + + @discardableResult + public func notEqual(_ attribute: String, value: Any) -> Builder { + return push(Query.notEqual(attribute, value: value)) + } + + @discardableResult + public func lessThan(_ attribute: String, value: Any) -> Builder { + return push(Query.lessThan(attribute, value: value)) + } + + @discardableResult + public func lessThanEqual(attribute: String, value: Any) -> Builder { + return push(Query.lessThanEqual(attribute: attribute, value: value)) + } + + @discardableResult + public func greaterThan(_ attribute: String, value: Any) -> Builder { + return push(Query.greaterThan(attribute, value: value)) + } + + @discardableResult + public func greaterThanEqual(_ attribute: String, value: Any) -> Builder { + return push(Query.greaterThanEqual(attribute, value: value)) + } + + @discardableResult + public func search(_ attribute: String, value: String) -> Builder { + return push(Query.search(attribute, value: value)) + } + + @discardableResult + public func isNull(_ attribute: String) -> Builder { + return push(Query.isNull(attribute)) + } + + @discardableResult + public func isNotNull(_ attribute: String) -> Builder { + return push(Query.isNotNull(attribute)) + } + + @discardableResult + public func between(_ attribute: String, start: Int, end: Int) -> Builder { + return push(Query.between(attribute, start: start, end: end)) + } + + @discardableResult + public func between(_ attribute: String, start: Double, end: Double) -> Builder { + return push(Query.between(attribute, start: start, end: end)) + } + + @discardableResult + public func between(_ attribute: String, start: String, end: String) -> Builder { + return push(Query.between(attribute, start: start, end: end)) + } + + @discardableResult + public func startsWith(_ attribute: String, value: String) -> Builder { + return push(Query.startsWith(attribute, value: value)) + } + + @discardableResult + public func endsWith(_ attribute: String, value: String) -> Builder { + return push(Query.endsWith(attribute, value: value)) + } + + @discardableResult + public func select(_ attributes: [String]) -> Builder { + return push(Query.select(attributes)) + } + + @discardableResult + public func cursorAfter(_ id: String) -> Builder { + return push(Query.cursorAfter(id)) + } + + @discardableResult + public func cursorBefore(_ id: String) -> Builder { + return push(Query.cursorBefore(id)) + } + + @discardableResult + public func orderAsc(_ attribute: String) -> Builder { + return push(Query.orderAsc(attribute)) + } + + @discardableResult + public func orderDesc(_ attribute: String) -> Builder { + return push(Query.orderDesc(attribute)) + } + + @discardableResult + public func orderRandom() -> Builder { + return push(Query.orderRandom()) + } + + @discardableResult + public func limit(_ limit: Int) -> Builder { + return push(Query.limit(limit)) + } + + @discardableResult + public func offset(_ offset: Int) -> Builder { + return push(Query.offset(offset)) + } + + @discardableResult + public func contains(_ attribute: String, value: Any) -> Builder { + return push(Query.contains(attribute, value: value)) + } + + @discardableResult + public func containsAny(_ attribute: String, value: [Any]) -> Builder { + return push(Query.containsAny(attribute, value: value)) + } + + @discardableResult + public func containsAll(_ attribute: String, value: [Any]) -> Builder { + return push(Query.containsAll(attribute, value: value)) + } + + @discardableResult + public func notContains(_ attribute: String, value: Any) -> Builder { + return push(Query.notContains(attribute, value: value)) + } + + @discardableResult + public func notSearch(_ attribute: String, value: String) -> Builder { + return push(Query.notSearch(attribute, value: value)) + } + + @discardableResult + public func notBetween(_ attribute: String, start: Int, end: Int) -> Builder { + return push(Query.notBetween(attribute, start: start, end: end)) + } + + @discardableResult + public func notBetween(_ attribute: String, start: Double, end: Double) -> Builder { + return push(Query.notBetween(attribute, start: start, end: end)) + } + + @discardableResult + public func notBetween(_ attribute: String, start: String, end: String) -> Builder { + return push(Query.notBetween(attribute, start: start, end: end)) + } + + @discardableResult + public func notStartsWith(_ attribute: String, value: String) -> Builder { + return push(Query.notStartsWith(attribute, value: value)) + } + + @discardableResult + public func notEndsWith(_ attribute: String, value: String) -> Builder { + return push(Query.notEndsWith(attribute, value: value)) + } + + @discardableResult + public func regex(_ attribute: String, pattern: String) -> Builder { + return push(Query.regex(attribute, pattern: pattern)) + } + + @discardableResult + public func exists(_ attributes: [String]) -> Builder { + return push(Query.exists(attributes)) + } + + @discardableResult + public func notExists(_ attributes: [String]) -> Builder { + return push(Query.notExists(attributes)) + } + + @discardableResult + public func createdBefore(_ value: String) -> Builder { + return push(Query.createdBefore(value)) + } + + @discardableResult + public func createdAfter(_ value: String) -> Builder { + return push(Query.createdAfter(value)) + } + + @discardableResult + public func createdBetween(_ start: String, _ end: String) -> Builder { + return push(Query.createdBetween(start, end)) + } + + @discardableResult + public func updatedBefore(_ value: String) -> Builder { + return push(Query.updatedBefore(value)) + } + + @discardableResult + public func updatedAfter(_ value: String) -> Builder { + return push(Query.updatedAfter(value)) + } + + @discardableResult + public func updatedBetween(_ start: String, _ end: String) -> Builder { + return push(Query.updatedBetween(start, end)) + } + + @discardableResult + public func or(_ queries: [String]) -> Builder { + return push(Query.or(queries)) + } + + @discardableResult + public func and(_ queries: [String]) -> Builder { + return push(Query.and(queries)) + } + + @discardableResult + public func elemMatch(_ attribute: String, queries: [String]) -> Builder { + return push(Query.elemMatch(attribute, queries: queries)) + } + + @discardableResult + public func distanceEqual(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> Builder { + return push(Query.distanceEqual(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func distanceNotEqual(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> Builder { + return push(Query.distanceNotEqual(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func distanceGreaterThan(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> Builder { + return push(Query.distanceGreaterThan(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func distanceLessThan(_ attribute: String, values: [Any], distance: Double, meters: Bool = true) -> Builder { + return push(Query.distanceLessThan(attribute, values: values, distance: distance, meters: meters)) + } + + @discardableResult + public func intersects(_ attribute: String, values: [Any]) -> Builder { + return push(Query.intersects(attribute, values: values)) + } + + @discardableResult + public func notIntersects(_ attribute: String, values: [Any]) -> Builder { + return push(Query.notIntersects(attribute, values: values)) + } + + @discardableResult + public func crosses(_ attribute: String, values: [Any]) -> Builder { + return push(Query.crosses(attribute, values: values)) + } + + @discardableResult + public func notCrosses(_ attribute: String, values: [Any]) -> Builder { + return push(Query.notCrosses(attribute, values: values)) + } + + @discardableResult + public func overlaps(_ attribute: String, values: [Any]) -> Builder { + return push(Query.overlaps(attribute, values: values)) + } + + @discardableResult + public func notOverlaps(_ attribute: String, values: [Any]) -> Builder { + return push(Query.notOverlaps(attribute, values: values)) + } + + @discardableResult + public func touches(_ attribute: String, values: [Any]) -> Builder { + return push(Query.touches(attribute, values: values)) + } + + @discardableResult + public func notTouches(_ attribute: String, values: [Any]) -> Builder { + return push(Query.notTouches(attribute, values: values)) + } + + @discardableResult + public func count(_ attribute: String = "*", alias: String = "") -> Builder { + return push(Query.count(attribute, alias: alias)) + } + + @discardableResult + public func countDistinct(_ attribute: String, alias: String = "") -> Builder { + return push(Query.countDistinct(attribute, alias: alias)) + } + + @discardableResult + public func sum(_ attribute: String, alias: String = "") -> Builder { + return push(Query.sum(attribute, alias: alias)) + } + + @discardableResult + public func avg(_ attribute: String, alias: String = "") -> Builder { + return push(Query.avg(attribute, alias: alias)) + } + + @discardableResult + public func min(_ attribute: String, alias: String = "") -> Builder { + return push(Query.min(attribute, alias: alias)) + } + + @discardableResult + public func max(_ attribute: String, alias: String = "") -> Builder { + return push(Query.max(attribute, alias: alias)) + } + + @discardableResult + public func stddev(_ attribute: String, alias: String = "") -> Builder { + return push(Query.stddev(attribute, alias: alias)) + } + + @discardableResult + public func stddevPop(_ attribute: String, alias: String = "") -> Builder { + return push(Query.stddevPop(attribute, alias: alias)) + } + + @discardableResult + public func stddevSamp(_ attribute: String, alias: String = "") -> Builder { + return push(Query.stddevSamp(attribute, alias: alias)) + } + + @discardableResult + public func variance(_ attribute: String, alias: String = "") -> Builder { + return push(Query.variance(attribute, alias: alias)) + } + + @discardableResult + public func varPop(_ attribute: String, alias: String = "") -> Builder { + return push(Query.varPop(attribute, alias: alias)) + } + + @discardableResult + public func varSamp(_ attribute: String, alias: String = "") -> Builder { + return push(Query.varSamp(attribute, alias: alias)) + } + + @discardableResult + public func bitAnd(_ attribute: String, alias: String = "") -> Builder { + return push(Query.bitAnd(attribute, alias: alias)) + } + + @discardableResult + public func bitOr(_ attribute: String, alias: String = "") -> Builder { + return push(Query.bitOr(attribute, alias: alias)) + } + + @discardableResult + public func bitXor(_ attribute: String, alias: String = "") -> Builder { + return push(Query.bitXor(attribute, alias: alias)) + } + + @discardableResult + public func groupBy(_ attributes: [String]) -> Builder { + return push(Query.groupBy(attributes)) + } + + @discardableResult + public func having(_ queries: [String]) -> Builder { + return push(Query.having(queries)) + } + + @discardableResult + public func distinct() -> Builder { + return push(Query.distinct()) + } + + @discardableResult + public func join(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> Builder { + return push(Query.join(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func join(_ table: String, on: [String]) -> Builder { + return push(Query.join(table, on: on)) + } + + @discardableResult + public func join(_ table: String, alias: String, on: [String]) -> Builder { + return push(Query.join(table, alias: alias, on: on)) + } + + @discardableResult + public func leftJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> Builder { + return push(Query.leftJoin(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func leftJoin(_ table: String, on: [String]) -> Builder { + return push(Query.leftJoin(table, on: on)) + } + + @discardableResult + public func leftJoin(_ table: String, alias: String, on: [String]) -> Builder { + return push(Query.leftJoin(table, alias: alias, on: on)) + } + + @discardableResult + public func rightJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> Builder { + return push(Query.rightJoin(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func rightJoin(_ table: String, on: [String]) -> Builder { + return push(Query.rightJoin(table, on: on)) + } + + @discardableResult + public func rightJoin(_ table: String, alias: String, on: [String]) -> Builder { + return push(Query.rightJoin(table, alias: alias, on: on)) + } + + @discardableResult + public func fullOuterJoin(_ table: String, left: String, right: String, operator op: String = "=", alias: String = "") -> Builder { + return push(Query.fullOuterJoin(table, left: left, right: right, operator: op, alias: alias)) + } + + @discardableResult + public func fullOuterJoin(_ table: String, on: [String]) -> Builder { + return push(Query.fullOuterJoin(table, on: on)) + } + + @discardableResult + public func fullOuterJoin(_ table: String, alias: String, on: [String]) -> Builder { + return push(Query.fullOuterJoin(table, alias: alias, on: on)) + } + + @discardableResult + public func crossJoin(_ table: String, alias: String = "") -> Builder { + return push(Query.crossJoin(table, alias: alias)) + } + + @discardableResult + public func vectorDot(_ attribute: String, vector: [Double]) -> Builder { + return push(Query.vectorDot(attribute, vector: vector)) + } + + @discardableResult + public func vectorCosine(_ attribute: String, vector: [Double]) -> Builder { + return push(Query.vectorCosine(attribute, vector: vector)) + } + + @discardableResult + public func vectorEuclidean(_ attribute: String, vector: [Double]) -> Builder { + return push(Query.vectorEuclidean(attribute, vector: vector)) + } + + @discardableResult + public func covers(_ attribute: String, values: [Any]) -> Builder { + return push(Query.covers(attribute, values: values)) + } + + @discardableResult + public func notCovers(_ attribute: String, values: [Any]) -> Builder { + return push(Query.notCovers(attribute, values: values)) + } + + @discardableResult + public func spatialEquals(_ attribute: String, values: [Any]) -> Builder { + return push(Query.spatialEquals(attribute, values: values)) + } + + @discardableResult + public func notSpatialEquals(_ attribute: String, values: [Any]) -> Builder { + return push(Query.notSpatialEquals(attribute, values: values)) + } + + @discardableResult + public func page(_ page: Int, perPage: Int = 25) -> Builder { + return push(Query.page(page, perPage: perPage)) + } + + @discardableResult + private func push(_ query: String) -> Builder { + queries.append(query) + return self + } + + @discardableResult + private func push(_ query: [String]) -> Builder { + queries.append(contentsOf: query) + return self + } + } +} diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index 4bd6a5e8bf..cccc1dee33 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -774,7 +774,7 @@ export class Query { }; /** Start a fluent query builder. */ - static builder = (): QueryBuilder => new QueryBuilder(); + static builder = (): Query.Builder => new Query.Builder(); private static createJoin( method: string, @@ -819,14 +819,15 @@ export class Query { export type QueryInput = | string | Query - | QueryBuilder + | Query.Builder | QueryInput[]; /** - * Fluent collector of Query fragments. Call {@link QueryBuilder.build} to + * Fluent collector of Query fragments. Call {@link Query.Builder.build} to * produce the `string[]` expected by list/createQuery methods. */ -export class QueryBuilder { +export namespace Query { + export class Builder { private readonly queries: string[] = []; build(): string[] { @@ -1174,12 +1175,13 @@ export class QueryBuilder { return this; } } +} export function normalizeQueries(queries?: QueryInput | null): string[] { if (queries == null) { return []; } - if (queries instanceof QueryBuilder) { + if (queries instanceof Query.Builder) { return queries.build(); } if (!Array.isArray(queries)) { @@ -1202,7 +1204,7 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { function isQueryList(value: unknown[]): boolean { return value.every( (item) => - item instanceof QueryBuilder || + item instanceof Query.Builder || item instanceof Query || typeof item === "string" || (Array.isArray(item) && isQueryList(item)) @@ -1210,7 +1212,7 @@ function isQueryList(value: unknown[]): boolean { } export function flattenParam(value: unknown): unknown { - if (value instanceof QueryBuilder || value instanceof Query) { + if (value instanceof Query.Builder || value instanceof Query) { return normalizeQueries(value as QueryInput); } if (Array.isArray(value) && isQueryList(value)) { @@ -1218,3 +1220,6 @@ export function flattenParam(value: unknown): unknown { } return value; } + +export const QueryBuilder = Query.Builder; +export type QueryBuilder = Query.Builder; diff --git a/tests/e2e/languages/dotnet/Tests.cs b/tests/e2e/languages/dotnet/Tests.cs index f8288cc9b1..3f535a76a5 100644 --- a/tests/e2e/languages/dotnet/Tests.cs +++ b/tests/e2e/languages/dotnet/Tests.cs @@ -272,7 +272,7 @@ public async Task Test1() var pageQueries = Query.Page(2, 10); TestContext.WriteLine(pageQueries[0]); TestContext.WriteLine(pageQueries[1]); - TestContext.WriteLine(Query.Builder().Limit(1).Build()[0]); + TestContext.WriteLine(new Query.Builder().Limit(1).Build()[0]); // Permission & Roles helper tests TestContext.WriteLine(Permission.Read(Role.Any())); diff --git a/tests/e2e/languages/php/test.php b/tests/e2e/languages/php/test.php index d25adcf738..4b048b82aa 100644 --- a/tests/e2e/languages/php/test.php +++ b/tests/e2e/languages/php/test.php @@ -6,7 +6,7 @@ include __DIR__ . '/../../sdks/php/src/Appwrite/Service.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/InputFile.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Query.php'; -include __DIR__ . '/../../sdks/php/src/Appwrite/QueryBuilder.php'; +include __DIR__ . '/../../sdks/php/src/Appwrite/Query/Builder.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Permission.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/Role.php'; include __DIR__ . '/../../sdks/php/src/Appwrite/ID.php'; diff --git a/tests/e2e/languages/unity/Tests.cs b/tests/e2e/languages/unity/Tests.cs index 4c92fa875e..930bcb01b0 100644 --- a/tests/e2e/languages/unity/Tests.cs +++ b/tests/e2e/languages/unity/Tests.cs @@ -400,7 +400,7 @@ private async Task RunAsyncTest() var pageQueries = Query.Page(2, 10); LogResult(pageQueries[0]); LogResult(pageQueries[1]); - LogResult(Query.Builder().Limit(1).Build()[0]); + LogResult(new Query.Builder().Limit(1).Build()[0]); // Permission & Roles helper tests LogResult(Permission.Read(Role.Any())); LogResult(Permission.Write(Role.User(ID.Custom("userid")))); From da191e46492453ca2dbbccaf9454821672534d6b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 20:39:14 +1200 Subject: [PATCH 10/42] fix: compile nested query builders and hydrate array enums Dart part-of is resolved relative to lib/query, so the builder library looked for packageName.dart in the wrong directory, and a top-level Builder class collides with Flutter's widget. Go treats query.Builder() as a type conversion. PHP mock payloads and from() treated array-of-enum fields as a single string, which is how live-spec OAuth prompt examples are encoded. CLI Validation now compiles against a Go SDK generated from the same spec instead of the last published module. --- .github/workflows/validation.yml | 9 +- example.php | 4 +- src/SDK/Language/Dart.php | 6 +- src/SDK/Language/PHP.php | 4 +- templates/dart/lib/query.dart.twig | 2 +- templates/dart/lib/query/builder.dart.twig | 172 +++++++++--------- templates/dart/lib/src/models/model.dart.twig | 8 +- .../lib/src/models/request_model.dart.twig | 2 +- templates/php/src/Models/Model.php.twig | 4 +- .../php/src/Models/RequestModel.php.twig | 4 +- tests/e2e/Base.php | 84 +++++++++ tests/e2e/languages/go-v2/tests.go | 2 +- tests/e2e/languages/go/tests.go | 2 +- 13 files changed, 203 insertions(+), 100 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index a9e93513a1..179147eefc 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -91,7 +91,13 @@ jobs: run: composer install - name: Generate SDK for ${{ matrix.sdk }} - run: php example.php ${{ matrix.sdk }} ${{ matrix.platform }} + run: | + if [ "${{ matrix.sdk }}" = "cli" ]; then + # CLI commands are generated from the live spec but go.mod pins the + # last published Go SDK. Compile against a Go SDK from the same spec. + SDK_GO_VERSION=7.1.0 php example.php go server + fi + php example.php ${{ matrix.sdk }} ${{ matrix.platform }} - name: Verify generated SDK is idempotent if: matrix.sdk == 'zed-extension' || matrix.sdk == 'cli' @@ -214,6 +220,7 @@ jobs: npm run build ;; cli) + go mod edit -replace github.com/appwrite/sdk-for-go/v7=../go go mod tidy unformatted=$(gofmt -l .) if [ -n "$unformatted" ]; then diff --git a/example.php b/example.php index 27dfc37d68..cda6353fe4 100644 --- a/example.php +++ b/example.php @@ -420,10 +420,12 @@ function buildStaticSpecification(): Specification { $sdk = new SDK(new Go(), buildSpecification($spec)); // The version decides the major-version suffix Go requires from v2 on, // so examples/go declares `sdk-for-go/v6` like the published module. + // CLI Validation compiles against this tree via SDK_GO_VERSION=7.1.0 so + // the module path matches github.com/appwrite/sdk-for-go/v7. configureSDK($sdk, [ 'gitUserName' => 'appwrite', 'gitRepoName' => 'sdk-for-go', - 'version' => '6.2.0', + 'version' => getenv('SDK_GO_VERSION') ?: '6.2.0', ]); $sdk->generate(__DIR__ . '/examples/go'); } diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index c77b5499c2..6759ca701f 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -287,8 +287,10 @@ public function getModelToMapValue(Schema $property, string $propertyName, bool : "{$name}{$nullAware}.toMap()"; } - if ($property->enum !== []) { - return "{$name}{$nullAware}.value"; + if ($this->getEnumSchema($property)->enum !== []) { + return $property instanceof ArraySchema + ? "{$name}{$nullAware}.map((item) => item.value).toList()" + : "{$name}{$nullAware}.value"; } return $name; diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index ac7b82becf..904212a335 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -553,7 +553,9 @@ protected function getMockPropertyValue(string $name, Schema $property, Specific } $enumSchema = $this->getTypedEnumSchema($property); if ($enumSchema instanceof Schema) { - return '"' . $this->escapePhpString((string) $enumSchema->enum[0]) . '"'; + $value = '"' . $this->escapePhpString((string) $enumSchema->enum[0]) . '"'; + + return $property instanceof ArraySchema ? 'array(' . $value . ')' : $value; } $example = $this->getSchemaExample($property); diff --git a/templates/dart/lib/query.dart.twig b/templates/dart/lib/query.dart.twig index efcb0b873b..28e5187df3 100644 --- a/templates/dart/lib/query.dart.twig +++ b/templates/dart/lib/query.dart.twig @@ -466,7 +466,7 @@ class Query { } /// Start a fluent query builder. - static Builder builder() => Builder(); + static QueryBuilder builder() => QueryBuilder(); static String _createJoin( String method, diff --git a/templates/dart/lib/query/builder.dart.twig b/templates/dart/lib/query/builder.dart.twig index 6613319ef6..df819f3b2f 100644 --- a/templates/dart/lib/query/builder.dart.twig +++ b/templates/dart/lib/query/builder.dart.twig @@ -1,127 +1,127 @@ -part of '{{ language.params.packageName }}.dart'; +part of '../{{ language.params.packageName }}.dart'; /// Fluent collector of query strings. Call [build] to produce the list /// expected by list methods. -class Builder { +class QueryBuilder { final List _queries = []; List build() => List.from(_queries); - Builder equal(String attribute, dynamic value) => + QueryBuilder equal(String attribute, dynamic value) => _push(Query.equal(attribute, value)); - Builder notEqual(String attribute, dynamic value) => + QueryBuilder notEqual(String attribute, dynamic value) => _push(Query.notEqual(attribute, value)); - Builder lessThan(String attribute, dynamic value) => + QueryBuilder lessThan(String attribute, dynamic value) => _push(Query.lessThan(attribute, value)); - Builder lessThanEqual(String attribute, dynamic value) => + QueryBuilder lessThanEqual(String attribute, dynamic value) => _push(Query.lessThanEqual(attribute, value)); - Builder greaterThan(String attribute, dynamic value) => + QueryBuilder greaterThan(String attribute, dynamic value) => _push(Query.greaterThan(attribute, value)); - Builder greaterThanEqual(String attribute, dynamic value) => + QueryBuilder greaterThanEqual(String attribute, dynamic value) => _push(Query.greaterThanEqual(attribute, value)); - Builder search(String attribute, String value) => + QueryBuilder search(String attribute, String value) => _push(Query.search(attribute, value)); - Builder isNull(String attribute) => _push(Query.isNull(attribute)); + QueryBuilder isNull(String attribute) => _push(Query.isNull(attribute)); - Builder isNotNull(String attribute) => + QueryBuilder isNotNull(String attribute) => _push(Query.isNotNull(attribute)); - Builder between(String attribute, dynamic start, dynamic end) => + QueryBuilder between(String attribute, dynamic start, dynamic end) => _push(Query.between(attribute, start, end)); - Builder startsWith(String attribute, String value) => + QueryBuilder startsWith(String attribute, String value) => _push(Query.startsWith(attribute, value)); - Builder endsWith(String attribute, String value) => + QueryBuilder endsWith(String attribute, String value) => _push(Query.endsWith(attribute, value)); - Builder select(List attributes) => + QueryBuilder select(List attributes) => _push(Query.select(attributes)); - Builder cursorAfter(String documentId) => + QueryBuilder cursorAfter(String documentId) => _push(Query.cursorAfter(documentId)); - Builder cursorBefore(String documentId) => + QueryBuilder cursorBefore(String documentId) => _push(Query.cursorBefore(documentId)); - Builder orderAsc(String attribute) => + QueryBuilder orderAsc(String attribute) => _push(Query.orderAsc(attribute)); - Builder orderDesc(String attribute) => + QueryBuilder orderDesc(String attribute) => _push(Query.orderDesc(attribute)); - Builder orderRandom() => _push(Query.orderRandom()); + QueryBuilder orderRandom() => _push(Query.orderRandom()); - Builder limit(int limit) => _push(Query.limit(limit)); + QueryBuilder limit(int limit) => _push(Query.limit(limit)); - Builder offset(int offset) => _push(Query.offset(offset)); + QueryBuilder offset(int offset) => _push(Query.offset(offset)); - Builder contains(String attribute, dynamic value) => + QueryBuilder contains(String attribute, dynamic value) => _push(Query.contains(attribute, value)); - Builder containsAny(String attribute, List value) => + QueryBuilder containsAny(String attribute, List value) => _push(Query.containsAny(attribute, value)); - Builder containsAll(String attribute, List value) => + QueryBuilder containsAll(String attribute, List value) => _push(Query.containsAll(attribute, value)); - Builder notContains(String attribute, dynamic value) => + QueryBuilder notContains(String attribute, dynamic value) => _push(Query.notContains(attribute, value)); - Builder notSearch(String attribute, String value) => + QueryBuilder notSearch(String attribute, String value) => _push(Query.notSearch(attribute, value)); - Builder notBetween(String attribute, dynamic start, dynamic end) => + QueryBuilder notBetween(String attribute, dynamic start, dynamic end) => _push(Query.notBetween(attribute, start, end)); - Builder notStartsWith(String attribute, String value) => + QueryBuilder notStartsWith(String attribute, String value) => _push(Query.notStartsWith(attribute, value)); - Builder notEndsWith(String attribute, String value) => + QueryBuilder notEndsWith(String attribute, String value) => _push(Query.notEndsWith(attribute, value)); - Builder regex(String attribute, String pattern) => + QueryBuilder regex(String attribute, String pattern) => _push(Query.regex(attribute, pattern)); - Builder exists(List attributes) => + QueryBuilder exists(List attributes) => _push(Query.exists(attributes)); - Builder notExists(List attributes) => + QueryBuilder notExists(List attributes) => _push(Query.notExists(attributes)); - Builder createdBefore(String value) => + QueryBuilder createdBefore(String value) => _push(Query.createdBefore(value)); - Builder createdAfter(String value) => + QueryBuilder createdAfter(String value) => _push(Query.createdAfter(value)); - Builder createdBetween(String start, String end) => + QueryBuilder createdBetween(String start, String end) => _push(Query.createdBetween(start, end)); - Builder updatedBefore(String value) => + QueryBuilder updatedBefore(String value) => _push(Query.updatedBefore(value)); - Builder updatedAfter(String value) => + QueryBuilder updatedAfter(String value) => _push(Query.updatedAfter(value)); - Builder updatedBetween(String start, String end) => + QueryBuilder updatedBetween(String start, String end) => _push(Query.updatedBetween(start, end)); - Builder or(List queries) => _push(Query.or(queries)); + QueryBuilder or(List queries) => _push(Query.or(queries)); - Builder and(List queries) => _push(Query.and(queries)); + QueryBuilder and(List queries) => _push(Query.and(queries)); - Builder elemMatch(String attribute, List queries) => + QueryBuilder elemMatch(String attribute, List queries) => _push(Query.elemMatch(attribute, queries)); - Builder distanceEqual( + QueryBuilder distanceEqual( String attribute, List values, num distance, [ @@ -129,7 +129,7 @@ class Builder { ]) => _push(Query.distanceEqual(attribute, values, distance, meters)); - Builder distanceNotEqual( + QueryBuilder distanceNotEqual( String attribute, List values, num distance, [ @@ -137,7 +137,7 @@ class Builder { ]) => _push(Query.distanceNotEqual(attribute, values, distance, meters)); - Builder distanceGreaterThan( + QueryBuilder distanceGreaterThan( String attribute, List values, num distance, [ @@ -145,7 +145,7 @@ class Builder { ]) => _push(Query.distanceGreaterThan(attribute, values, distance, meters)); - Builder distanceLessThan( + QueryBuilder distanceLessThan( String attribute, List values, num distance, [ @@ -153,83 +153,83 @@ class Builder { ]) => _push(Query.distanceLessThan(attribute, values, distance, meters)); - Builder intersects(String attribute, List values) => + QueryBuilder intersects(String attribute, List values) => _push(Query.intersects(attribute, values)); - Builder notIntersects(String attribute, List values) => + QueryBuilder notIntersects(String attribute, List values) => _push(Query.notIntersects(attribute, values)); - Builder crosses(String attribute, List values) => + QueryBuilder crosses(String attribute, List values) => _push(Query.crosses(attribute, values)); - Builder notCrosses(String attribute, List values) => + QueryBuilder notCrosses(String attribute, List values) => _push(Query.notCrosses(attribute, values)); - Builder overlaps(String attribute, List values) => + QueryBuilder overlaps(String attribute, List values) => _push(Query.overlaps(attribute, values)); - Builder notOverlaps(String attribute, List values) => + QueryBuilder notOverlaps(String attribute, List values) => _push(Query.notOverlaps(attribute, values)); - Builder touches(String attribute, List values) => + QueryBuilder touches(String attribute, List values) => _push(Query.touches(attribute, values)); - Builder notTouches(String attribute, List values) => + QueryBuilder notTouches(String attribute, List values) => _push(Query.notTouches(attribute, values)); - Builder count([String attribute = '*', String alias = '']) => + QueryBuilder count([String attribute = '*', String alias = '']) => _push(Query.count(attribute, alias)); - Builder countDistinct(String attribute, [String alias = '']) => + QueryBuilder countDistinct(String attribute, [String alias = '']) => _push(Query.countDistinct(attribute, alias)); - Builder sum(String attribute, [String alias = '']) => + QueryBuilder sum(String attribute, [String alias = '']) => _push(Query.sum(attribute, alias)); - Builder avg(String attribute, [String alias = '']) => + QueryBuilder avg(String attribute, [String alias = '']) => _push(Query.avg(attribute, alias)); - Builder min(String attribute, [String alias = '']) => + QueryBuilder min(String attribute, [String alias = '']) => _push(Query.min(attribute, alias)); - Builder max(String attribute, [String alias = '']) => + QueryBuilder max(String attribute, [String alias = '']) => _push(Query.max(attribute, alias)); - Builder stddev(String attribute, [String alias = '']) => + QueryBuilder stddev(String attribute, [String alias = '']) => _push(Query.stddev(attribute, alias)); - Builder stddevPop(String attribute, [String alias = '']) => + QueryBuilder stddevPop(String attribute, [String alias = '']) => _push(Query.stddevPop(attribute, alias)); - Builder stddevSamp(String attribute, [String alias = '']) => + QueryBuilder stddevSamp(String attribute, [String alias = '']) => _push(Query.stddevSamp(attribute, alias)); - Builder variance(String attribute, [String alias = '']) => + QueryBuilder variance(String attribute, [String alias = '']) => _push(Query.variance(attribute, alias)); - Builder varPop(String attribute, [String alias = '']) => + QueryBuilder varPop(String attribute, [String alias = '']) => _push(Query.varPop(attribute, alias)); - Builder varSamp(String attribute, [String alias = '']) => + QueryBuilder varSamp(String attribute, [String alias = '']) => _push(Query.varSamp(attribute, alias)); - Builder bitAnd(String attribute, [String alias = '']) => + QueryBuilder bitAnd(String attribute, [String alias = '']) => _push(Query.bitAnd(attribute, alias)); - Builder bitOr(String attribute, [String alias = '']) => + QueryBuilder bitOr(String attribute, [String alias = '']) => _push(Query.bitOr(attribute, alias)); - Builder bitXor(String attribute, [String alias = '']) => + QueryBuilder bitXor(String attribute, [String alias = '']) => _push(Query.bitXor(attribute, alias)); - Builder groupBy(List attributes) => + QueryBuilder groupBy(List attributes) => _push(Query.groupBy(attributes)); - Builder having(List queries) => _push(Query.having(queries)); + QueryBuilder having(List queries) => _push(Query.having(queries)); - Builder distinct() => _push(Query.distinct()); + QueryBuilder distinct() => _push(Query.distinct()); - Builder join( + QueryBuilder join( String table, Object leftOrAliasOrOn, [ Object rightOrOn = '', @@ -238,7 +238,7 @@ class Builder { ]) => _push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - Builder leftJoin( + QueryBuilder leftJoin( String table, Object leftOrAliasOrOn, [ Object rightOrOn = '', @@ -247,7 +247,7 @@ class Builder { ]) => _push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - Builder rightJoin( + QueryBuilder rightJoin( String table, Object leftOrAliasOrOn, [ Object rightOrOn = '', @@ -256,7 +256,7 @@ class Builder { ]) => _push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - Builder fullOuterJoin( + QueryBuilder fullOuterJoin( String table, Object leftOrAliasOrOn, [ Object rightOrOn = '', @@ -265,34 +265,34 @@ class Builder { ]) => _push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - Builder crossJoin(String table, [String alias = '']) => + QueryBuilder crossJoin(String table, [String alias = '']) => _push(Query.crossJoin(table, alias)); - Builder vectorDot(String attribute, List vector) => + QueryBuilder vectorDot(String attribute, List vector) => _push(Query.vectorDot(attribute, vector)); - Builder vectorCosine(String attribute, List vector) => + QueryBuilder vectorCosine(String attribute, List vector) => _push(Query.vectorCosine(attribute, vector)); - Builder vectorEuclidean(String attribute, List vector) => + QueryBuilder vectorEuclidean(String attribute, List vector) => _push(Query.vectorEuclidean(attribute, vector)); - Builder covers(String attribute, List values) => + QueryBuilder covers(String attribute, List values) => _push(Query.covers(attribute, values)); - Builder notCovers(String attribute, List values) => + QueryBuilder notCovers(String attribute, List values) => _push(Query.notCovers(attribute, values)); - Builder spatialEquals(String attribute, List values) => + QueryBuilder spatialEquals(String attribute, List values) => _push(Query.spatialEquals(attribute, values)); - Builder notSpatialEquals(String attribute, List values) => + QueryBuilder notSpatialEquals(String attribute, List values) => _push(Query.notSpatialEquals(attribute, values)); - Builder page(int page, [int perPage = 25]) => + QueryBuilder page(int page, [int perPage = 25]) => _push(Query.page(page, perPage)); - Builder _push(Object query) { + QueryBuilder _push(Object query) { if (query is List) { _queries.addAll(query.cast()); } else { diff --git a/templates/dart/lib/src/models/model.dart.twig b/templates/dart/lib/src/models/model.dart.twig index 2b42152c9c..1d8e13e83b 100644 --- a/templates/dart/lib/src/models/model.dart.twig +++ b/templates/dart/lib/src/models/model.dart.twig @@ -38,7 +38,13 @@ class {{ definitionName | caseUcfirst | overrideIdentifier }} implements Model { ? {% endif %}{{ modelName }}.fromMap( {% if not (property | schemaRequired) %} {% else %} {% endif %}map['{{ propertyName | escapeDollarSign }}'], {% if not (property | schemaRequired) %} ) - : null{% else %} ){% endif %}{% endif %}{% elseif property.enum %}{% set enumName = (property | enumName) %}{% if (property | schemaRequired) %}enums.{{ enumName | caseUcfirst }}.values.firstWhere( + : null{% else %} ){% endif %}{% endif %}{% elseif (property | enumValues) is not empty %}{% set enumName = (property | enumName) %}{% if (property | schemaType) == 'array' %}{% if not (property | schemaRequired) %}map['{{ propertyName | escapeDollarSign }}'] != null + ? {% endif %}List.from( +{% if not (property | schemaRequired) %} {% else %} {% endif %}((map['{{ propertyName | escapeDollarSign }}'] is List) ? map['{{ propertyName | escapeDollarSign }}'] : [map['{{ propertyName | escapeDollarSign }}']]).map( +{% if not (property | schemaRequired) %} {% else %} {% endif %}(item) => enums.{{ enumName | caseUcfirst }}.values.firstWhere((e) => e.value == item), +{% if not (property | schemaRequired) %} {% else %} {% endif %}), +{% if not (property | schemaRequired) %} ) + : null{% else %} ){% endif %}{% elseif (property | schemaRequired) %}enums.{{ enumName | caseUcfirst }}.values.firstWhere( (e) => e.value == map['{{ propertyName | escapeDollarSign }}'], ){% else %}map['{{ propertyName | escapeDollarSign }}'] != null ? enums.{{ enumName | caseUcfirst }}.values.firstWhere( diff --git a/templates/dart/lib/src/models/request_model.dart.twig b/templates/dart/lib/src/models/request_model.dart.twig index 4662669bdf..c54f3787ac 100644 --- a/templates/dart/lib/src/models/request_model.dart.twig +++ b/templates/dart/lib/src/models/request_model.dart.twig @@ -21,7 +21,7 @@ class {{ requestModelName | caseUcfirst | overrideIdentifier }} implements Model Map toMap() { return { {% for property in requestModel.properties %} - "{{ (property | schemaName) | escapeDollarSign }}": {% if (property | schemaModel) %}{% if (property | schemaType) == 'array' %}{{(property | schemaName) | escapeKeyword}}{% if not (property | schemaRequired) %}?{% endif %}.map((p) => p.toMap()).toList(){% else %}{{(property | schemaName) | escapeKeyword}}{% if not (property | schemaRequired) %}?{% endif %}.toMap(){% endif %}{% elseif property.enum %}{{(property | schemaName) | escapeKeyword}}{% if not (property | schemaRequired) %}?{% endif %}.value{% else %}{{(property | schemaName) | escapeKeyword }}{% endif %}, + "{{ (property | schemaName) | escapeDollarSign }}": {% if (property | schemaModel) %}{% if (property | schemaType) == 'array' %}{{(property | schemaName) | escapeKeyword}}{% if not (property | schemaRequired) %}?{% endif %}.map((p) => p.toMap()).toList(){% else %}{{(property | schemaName) | escapeKeyword}}{% if not (property | schemaRequired) %}?{% endif %}.toMap(){% endif %}{% elseif (property | enumValues) is not empty %}{{(property | schemaName) | escapeKeyword}}{% if not (property | schemaRequired) %}?{% endif %}{% if (property | schemaType) == 'array' %}.map((item) => item.value).toList(){% else %}.value{% endif %}{% else %}{{(property | schemaName) | escapeKeyword }}{% endif %}, {% endfor %} }; } diff --git a/templates/php/src/Models/Model.php.twig b/templates/php/src/Models/Model.php.twig index 82a823887c..765aaf7f51 100644 --- a/templates/php/src/Models/Model.php.twig +++ b/templates/php/src/Models/Model.php.twig @@ -105,7 +105,7 @@ readonly class {{ definitionName | caseUcfirst | overrideIdentifier }} static fn (mixed $item): mixed => static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $item), $data['{{ (property | schemaName) }}'] ) - : $data['{{ (property | schemaName) }}']{% elseif (property | enumValues) is not empty %}static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}']){% else %}$data['{{ (property | schemaName) }}']{% endif %}{{ (not loop.last or optionalProperties is not empty or definition.additionalProperties) ? ',' : '' }} + : [static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}'])]{% elseif (property | enumValues) is not empty %}static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}']){% else %}$data['{{ (property | schemaName) }}']{% endif %}{{ (not loop.last or optionalProperties is not empty or definition.additionalProperties) ? ',' : '' }} {% endfor %} {% for property in optionalProperties %} {% if (property | schemaType) == 'array' and (property | schemaModel) %} @@ -126,7 +126,7 @@ readonly class {{ definitionName | caseUcfirst | overrideIdentifier }} static fn (mixed $item): mixed => static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $item), $data['{{ (property | schemaName) }}'] ) - : $data['{{ (property | schemaName) }}'] + : [static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}'])] ) {% elseif (property | schemaModel) %} {{ (property | schemaName) | caseCamel }}: array_key_exists('{{ (property | schemaName) }}', $data) ? static::hydrateTypedValue({{ (property | schemaModel) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}'], true) : null{{ (not loop.last or definition.additionalProperties) ? ',' : '' }} diff --git a/templates/php/src/Models/RequestModel.php.twig b/templates/php/src/Models/RequestModel.php.twig index 4285d78ad0..7b3af540c4 100644 --- a/templates/php/src/Models/RequestModel.php.twig +++ b/templates/php/src/Models/RequestModel.php.twig @@ -83,7 +83,7 @@ readonly class {{ requestModelName | caseUcfirst | overrideIdentifier }} static fn (mixed $item): mixed => static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $item), $data['{{ (property | schemaName) }}'] ) - : $data['{{ (property | schemaName) }}']{% elseif (property | enumValues) is not empty %}static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}']){% else %}$data['{{ (property | schemaName) }}']{% endif %}{{ (not loop.last or optionalProperties is not empty) ? ',' : '' }} + : [static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}'])]{% elseif (property | enumValues) is not empty %}static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}']){% else %}$data['{{ (property | schemaName) }}']{% endif %}{{ (not loop.last or optionalProperties is not empty) ? ',' : '' }} {% endfor %} {% for property in optionalProperties %} {{ (property | schemaName) | caseCamel }}: array_key_exists('{{ (property | schemaName) }}', $data) @@ -105,7 +105,7 @@ readonly class {{ requestModelName | caseUcfirst | overrideIdentifier }} static fn (mixed $item): mixed => static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $item), $data['{{ (property | schemaName) }}'] ) - : $data['{{ (property | schemaName) }}'] + : [static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}'])] ) {% elseif (property | enumValues) is not empty %} ? static::hydrateTypedValue({{ ((property | enumName)) | caseUcfirst | overrideIdentifier }}::class, $data['{{ (property | schemaName) }}'], true) diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 3a349feac0..a83ddfee35 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -11,9 +11,11 @@ use FilesystemIterator; use Throwable; use Appwrite\SDK\Language; +use Appwrite\SDK\Language\PHP as PHPLanguage; use Appwrite\SDK\SDK; use Utopia\OpenAPI\Model\StringSchema; use Utopia\OpenAPI\Parser; +use Utopia\OpenAPI\Specification; use PHPUnit\Framework\TestCase; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; @@ -509,6 +511,7 @@ public function testHTTPSuccess(): void $this->assertOpenEnumsAllowAnyString(); $this->assertClosedOneOfStringEnumsAreEnums(); + $this->assertArrayEnumScalarHydrates(); $sdk = new SDK($this->getLanguage(), Parser::parse($spec)); @@ -718,6 +721,87 @@ private function assertOneOfStringEnumsUseWireNames(string $dir): void $this->assertStringNotContainsString($titleName, $contents); } + private function assertArrayEnumScalarHydrates(): void + { + if ($this->language !== 'php') { + return; + } + + $specification = Parser::parse([ + 'openapi' => '3.0.0', + 'info' => ['title' => 'test', 'version' => '1.0.0'], + 'paths' => ['/oauth' => ['post' => [ + 'operationId' => 'updateOAuth', + 'tags' => ['project'], + 'responses' => ['200' => [ + 'description' => 'ok', + 'content' => ['application/json' => [ + 'schema' => ['$ref' => '#/components/schemas/OAuth2Google'], + ]], + ]], + ]]], + 'components' => ['schemas' => [ + 'OAuth2Google' => [ + 'type' => 'object', + 'required' => ['prompt'], + 'properties' => [ + 'prompt' => [ + 'type' => 'array', + 'example' => 'none', + 'items' => [ + 'type' => 'string', + 'enum' => ['none', 'consent'], + 'x-enum-name' => 'OAuth2Prompt', + ], + ], + ], + ], + ]], + ]); + + $php = new class () extends PHPLanguage { + public function mockPayload(string $definitionName, Specification $spec): string + { + return $this->getMockDefinitionPayload($definitionName, $spec); + } + }; + $payload = $php->mockPayload('OAuth2Google', $specification); + $this->assertStringContainsString('"prompt" => array("none")', $payload); + $this->assertStringNotContainsString('"prompt" => "none"', $payload); + + $dir = \sys_get_temp_dir() . '/sdk-array-enum-' . \bin2hex(\random_bytes(4)); + $sdk = new SDK(new PHPLanguage(), $specification); + $sdk + ->setName('php') + ->setVersion('0.0.1') + ->setNamespace('appwrite'); + $sdk->generate($dir); + + try { + $modelPath = $dir . '/src/Appwrite/Models/OAuth2Google.php'; + $this->assertFileExists($modelPath); + $modelSource = \file_get_contents($modelPath); + $this->assertIsString($modelSource); + $this->assertStringContainsString( + '[static::hydrateTypedValue(OAuth2Prompt::class, $data[\'prompt\'])]', + $modelSource + ); + + require_once $dir . '/src/Appwrite/Models/ArraySerializable.php'; + foreach (\glob($dir . '/src/Appwrite/Enums/*.php') ?: [] as $file) { + require_once $file; + } + require_once $modelPath; + + $modelClass = 'Appwrite\\Models\\OAuth2Google'; + $model = $modelClass::from(['prompt' => 'none']); + $this->assertCount(1, $model->prompt); + $this->assertSame('none', (string) $model->prompt[0]); + } finally { + $this->rmdirRecursive($dir); + } + } + private function assertOpenEnumsAllowAnyString(): void { $enumBranch = [ diff --git a/tests/e2e/languages/go-v2/tests.go b/tests/e2e/languages/go-v2/tests.go index 1e2b19ee34..323b9f8119 100644 --- a/tests/e2e/languages/go-v2/tests.go +++ b/tests/e2e/languages/go-v2/tests.go @@ -325,7 +325,7 @@ func testQueries() { pageQueries := query.Page(2, 10) fmt.Println(pageQueries[0]) fmt.Println(pageQueries[1]) - fmt.Println(query.Builder().Limit(1).Build()[0]) + fmt.Println(query.NewBuilder().Limit(1).Build()[0]) } func testPermissionHelpers() { diff --git a/tests/e2e/languages/go/tests.go b/tests/e2e/languages/go/tests.go index 86deec7996..4602b61a42 100644 --- a/tests/e2e/languages/go/tests.go +++ b/tests/e2e/languages/go/tests.go @@ -325,7 +325,7 @@ func testQueries() { pageQueries := query.Page(2, 10) fmt.Println(pageQueries[0]) fmt.Println(pageQueries[1]) - fmt.Println(query.Builder().Limit(1).Build()[0]) + fmt.Println(query.NewBuilder().Limit(1).Build()[0]) } func testPermissionHelpers() { From e85873f61ec92af08c5e104304ed86e548592284 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 20:54:27 +1200 Subject: [PATCH 11/42] fix: pin CLI to the locally generated Go SDK A replace in go.mod is the local pin; Validation no longer needs an env override or go mod edit. example.php writes examples/go from the same spec so ../go matches the CLI module path. --- .github/workflows/validation.yml | 9 +-------- example.php | 15 +++++++++++---- templates/cli/go.mod.twig | 8 +++++++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml index 179147eefc..a9e93513a1 100644 --- a/.github/workflows/validation.yml +++ b/.github/workflows/validation.yml @@ -91,13 +91,7 @@ jobs: run: composer install - name: Generate SDK for ${{ matrix.sdk }} - run: | - if [ "${{ matrix.sdk }}" = "cli" ]; then - # CLI commands are generated from the live spec but go.mod pins the - # last published Go SDK. Compile against a Go SDK from the same spec. - SDK_GO_VERSION=7.1.0 php example.php go server - fi - php example.php ${{ matrix.sdk }} ${{ matrix.platform }} + run: php example.php ${{ matrix.sdk }} ${{ matrix.platform }} - name: Verify generated SDK is idempotent if: matrix.sdk == 'zed-extension' || matrix.sdk == 'cli' @@ -220,7 +214,6 @@ jobs: npm run build ;; cli) - go mod edit -replace github.com/appwrite/sdk-for-go/v7=../go go mod tidy unformatted=$(gofmt -l .) if [ -n "$unformatted" ]; then diff --git a/example.php b/example.php index cda6353fe4..dec7d91293 100644 --- a/example.php +++ b/example.php @@ -357,6 +357,15 @@ function buildStaticSpecification(): Specification { // CLI if (!$requestedSdk || $requestedSdk === 'cli') { + // CLI go.mod replace-pins github.com/appwrite/sdk-for-go/v7 to ../go. + $go = new SDK(new Go(), buildSpecification($spec)); + configureSDK($go, [ + 'gitUserName' => 'appwrite', + 'gitRepoName' => 'sdk-for-go', + 'version' => '7.1.0', + ]); + $go->generate(__DIR__ . '/examples/go'); + $language = new CLI(); $language->setExecutableName('appwrite'); $language->setLogo($cliLogo); @@ -419,13 +428,11 @@ function buildStaticSpecification(): Specification { if (!$requestedSdk || $requestedSdk === 'go') { $sdk = new SDK(new Go(), buildSpecification($spec)); // The version decides the major-version suffix Go requires from v2 on, - // so examples/go declares `sdk-for-go/v6` like the published module. - // CLI Validation compiles against this tree via SDK_GO_VERSION=7.1.0 so - // the module path matches github.com/appwrite/sdk-for-go/v7. + // so examples/go declares `sdk-for-go/v7` like the published module. configureSDK($sdk, [ 'gitUserName' => 'appwrite', 'gitRepoName' => 'sdk-for-go', - 'version' => getenv('SDK_GO_VERSION') ?: '6.2.0', + 'version' => '7.1.0', ]); $sdk->generate(__DIR__ . '/examples/go'); } diff --git a/templates/cli/go.mod.twig b/templates/cli/go.mod.twig index 1f11b50605..e13328f32f 100644 --- a/templates/cli/go.mod.twig +++ b/templates/cli/go.mod.twig @@ -7,7 +7,7 @@ go 1.26.5 // so anything added by hand to the generated file is lost on the next run. require ( {% if not sdk.test %} - github.com/appwrite/sdk-for-go/v7 v7.1.0 + {{ language.params.sdkImportPath }} v7.1.0 {% endif %} github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 @@ -22,3 +22,9 @@ require ( // Floor raised above the v1.3.6 huh v1.0.0 asks for, which misrepaints on resize. require github.com/charmbracelet/bubbletea v1.3.10 // indirect +{% if not sdk.test %} + +// Live spec can add methods the last published Go SDK does not have yet. +// example.php generates that SDK next to this module as examples/go. +replace {{ language.params.sdkImportPath }} => ../go +{% endif %} From ab9dd4fb58e3015da3947be49d0cb4b4e7f50c22 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 20:59:02 +1200 Subject: [PATCH 12/42] refactor: flatten query values on Client instead of Query Request flattening is a transport concern. Keep Query as the helper surface and run flatten from Client in Kotlin, Android, Swift, and Apple. --- .../src/main/java/io/package/Client.kt.twig | 42 +++++++++++++++++- .../src/main/java/io/package/Query.kt.twig | 40 ----------------- templates/apple/Sources/Client.swift.twig | 44 ++++++++++++++++++- .../main/kotlin/io/appwrite/Client.kt.twig | 42 +++++++++++++++++- .../src/main/kotlin/io/appwrite/Query.kt.twig | 40 ----------------- templates/swift/Sources/Client.swift.twig | 44 ++++++++++++++++++- templates/swift/Sources/Query.swift.twig | 42 ------------------ 7 files changed, 168 insertions(+), 126 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index 0d3a9f5cc7..d8aa78e619 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -300,7 +300,7 @@ class Client @JvmOverloads constructor( converter: ((Any) -> T)? = null ): T { val filteredParams = params - .mapValues { Query.flatten(it.value) } + .mapValues { flatten(it.value) } .filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() @@ -672,4 +672,44 @@ class Client @JvmOverloads constructor( } }) } + + private fun flatten(value: Any?): Any? { + if (value is Query.Builder) { + return value.build() + } + if (value is Query) { + return value.toString() + } + + val items = when (value) { + is Array<*> -> value.asList() + is Collection<*> -> value + else -> return value + } + + val out = ArrayList(items.size) + var changed = false + for (item in items) { + val flat = flatten(item) + if (item is Query.Builder || item is Query) { + if (flat is Collection<*>) { + out.addAll(flat) + } else { + out.add(flat) + } + changed = true + continue + } + if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { + out.addAll(flat) + changed = true + continue + } + if (flat !== item) { + changed = true + } + out.add(flat) + } + return if (changed) out else value + } } 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 ee827e8380..6499c6df16 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 @@ -869,46 +869,6 @@ class Query( */ fun builder() = Builder() - internal fun flatten(value: Any?): Any? { - if (value is Builder) { - return value.build() - } - if (value is Query) { - return value.toString() - } - - val items = when (value) { - is Array<*> -> value.asList() - is Collection<*> -> value - else -> return value - } - - val out = ArrayList(items.size) - var changed = false - for (item in items) { - val flat = flatten(item) - if (item is Builder || item is Query) { - if (flat is Collection<*>) { - out.addAll(flat) - } else { - out.add(flat) - } - changed = true - continue - } - if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { - out.addAll(flat) - changed = true - continue - } - if (flat !== item) { - changed = true - } - out.add(flat) - } - return if (changed) out else value - } - private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { val values = if (alias.isNotEmpty()) listOf(left, operator, right, alias) else listOf(left, operator, right) return Query(method, table, values).toJson() diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 20e96d45a0..edb2ca95f3 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -314,7 +314,7 @@ open class Client { converter: ((Any) throws -> T)? = nil ) async throws -> T { let validParams = params - .mapValues { Query.flatten($0) } + .mapValues { flatten($0) } .filter { $0.value != nil } let queryParameters = method == "GET" && !validParams.isEmpty @@ -704,6 +704,48 @@ open class Client { value: "{{ spec.info.title | caseLower }}-\(operatingSystem)://\(packageInfo.packageName)" ) } + + private func flatten(_ value: Any?) -> Any? { + if let builder = value as? Query.Builder { + return builder.build() + } + if let query = value as? Query { + return query.description + } + + let items: [Any] + if let typed = value as? [Any] { + items = typed + } else if let typed = value as? [Any?] { + items = typed.compactMap { $0 } + } else { + return value + } + + var out: [Any] = [] + var changed = false + for item in items { + let flat = flatten(item) + if item is Query.Builder || item is Query { + if let list = flat as? [Any] { + out.append(contentsOf: list) + } else if let flat { + out.append(flat) + } + changed = true + continue + } + if let list = flat as? [Any], (item is [Any] || item is [String]), list.allSatisfy({ $0 is String }) { + out.append(contentsOf: list) + changed = true + continue + } + if let flat { + out.append(flat) + } + } + return changed ? out : value + } } extension Client { diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index c33fbd5b7e..89a90005ff 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -239,7 +239,7 @@ class Client @JvmOverloads constructor( params: Map = mapOf(), ): Request { val filteredParams = params - .mapValues { Query.flatten(it.value) } + .mapValues { flatten(it.value) } .filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() @@ -699,4 +699,44 @@ class Client @JvmOverloads constructor( } }) } + + private fun flatten(value: Any?): Any? { + if (value is Query.Builder) { + return value.build() + } + if (value is Query) { + return value.toString() + } + + val items = when (value) { + is Array<*> -> value.asList() + is Collection<*> -> value + else -> return value + } + + val out = ArrayList(items.size) + var changed = false + for (item in items) { + val flat = flatten(item) + if (item is Query.Builder || item is Query) { + if (flat is Collection<*>) { + out.addAll(flat) + } else { + out.add(flat) + } + changed = true + continue + } + if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { + out.addAll(flat) + changed = true + continue + } + if (flat !== item) { + changed = true + } + out.add(flat) + } + return if (changed) out else value + } } 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 8f7dd7e213..01bf8b1fe0 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Query.kt.twig @@ -577,46 +577,6 @@ class Query( */ fun builder() = Builder() - internal fun flatten(value: Any?): Any? { - if (value is Builder) { - return value.build() - } - if (value is Query) { - return value.toString() - } - - val items = when (value) { - is Array<*> -> value.asList() - is Collection<*> -> value - else -> return value - } - - val out = ArrayList(items.size) - var changed = false - for (item in items) { - val flat = flatten(item) - if (item is Builder || item is Query) { - if (flat is Collection<*>) { - out.addAll(flat) - } else { - out.add(flat) - } - changed = true - continue - } - if ((item is Array<*> || item is Collection<*>) && flat is Collection<*> && flat.all { it is String }) { - out.addAll(flat) - changed = true - continue - } - if (flat !== item) { - changed = true - } - out.add(flat) - } - return if (changed) out else value - } - private fun joinQuery(method: String, table: String, left: String, right: String, operator: String, alias: String): String { val values = if (alias.isNotEmpty()) listOf(left, operator, right, alias) else listOf(left, operator, right) return Query(method, table, values).toJson() diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 513c28c16a..e1e0f9275a 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -325,7 +325,7 @@ open class Client { params: [String: Any?] = [:] ) throws -> HTTPClientRequest { let validParams = params - .mapValues { Query.flatten($0) } + .mapValues { flatten($0) } .filter { $0.value != nil } let queryParameters = method == "GET" && !validParams.isEmpty @@ -706,6 +706,48 @@ open class Client { value: "{{ spec.info.title | caseLower }}-\(operatingSystem)://\(packageInfo.packageName)" ) } + + private func flatten(_ value: Any?) -> Any? { + if let builder = value as? Query.Builder { + return builder.build() + } + if let query = value as? Query { + return query.description + } + + let items: [Any] + if let typed = value as? [Any] { + items = typed + } else if let typed = value as? [Any?] { + items = typed.compactMap { $0 } + } else { + return value + } + + var out: [Any] = [] + var changed = false + for item in items { + let flat = flatten(item) + if item is Query.Builder || item is Query { + if let list = flat as? [Any] { + out.append(contentsOf: list) + } else if let flat { + out.append(flat) + } + changed = true + continue + } + if let list = flat as? [Any], (item is [Any] || item is [String]), list.allSatisfy({ $0 is String }) { + out.append(contentsOf: list) + changed = true + continue + } + if let flat { + out.append(flat) + } + } + return changed ? out : value + } } extension Client { diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index 53eee30343..bbd41a960f 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -885,48 +885,6 @@ public struct Query : Codable, CustomStringConvertible { return Builder() } - static func flatten(_ value: Any?) -> Any? { - if let builder = value as? Builder { - return builder.build() - } - if let query = value as? Query { - return query.description - } - - let items: [Any] - if let typed = value as? [Any] { - items = typed - } else if let typed = value as? [Any?] { - items = typed.compactMap { $0 } - } else { - return value - } - - var out: [Any] = [] - var changed = false - for item in items { - let flat = flatten(item) - if item is Builder || item is Query { - if let list = flat as? [Any] { - out.append(contentsOf: list) - } else if let flat { - out.append(flat) - } - changed = true - continue - } - if let list = flat as? [Any], (item is [Any] || item is [String]), list.allSatisfy({ $0 is String }) { - out.append(contentsOf: list) - changed = true - continue - } - if let flat { - out.append(flat) - } - } - return changed ? out : value - } - private static func joinQuery(_ method: String, table: String, left: String, right: String, operator op: String, alias: String) -> String { var values: [String] = [left, op, right] if !alias.isEmpty { From 8fc3bd4d13341e476fb8fe1dee648710fcca6582 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:11:20 +1200 Subject: [PATCH 13/42] fix: flatten query trees without dropping numeric arrays Rust was treating every nested array as query input, Go compared uncomparable slices, and Dart/Flutter left wrapped builders nested. Skills now show build() only; typed CLI query helpers include the new aggregates. Regenerating Rust deletes leftover src/query.rs. --- src/SDK/Language/Rust.php | 9 ++++ .../typegen/templates/databases.ts.hbs | 9 ++++ .../internal/typegen/templates/types.ts.hbs | 9 ++++ templates/dart/lib/src/client_mixin.dart.twig | 4 +- .../flutter/lib/src/client_mixin.dart.twig | 4 +- templates/go/client.go.twig | 2 +- templates/go/client_test.go.twig | 23 ++++++++++ templates/rust/src/client.rs.twig | 43 +++++++++++++------ templates/skills/dart.md.twig | 9 +--- templates/skills/dotnet.md.twig | 6 +-- templates/skills/go.md.twig | 12 ++---- templates/skills/kotlin.md.twig | 9 +--- templates/skills/php.md.twig | 6 +-- templates/skills/python.md.twig | 6 +-- templates/skills/ruby.md.twig | 6 +-- templates/skills/swift.md.twig | 6 +-- templates/skills/typescript.md.twig | 9 +--- 17 files changed, 100 insertions(+), 72 deletions(-) diff --git a/src/SDK/Language/Rust.php b/src/SDK/Language/Rust.php index 9a6e882aa3..4bec941bc5 100644 --- a/src/SDK/Language/Rust.php +++ b/src/SDK/Language/Rust.php @@ -678,4 +678,13 @@ protected function formatArrayItemExample(mixed $value, Schema $items): string }, }; } + + #[Override] + public function postGenerate(string $target): void + { + $legacy = $target . '/src/query.rs'; + if (\is_file($legacy) && \is_dir($target . '/src/query')) { + \unlink($legacy); + } + } } diff --git a/templates/cli/internal/typegen/templates/databases.ts.hbs b/templates/cli/internal/typegen/templates/databases.ts.hbs index 661ffab65b..7947c08c3c 100644 --- a/templates/cli/internal/typegen/templates/databases.ts.hbs +++ b/templates/cli/internal/typegen/templates/databases.ts.hbs @@ -35,6 +35,15 @@ const createQueryBuilder = (): QueryBuilder => ({ avg: (attribute, alias = '') => Query.avg(attribute, alias), min: (attribute, alias = '') => Query.min(attribute, alias), max: (attribute, alias = '') => Query.max(attribute, alias), + stddev: (attribute, alias = '') => Query.stddev(attribute, alias), + stddevPop: (attribute, alias = '') => Query.stddevPop(attribute, alias), + stddevSamp: (attribute, alias = '') => Query.stddevSamp(attribute, alias), + variance: (attribute, alias = '') => Query.variance(attribute, alias), + varPop: (attribute, alias = '') => Query.varPop(attribute, alias), + varSamp: (attribute, alias = '') => Query.varSamp(attribute, alias), + bitAnd: (attribute, alias = '') => Query.bitAnd(attribute, alias), + bitOr: (attribute, alias = '') => Query.bitOr(attribute, alias), + bitXor: (attribute, alias = '') => Query.bitXor(attribute, alias), groupBy: (attributes) => Query.groupBy(attributes), having: (queries) => Query.having(queries), distinct: () => Query.distinct(), diff --git a/templates/cli/internal/typegen/templates/types.ts.hbs b/templates/cli/internal/typegen/templates/types.ts.hbs index 16c70d7135..bc44186e3a 100644 --- a/templates/cli/internal/typegen/templates/types.ts.hbs +++ b/templates/cli/internal/typegen/templates/types.ts.hbs @@ -67,6 +67,15 @@ export type QueryBuilder = { avg: (attribute: string, alias?: string) => string; min: (attribute: string, alias?: string) => string; max: (attribute: string, alias?: string) => string; + stddev: (attribute: string, alias?: string) => string; + stddevPop: (attribute: string, alias?: string) => string; + stddevSamp: (attribute: string, alias?: string) => string; + variance: (attribute: string, alias?: string) => string; + varPop: (attribute: string, alias?: string) => string; + varSamp: (attribute: string, alias?: string) => string; + bitAnd: (attribute: string, alias?: string) => string; + bitOr: (attribute: string, alias?: string) => string; + bitXor: (attribute: string, alias?: string) => string; groupBy: (attributes: string[]) => string; having: (queries: string[]) => string; distinct: () => string; diff --git a/templates/dart/lib/src/client_mixin.dart.twig b/templates/dart/lib/src/client_mixin.dart.twig index 2b24e9676c..f7d19183c8 100644 --- a/templates/dart/lib/src/client_mixin.dart.twig +++ b/templates/dart/lib/src/client_mixin.dart.twig @@ -161,7 +161,9 @@ dynamic _flattenValue(dynamic value) { var changed = false; for (final item in value) { final flat = _flattenValue(item); - if (item is List && flat is List && flat.every((entry) => entry is String)) { + if (item is! String && + flat is List && + flat.every((entry) => entry is String)) { out.addAll(flat); changed = true; continue; diff --git a/templates/flutter/lib/src/client_mixin.dart.twig b/templates/flutter/lib/src/client_mixin.dart.twig index 50ef16dd3e..ba848baf11 100644 --- a/templates/flutter/lib/src/client_mixin.dart.twig +++ b/templates/flutter/lib/src/client_mixin.dart.twig @@ -164,7 +164,9 @@ dynamic _flattenValue(dynamic value) { var changed = false; for (final item in value) { final flat = _flattenValue(item); - if (item is List && flat is List && flat.every((entry) => entry is String)) { + if (item is! String && + flat is List && + flat.every((entry) => entry is String)) { out.addAll(flat); changed = true; continue; diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index 4ce5836ecc..5dd2508acb 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -812,7 +812,7 @@ func flattenValue(value interface{}) interface{} { changed = true continue } - if flat != item { + if !reflect.DeepEqual(flat, item) { changed = true } out = append(out, flat) diff --git a/templates/go/client_test.go.twig b/templates/go/client_test.go.twig index 9d13b437bc..179459a270 100644 --- a/templates/go/client_test.go.twig +++ b/templates/go/client_test.go.twig @@ -6,8 +6,11 @@ import ( "net/http" "net/http/httptest" "net/url" + "reflect" "sync" "testing" + + "{{ sdk | goPackagePath }}/query" ) func TestSetProject(t *testing.T) { @@ -272,4 +275,24 @@ func TestResponseBodyRejectsAnotherType(t *testing.T) { t.Error("Expected an error for a nil response") } } + +func TestFlattenValueNestedFloatsDoesNotPanic(t *testing.T) { + geometry := [][]float64{ + {1, 2}, + {3, 4}, + } + got := flattenValue(geometry) + if !reflect.DeepEqual(got, geometry) { + t.Errorf("Expected nested floats to pass through, got %#v", got) + } +} + +func TestFlattenValueSpreadsWrappedBuilder(t *testing.T) { + builder := query.NewBuilder().Limit(1) + got := flattenValue([]interface{}{builder}) + want := []interface{}{query.Limit(1)} + if !reflect.DeepEqual(got, want) { + t.Errorf("Expected %#v, got %#v", want, got) + } +} {% endautoescape %} diff --git a/templates/rust/src/client.rs.twig b/templates/rust/src/client.rs.twig index 6d26da9fc1..579e559fd7 100644 --- a/templates/rust/src/client.rs.twig +++ b/templates/rust/src/client.rs.twig @@ -354,22 +354,25 @@ impl Client { fn flatten_value(value: Value) -> Value { match value { - Value::Array(items) - if items - .iter() - .all(|item| item.is_string() || item.is_array()) => - { - Value::Array( - Query::normalize_value(&Value::Array(items)) - .into_iter() - .map(Value::String) - .collect(), - ) - } + Value::Array(items) if Self::is_query_string_tree(&items) => Value::Array( + Query::normalize_value(&Value::Array(items)) + .into_iter() + .map(Value::String) + .collect(), + ), other => other, } } + fn is_query_string_tree(items: &[Value]) -> bool { + !items.is_empty() + && items.iter().all(|item| match item { + Value::String(_) => true, + Value::Array(nested) => Self::is_query_string_tree(nested), + _ => false, + }) + } + pub async fn call( &self, method: Method, @@ -1040,4 +1043,20 @@ mod tests { assert_eq!(client.endpoint(), "https://custom.example.com/v1"); } + + #[test] + fn flatten_value_keeps_numeric_nested_arrays() { + let geometry = serde_json::json!([[1, 2], [3, 4]]); + assert_eq!(Client::flatten_value(geometry.clone()), geometry); + } + + #[test] + fn flatten_value_spreads_nested_query_strings() { + let nested = serde_json::json!([["{\"method\":\"limit\",\"values\":[1]}"]]); + let flat = Client::flatten_value(nested); + assert_eq!( + flat, + serde_json::json!(["{\"method\":\"limit\",\"values\":[1]}"]) + ); + } } diff --git a/templates/skills/dart.md.twig b/templates/skills/dart.md.twig index b469d1bef1..13abc23ae7 100644 --- a/templates/skills/dart.md.twig +++ b/templates/skills/dart.md.twig @@ -210,7 +210,7 @@ Query.leftJoin('orders', 'ord', [ Query.equal('ord.status', 'paid'), ]) -// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +// Fluent builder. Pass build() into list methods. final queries = Query.builder() .equal('status', 'published') .leftJoin('orders', r'$id', 'customerId', '=', 'ord') @@ -220,13 +220,6 @@ final queries = Query.builder() .build(); await tablesDB.listRows( - databaseId: '[DATABASE_ID]', - tableId: '[TABLE_ID]', - queries: queries, // or [Query.builder().equal('status', 'active')] -); - -// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. -await tablesDB.createQuery( databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', queries: queries, diff --git a/templates/skills/dotnet.md.twig b/templates/skills/dotnet.md.twig index 56397be528..3681bed2af 100644 --- a/templates/skills/dotnet.md.twig +++ b/templates/skills/dotnet.md.twig @@ -153,7 +153,7 @@ Query.LeftJoin("orders", "ord", new List { Query.Equal("ord.status", "paid"), }) -// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +// Fluent builder. Pass Build() into list methods. var queries = new Query.Builder() .Equal("status", "published") .LeftJoin("orders", "$id", "customerId", "=", "ord") @@ -163,10 +163,6 @@ var queries = new Query.Builder() .Build(); await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", queries); -// await tablesDB.ListRows("[DATABASE_ID]", "[TABLE_ID]", new[] { new Query.Builder().Equal("status", "active") }); - -// POST CreateQuery is the large-payload path when it exists; ListRows still accepts the same query strings. -await tablesDB.CreateQuery("[DATABASE_ID]", "[TABLE_ID]", queries); ``` ### File Storage diff --git a/templates/skills/go.md.twig b/templates/skills/go.md.twig index d737a1b1a6..8d67a9b331 100644 --- a/templates/skills/go.md.twig +++ b/templates/skills/go.md.twig @@ -166,32 +166,28 @@ query.Min("price", "minPrice") query.Max("price", "maxPrice") // Grouping -query.GroupBy([]string{"status", "year"}) +query.GroupBy([]interface{}{"status", "year"}) query.Having([]string{query.GreaterThan("total", 1)}) query.Distinct() // Joins (table id, left key, right key, operator, alias) -query.Join("orders", "$id", "customerId") +query.Join("orders", "$id", "customerId", "=", "") query.LeftJoin("orders", "$id", "customerId", "=", "ord") query.LeftJoinOn("orders", "ord", []string{ query.On("$id", "customerId", ""), query.Equal("ord.status", "paid"), }) -// Fluent builder. Wrapping the builder in an extra slice is OK — the SDK flattens it. +// Fluent builder. Pass Build() into list methods. queries := query.NewBuilder(). Equal("status", "published"). LeftJoin("orders", "$id", "customerId", "=", "ord"). Count("*", "total"). - GroupBy([]string{"status"}). + GroupBy([]interface{}{"status"}). Limit(25). Build() _, _ = service.ListRows("[DATABASE_ID]", "[TABLE_ID]", service.WithListRowsQueries(queries)) -// Wrapping the builder in an extra slice is OK — the SDK flattens it. - -// POST CreateQuery is the large-payload path when it exists; ListRows still accepts the same query strings. -_, _ = service.CreateQuery("[DATABASE_ID]", "[TABLE_ID]", queries) ``` ### File Storage diff --git a/templates/skills/kotlin.md.twig b/templates/skills/kotlin.md.twig index 97aa825e70..bdf12deaa3 100644 --- a/templates/skills/kotlin.md.twig +++ b/templates/skills/kotlin.md.twig @@ -234,7 +234,7 @@ Query.leftJoin("orders", "ord", listOf( Query.equal("ord.status", "paid"), )) -// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +// Fluent builder. Pass build() into list methods. val queries = Query.builder() .equal("status", "published") .leftJoin("orders", "\$id", "customerId", "=", "ord") @@ -244,13 +244,6 @@ val queries = Query.builder() .build() tablesDB.listRows( - databaseId = "[DATABASE_ID]", - tableId = "[TABLE_ID]", - queries = queries, // or listOf(Query.builder().equal("status", "active")) -) - -// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. -tablesDB.createQuery( databaseId = "[DATABASE_ID]", tableId = "[TABLE_ID]", queries = queries, diff --git a/templates/skills/php.md.twig b/templates/skills/php.md.twig index a35dbbc78d..0933e68874 100644 --- a/templates/skills/php.md.twig +++ b/templates/skills/php.md.twig @@ -163,7 +163,7 @@ Query::leftJoin('orders', 'ord', [ Query::equal('ord.status', 'paid'), ]) -// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +// Fluent builder. Pass build() into list methods. $queries = Query::builder() ->equal('status', 'published') ->leftJoin('orders', '$id', 'customerId', '=', 'ord') @@ -173,10 +173,6 @@ $queries = Query::builder() ->build(); $tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', $queries); -// $tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', [Query::builder()->equal('status', 'active')]); - -// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. -$tablesDB->createQuery('[DATABASE_ID]', '[TABLE_ID]', $queries); ``` ### File Storage diff --git a/templates/skills/python.md.twig b/templates/skills/python.md.twig index b76e20b245..38430bb0fb 100644 --- a/templates/skills/python.md.twig +++ b/templates/skills/python.md.twig @@ -168,7 +168,7 @@ Query.left_join('orders', 'ord', [ Query.equal('ord.status', 'paid'), ]) -# Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +# Fluent builder. Pass build() into list methods. queries = ( Query.builder() .equal('status', 'published') @@ -180,10 +180,6 @@ queries = ( ) tables_db.list_rows('[DATABASE_ID]', '[TABLE_ID]', queries) -# tables_db.list_rows('[DATABASE_ID]', '[TABLE_ID]', [Query.builder().equal('status', 'active')]) - -# POST create_query is the large-payload path when it exists; list_rows still accepts the same query strings. -tables_db.create_query('[DATABASE_ID]', '[TABLE_ID]', queries) ``` ### File Storage diff --git a/templates/skills/ruby.md.twig b/templates/skills/ruby.md.twig index 672daac093..2280e6a122 100644 --- a/templates/skills/ruby.md.twig +++ b/templates/skills/ruby.md.twig @@ -168,7 +168,7 @@ Query.left_join('orders', 'ord', [ Query.equal('ord.status', 'paid'), ]) -# Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +# Fluent builder. Pass build() into list methods. queries = Query.builder .equal('status', 'published') .left_join('orders', '$id', 'customerId', '=', 'ord') @@ -178,10 +178,6 @@ queries = Query.builder .build tables_db.list_rows(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', queries: queries) -# tables_db.list_rows(..., queries: [Query.builder.equal('status', 'active')]) - -# POST create_query is the large-payload path when it exists; list_rows still accepts the same query strings. -tables_db.create_query(database_id: '[DATABASE_ID]', table_id: '[TABLE_ID]', queries: queries) ``` ### File Storage diff --git a/templates/skills/swift.md.twig b/templates/skills/swift.md.twig index 4efb4a0cfd..87f01d3e8b 100644 --- a/templates/skills/swift.md.twig +++ b/templates/skills/swift.md.twig @@ -192,7 +192,7 @@ Query.leftJoin("orders", alias: "ord", on: [ Query.equal("ord.status", value: "paid"), ]) -// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +// Fluent builder. Pass build() into list methods. let queries = Query.builder() .equal("status", value: "published") .leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord") @@ -202,10 +202,6 @@ let queries = Query.builder() .build() _ = try await tablesDB.listRows(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: queries) -// listRows(..., queries: [Query.builder().equal("status", value: "active")]) is also OK - -// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. -_ = try await tablesDB.createQuery(databaseId: "[DATABASE_ID]", tableId: "[TABLE_ID]", queries: queries) ``` ### File Storage diff --git a/templates/skills/typescript.md.twig b/templates/skills/typescript.md.twig index b4c3aa6d1d..4b0def9081 100644 --- a/templates/skills/typescript.md.twig +++ b/templates/skills/typescript.md.twig @@ -351,7 +351,7 @@ Query.leftJoin('orders', 'ord', [ Query.equal('ord.status', 'paid'), ]) -// Fluent builder. Wrapping the builder in an extra array is OK — the SDK flattens it. +// Fluent builder. Pass build() into list methods. const queries = Query.builder() .equal('status', 'published') .leftJoin('orders', '$id', 'customerId', '=', 'ord') @@ -361,13 +361,6 @@ const queries = Query.builder() .build(); await tablesDB.listRows({ - databaseId: '[DATABASE_ID]', - tableId: '[TABLE_ID]', - queries, // or [Query.builder().equal('status', 'active')] -}); - -// POST createQuery is the large-payload path when it exists; listRows still accepts the same query strings. -await tablesDB.createQuery({ databaseId: '[DATABASE_ID]', tableId: '[TABLE_ID]', queries, From 41cbbb82ba77afa05ef6a4011025822a03af6f65 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:44:46 +1200 Subject: [PATCH 14/42] test: assert flatten unwraps builders and keeps nested geometry Language e2e tests now print flatten-builder/geometry/list so a wrapped builder that stays nested or a numeric array that gets spread fails CI. Query flatten is callable from those tests; PHP form flatten is flattenForm so it no longer collides with Client::flatten. --- src/SDK/Language/PHP.php | 5 +++++ .../src/main/java/io/package/Client.kt.twig | 2 +- templates/apple/Sources/Client.swift.twig | 2 +- templates/dart/lib/src/client.dart.twig | 3 +++ templates/dart/lib/src/client_mixin.dart.twig | 2 ++ templates/dart/test/query_test.dart.twig | 19 ++++++++++++++++ templates/deno/mod.ts.twig | 3 ++- templates/deno/test/query.test.ts.twig | 15 ++++++++++++- templates/dotnet/Package/Query.cs.twig | 2 +- templates/flutter/lib/src/client.dart.twig | 5 ++++- .../flutter/lib/src/client_mixin.dart.twig | 2 ++ templates/go/client.go.twig | 4 ++++ .../main/kotlin/io/appwrite/Client.kt.twig | 2 +- templates/node/src/client.ts.twig | 2 +- templates/node/src/index.ts.twig | 2 +- templates/node/test/query.test.js.twig | 15 ++++++++++++- templates/php/base/requests/file.twig | 2 +- templates/php/src/Client.php.twig | 12 +++++----- templates/php/tests/ClientTest.php.twig | 19 ++++++++++++++++ templates/php/tests/QueryTest.php.twig | 19 ++++++++++++++++ templates/python/test/test_query.py.twig | 10 +++++++++ templates/react-native/src/index.ts.twig | 2 +- templates/rust/src/client.rs.twig | 2 +- templates/rust/tests/tests.rs | 19 ++++++++++++++++ templates/swift/Sources/Client.swift.twig | 2 +- templates/swift/Tests/Tests.swift.twig | 16 ++++++++++++++ templates/web/src/client.ts.twig | 2 +- templates/web/src/index.ts.twig | 2 +- tests/e2e/Android16Java17Test.php | 1 + tests/e2e/Android5Java17Test.php | 1 + tests/e2e/AppleSwift61Test.php | 1 + tests/e2e/Base.php | 6 +++++ tests/e2e/DartBetaTest.php | 1 + tests/e2e/DartStableTest.php | 1 + tests/e2e/Deno1193Test.php | 1 + tests/e2e/Deno1303Test.php | 1 + tests/e2e/DotNet60Test.php | 1 + tests/e2e/DotNet80Test.php | 1 + tests/e2e/DotNet90Test.php | 1 + tests/e2e/FlutterBetaTest.php | 1 + tests/e2e/FlutterStableTest.php | 1 + tests/e2e/Go113Test.php | 1 + tests/e2e/Go118Test.php | 1 + tests/e2e/KotlinJava17Test.php | 1 + tests/e2e/Node18Test.php | 1 + tests/e2e/Node20Test.php | 1 + tests/e2e/Node26Test.php | 1 + tests/e2e/PHP82Test.php | 1 + tests/e2e/PHP83Test.php | 1 + tests/e2e/Python310Test.php | 1 + tests/e2e/Python311Test.php | 1 + tests/e2e/Python312Test.php | 1 + tests/e2e/Python313Test.php | 1 + tests/e2e/Python39Test.php | 1 + tests/e2e/ReactNativeTest.php | 1 + tests/e2e/Ruby27Test.php | 1 + tests/e2e/Ruby30Test.php | 1 + tests/e2e/Ruby31Test.php | 1 + tests/e2e/Rust183Test.php | 1 + tests/e2e/Swift61Test.php | 1 + tests/e2e/Unity2021Test.php | 1 + tests/e2e/WebChromiumTest.php | 1 + tests/e2e/WebNodeTest.php | 1 + tests/e2e/languages/android/Tests.kt | 6 +++++ tests/e2e/languages/apple/Tests.swift | 6 +++++ tests/e2e/languages/dart/tests.dart | 11 +++++++++- tests/e2e/languages/deno/tests.ts | 4 ++++ tests/e2e/languages/dotnet/Tests.cs | 9 ++++++++ tests/e2e/languages/flutter/tests.dart | 9 ++++++++ tests/e2e/languages/go-v2/tests.go | 22 +++++++++++++++++++ tests/e2e/languages/go/tests.go | 19 ++++++++++++++++ tests/e2e/languages/kotlin/Tests.kt | 6 +++++ tests/e2e/languages/node/test.js | 6 ++++- tests/e2e/languages/php/test.php | 6 +++++ 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 | 6 +++++ tests/e2e/languages/unity/Tests.cs | 8 +++++++ tests/e2e/languages/web/index.html | 5 ++++- tests/e2e/languages/web/node.js | 5 ++++- 81 files changed, 346 insertions(+), 26 deletions(-) diff --git a/src/SDK/Language/PHP.php b/src/SDK/Language/PHP.php index 85a169b6c5..79f5c7891f 100644 --- a/src/SDK/Language/PHP.php +++ b/src/SDK/Language/PHP.php @@ -202,6 +202,11 @@ public function getFiles(): array 'destination' => 'src/{{ namespace | caseNamespacePath }}/Client.php', 'template' => 'php/src/Client.php.twig', ], + [ + 'scope' => 'default', + 'destination' => 'tests/{{ namespace | caseNamespacePath }}/ClientTest.php', + 'template' => 'php/tests/ClientTest.php.twig', + ], [ 'scope' => 'default', 'destination' => 'src/{{ namespace | caseNamespacePath }}/Permission.php', diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index d8aa78e619..06066757fb 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -673,7 +673,7 @@ class Client @JvmOverloads constructor( }) } - private fun flatten(value: Any?): Any? { + fun flatten(value: Any?): Any? { if (value is Query.Builder) { return value.build() } diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index edb2ca95f3..214bd3a80c 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -705,7 +705,7 @@ open class Client { ) } - private func flatten(_ value: Any?) -> Any? { + public func flatten(_ value: Any?) -> Any? { if let builder = value as? Query.Builder { return builder.build() } diff --git a/templates/dart/lib/src/client.dart.twig b/templates/dart/lib/src/client.dart.twig index f4378d8d7f..c4356a8219 100644 --- a/templates/dart/lib/src/client.dart.twig +++ b/templates/dart/lib/src/client.dart.twig @@ -68,6 +68,9 @@ abstract class Client { Function(UploadProgress)? onProgress, }); + /// Flatten builders and nested query-string lists for request params. + dynamic flatten(dynamic value); + /// Send the API request. Future call( HttpMethod method, { diff --git a/templates/dart/lib/src/client_mixin.dart.twig b/templates/dart/lib/src/client_mixin.dart.twig index f7d19183c8..3d48cdd869 100644 --- a/templates/dart/lib/src/client_mixin.dart.twig +++ b/templates/dart/lib/src/client_mixin.dart.twig @@ -6,6 +6,8 @@ import 'dart:developer'; import 'enums.dart'; mixin ClientMixin { + dynamic flatten(dynamic value) => _flattenValue(value); + http.BaseRequest prepareRequest( HttpMethod method, { required Uri uri, diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index 12e4289f91..337643e53d 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -573,4 +573,23 @@ void main() { expect(built, expected); }); + + test('flatten spreads wrapped builder', () { + final client = Client(); + expect(client.flatten([Query.builder().limit(1)]), [Query.limit(1)]); + }); + + test('flatten keeps nested numeric arrays', () { + final client = Client(); + final geometry = [ + [1, 2], + [3, 4], + ]; + expect(client.flatten(geometry), geometry); + }); + + test('flatten keeps query string list', () { + final client = Client(); + expect(client.flatten([Query.limit(1)]), [Query.limit(1)]); + }); } diff --git a/templates/deno/mod.ts.twig b/templates/deno/mod.ts.twig index b1564bf717..4f72937454 100644 --- a/templates/deno/mod.ts.twig +++ b/templates/deno/mod.ts.twig @@ -1,5 +1,5 @@ import { Client } from "./src/client.ts"; -import { Query, QueryBuilder } from "./src/query.ts"; +import { Query, QueryBuilder, flattenParam } from "./src/query.ts"; import { Permission } from "./src/permission.ts"; import { Role } from "./src/role.ts"; import { ID } from "./src/id.ts"; @@ -17,6 +17,7 @@ export { Client, Query, QueryBuilder, + flattenParam, Permission, Role, ID, diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index b5110b8c49..7a81a3cc0d 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -1,6 +1,6 @@ import {describe, it as test} from "https://deno.land/std@0.149.0/testing/bdd.ts"; import {assertEquals} from "https://deno.land/std@0.204.0/assert/assert_equals.ts"; -import {Query, QueryTypes, normalizeQueries} from "../src/query.ts"; +import {Query, QueryTypes, flattenParam, normalizeQueries} from "../src/query.ts"; type BasicFilterQueryTest = { description: string; @@ -475,4 +475,17 @@ describe('Query', () => { Query.offset(10), ]); }); + + test('flattenParam spreads wrapped builder', () => { + assertEquals(flattenParam([Query.builder().limit(1)]), [Query.limit(1)]); + }); + + test('flattenParam keeps nested numeric arrays', () => { + const geometry = [[1, 2], [3, 4]]; + assertEquals(flattenParam(geometry), geometry); + }); + + test('flattenParam keeps query string list', () => { + assertEquals(flattenParam([Query.limit(1)]), [Query.limit(1)]); + }); }) diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index c2130b4168..eb0dc4a3f5 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -550,7 +550,7 @@ namespace {{ spec.info.title | caseUcfirst }} return new List { Limit(perPage), Offset((page - 1) * perPage) }; } - internal static object? Flatten(object? value) + public static object? Flatten(object? value) { if (value is Builder builder) { diff --git a/templates/flutter/lib/src/client.dart.twig b/templates/flutter/lib/src/client.dart.twig index 792f3a1f28..d86f7e5db4 100644 --- a/templates/flutter/lib/src/client.dart.twig +++ b/templates/flutter/lib/src/client.dart.twig @@ -77,7 +77,10 @@ abstract class Client { Future {{ clientMethodName | caseCamel }}(); {% endfor %} -{% endfor %} /// Send the API request. +{% endfor %} /// Flatten builders and nested query-string lists for request params. + dynamic flatten(dynamic value); + + /// Send the API request. Future call( HttpMethod method, { String path = '', diff --git a/templates/flutter/lib/src/client_mixin.dart.twig b/templates/flutter/lib/src/client_mixin.dart.twig index ba848baf11..e1cca356ba 100644 --- a/templates/flutter/lib/src/client_mixin.dart.twig +++ b/templates/flutter/lib/src/client_mixin.dart.twig @@ -6,6 +6,8 @@ import 'dart:developer'; import 'enums.dart'; mixin ClientMixin { + dynamic flatten(dynamic value) => _flattenValue(value); + http.BaseRequest prepareRequest( HttpMethod method, { required Uri uri, diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index 5dd2508acb..70b22b2ecd 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -762,6 +762,10 @@ func flatten(params interface{}, prefix string, result *map[string]string) error return nil } +func Flatten(value interface{}) interface{} { + return flattenValue(value) +} + func flattenValue(value interface{}) interface{} { if value == nil { return nil diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index 89a90005ff..40b43d6804 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -700,7 +700,7 @@ class Client @JvmOverloads constructor( }) } - private fun flatten(value: Any?): Any? { + fun flatten(value: Any?): Any? { if (value is Query.Builder) { return value.build() } diff --git a/templates/node/src/client.ts.twig b/templates/node/src/client.ts.twig index 4edc299701..143d4a9343 100644 --- a/templates/node/src/client.ts.twig +++ b/templates/node/src/client.ts.twig @@ -616,6 +616,6 @@ class Client { } export { Client, {{spec.info.title | caseUcfirst}}Exception }; -export { Query, QueryBuilder } from './query'; +export { Query, QueryBuilder, flattenParam } from './query'; export type { Models, Payload, UploadProgress }; export type { QueryTypes, QueryTypesList, QueryInput } from './query'; diff --git a/templates/node/src/index.ts.twig b/templates/node/src/index.ts.twig index a546201a19..ec69551376 100644 --- a/templates/node/src/index.ts.twig +++ b/templates/node/src/index.ts.twig @@ -1,4 +1,4 @@ -export { Client, Query, QueryBuilder, {{spec.info.title | caseUcfirst}}Exception } from './client'; +export { Client, Query, QueryBuilder, flattenParam, {{spec.info.title | caseUcfirst}}Exception } from './client'; {% for service in services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index ac8a8b0bec..05329c9c70 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -1,4 +1,4 @@ -const { Query, normalizeQueries } = require("../dist/query"); +const { Query, normalizeQueries, flattenParam } = require("../dist/query"); const tests = [ { @@ -353,4 +353,17 @@ describe('Query', () => { Query.offset(10), ]); }); + + test('flattenParam spreads wrapped builder', () => { + expect(flattenParam([Query.builder().limit(1)])).toEqual([Query.limit(1)]); + }); + + test('flattenParam keeps nested numeric arrays', () => { + const geometry = [[1, 2], [3, 4]]; + expect(flattenParam(geometry)).toBe(geometry); + }); + + test('flattenParam keeps query string list', () => { + expect(flattenParam([Query.limit(1)])).toEqual([Query.limit(1)]); + }); }) diff --git a/templates/php/base/requests/file.twig b/templates/php/base/requests/file.twig index 179df19c6c..18e114dc98 100644 --- a/templates/php/base/requests/file.twig +++ b/templates/php/base/requests/file.twig @@ -184,7 +184,7 @@ return [$this->endpoint, $this->headers, $this->selfSigned, $this->timeout, $this->connectTimeout]; }, $this->client, Client::class); $flattenParams = \Closure::bind(function(array $params): array { - return $this->flatten($params); + return $this->flattenForm($params); }, $this->client, Client::class); [$endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout] = $clientConfig(); $responseHeaders = []; diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index aa53c49f9b..9d0ce78ff1 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -283,7 +283,7 @@ class Client } {% endif %} $headers = array_merge($this->headers, $headers); - $params = \array_map(self::flattenValue(...), $params); + $params = \array_map(self::flatten(...), $params); $querySeparator = str_contains($path, '?') ? '&' : '?'; $ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? $querySeparator . http_build_query($params) : '')); $responseHeaders = []; @@ -294,7 +294,7 @@ class Client break; case 'multipart/form-data': - $query = $this->flatten($params); + $query = $this->flattenForm($params); break; default: @@ -386,14 +386,14 @@ class Client * @param string $prefix * @return array */ - protected function flatten(array $data, string $prefix = ''): array { + protected function flattenForm(array $data, string $prefix = ''): array { $output = []; foreach($data as $key => $value) { $finalKey = $prefix ? "{$prefix}[{$key}]" : $key; if (is_array($value)) { - $output = array_merge($output, $this->flatten($value, $finalKey)); + $output = array_merge($output, $this->flattenForm($value, $finalKey)); } else { $output[$finalKey] = $value; @@ -422,7 +422,7 @@ class Client return $data; } - private static function flattenValue(mixed $value): mixed + public static function flatten(mixed $value): mixed { if ($value instanceof QueryBuilder) { return $value->build(); @@ -437,7 +437,7 @@ class Client $out = []; $changed = false; foreach ($value as $item) { - $flat = self::flattenValue($item); + $flat = self::flatten($item); if ($item instanceof QueryBuilder || $item instanceof Query) { if (\is_array($flat)) { array_push($out, ...$flat); diff --git a/templates/php/tests/ClientTest.php.twig b/templates/php/tests/ClientTest.php.twig index 40bd1aa7a7..179902a0eb 100644 --- a/templates/php/tests/ClientTest.php.twig +++ b/templates/php/tests/ClientTest.php.twig @@ -48,6 +48,25 @@ final class ClientTest extends TestCase { $result = $client->addHeader('X-Custom', 'value'); $this->assertSame($client, $result); } + + public function testFlattenSpreadsWrappedBuilder(): void { + $this->assertSame( + [Query::limit(1)], + array_values((array) Client::flatten([Query::builder()->limit(1)])) + ); + } + + public function testFlattenKeepsNestedNumericArrays(): void { + $geometry = [[1, 2], [3, 4]]; + $this->assertSame($geometry, Client::flatten($geometry)); + } + + public function testFlattenKeepsQueryStringList(): void { + $this->assertSame( + [Query::limit(1)], + array_values((array) Client::flatten([Query::limit(1)])) + ); + } {% for headerKey, header in globalHeaders %} public function testSet{{headerKey | caseUcfirst}}ReturnsClient(): void { diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index 327d093d91..cf8c6f71c0 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -529,4 +529,23 @@ final class QueryTest extends TestCase { $this->assertSame($expected, $built); } + + public function testFlattenSpreadsWrappedBuilder(): void { + $this->assertSame( + [Query::limit(1)], + array_values((array) Client::flatten([Query::builder()->limit(1)])) + ); + } + + public function testFlattenKeepsNestedNumericArrays(): void { + $geometry = [[1, 2], [3, 4]]; + $this->assertSame($geometry, Client::flatten($geometry)); + } + + public function testFlattenKeepsQueryStringList(): void { + $this->assertSame( + [Query::limit(1)], + array_values((array) Client::flatten([Query::limit(1)])) + ); + } } diff --git a/templates/python/test/test_query.py.twig b/templates/python/test/test_query.py.twig index 0c72e4438a..3670b0ed98 100644 --- a/templates/python/test/test_query.py.twig +++ b/templates/python/test/test_query.py.twig @@ -227,3 +227,13 @@ class TestQueryMethods(unittest.TestCase): self.assertEqual(builder.build(), Query._normalize(builder)) self.assertEqual(builder.build(), Query._normalize([builder])) self.assertEqual(builder.build(), Query._normalize([builder.build()])) + + def test_flatten_spreads_wrapped_builder(self): + self.assertEqual([Query.limit(1)], Query._flatten([Query.builder().limit(1)])) + + def test_flatten_keeps_nested_numeric_arrays(self): + geometry = [[1, 2], [3, 4]] + self.assertEqual(geometry, Query._flatten(geometry)) + + def test_flatten_keeps_query_string_list(self): + self.assertEqual([Query.limit(1)], Query._flatten([Query.limit(1)])) diff --git a/templates/react-native/src/index.ts.twig b/templates/react-native/src/index.ts.twig index f7b42d5549..3031cebe23 100644 --- a/templates/react-native/src/index.ts.twig +++ b/templates/react-native/src/index.ts.twig @@ -6,7 +6,7 @@ export { Realtime } from './services/realtime'; export type { Models, Payload, RealtimeResponseEvent, UploadProgress } from './client'; export type { RealtimeSubscription } from './services/realtime'; export type { QueryTypes, QueryTypesList, QueryInput } from './query'; -export { Query, QueryBuilder } from './query'; +export { Query, QueryBuilder, flattenParam } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; diff --git a/templates/rust/src/client.rs.twig b/templates/rust/src/client.rs.twig index 579e559fd7..04869ea4ac 100644 --- a/templates/rust/src/client.rs.twig +++ b/templates/rust/src/client.rs.twig @@ -352,7 +352,7 @@ impl Client { .collect() } - fn flatten_value(value: Value) -> Value { + pub fn flatten_value(value: Value) -> Value { match value { Value::Array(items) if Self::is_query_string_tree(&items) => Value::Array( Query::normalize_value(&Value::Array(items)) diff --git a/templates/rust/tests/tests.rs b/templates/rust/tests/tests.rs index c55264c196..f4e2e76319 100644 --- a/templates/rust/tests/tests.rs +++ b/templates/rust/tests/tests.rs @@ -329,6 +329,25 @@ fn test_queries() { println!("{}", page_queries[0]); println!("{}", page_queries[1]); println!("{}", Query::builder().limit(1).build()[0]); + let built = Query::builder().limit(1).build(); + let nested = json!([built.clone()]); + if Client::flatten_value(nested) == json!(built) { + println!("flatten-builder:ok"); + } else { + println!("flatten-builder:fail"); + } + let geometry = json!([[1, 2], [3, 4]]); + if Client::flatten_value(geometry.clone()) == geometry { + println!("flatten-geometry:ok"); + } else { + println!("flatten-geometry:fail"); + } + let query_list = json!([Query::limit(1).to_string()]); + if Client::flatten_value(query_list.clone()) == query_list { + println!("flatten-list:ok"); + } else { + println!("flatten-list:fail"); + } } fn test_permission_helpers() { diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index e1e0f9275a..05ff618169 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -707,7 +707,7 @@ open class Client { ) } - private func flatten(_ value: Any?) -> Any? { + public func flatten(_ value: Any?) -> Any? { if let builder = value as? Query.Builder { return builder.build() } diff --git a/templates/swift/Tests/Tests.swift.twig b/templates/swift/Tests/Tests.swift.twig index 1d93805754..12a26b1dbb 100644 --- a/templates/swift/Tests/Tests.swift.twig +++ b/templates/swift/Tests/Tests.swift.twig @@ -119,4 +119,20 @@ class Tests: XCTestCase { XCTAssertEqual(method(queries[0]), "limit") XCTAssertEqual(method(queries[1]), "offset") } + + func testFlattenSpreadsWrappedBuilder() { + let client = Client() + XCTAssertEqual(client.flatten([Query.builder().limit(1)]) as? [String], [Query.limit(1)]) + } + + func testFlattenKeepsNestedNumericArrays() { + let client = Client() + let geometry = [[1, 2], [3, 4]] + XCTAssertEqual(client.flatten(geometry) as? [[Int]], geometry) + } + + func testFlattenKeepsQueryStringList() { + let client = Client() + XCTAssertEqual(client.flatten([Query.limit(1)]) as? [String], [Query.limit(1)]) + } } diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index ab0d395488..b5ba866b20 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1113,7 +1113,7 @@ class Client { } export { Client, {{spec.info.title | caseUcfirst}}Exception }; -export { Query, QueryBuilder } from './query'; +export { Query, QueryBuilder, flattenParam } from './query'; export type { Models, Payload, UploadProgress }; export type { RealtimeResponseEvent }; export type { QueryTypes, QueryTypesList, QueryInput } from './query'; diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index 94a2c08eea..8fb8ffac6c 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -5,7 +5,7 @@ * For older versions, please check * [previous releases](https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/releases). */ -export { Client, Query, QueryBuilder, {{spec.info.title | caseUcfirst}}Exception } from './client'; +export { Client, Query, QueryBuilder, flattenParam, {{spec.info.title | caseUcfirst}}Exception } from './client'; {% for service in services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} diff --git a/tests/e2e/Android16Java17Test.php b/tests/e2e/Android16Java17Test.php index 5356bc74f4..e7141277b8 100644 --- a/tests/e2e/Android16Java17Test.php +++ b/tests/e2e/Android16Java17Test.php @@ -46,6 +46,7 @@ final class Android16Java17Test extends Base ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Android5Java17Test.php b/tests/e2e/Android5Java17Test.php index 608d57a6ab..c8dc130160 100644 --- a/tests/e2e/Android5Java17Test.php +++ b/tests/e2e/Android5Java17Test.php @@ -46,6 +46,7 @@ final class Android5Java17Test extends Base ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/AppleSwift61Test.php b/tests/e2e/AppleSwift61Test.php index b202dc230f..eecebb5895 100644 --- a/tests/e2e/AppleSwift61Test.php +++ b/tests/e2e/AppleSwift61Test.php @@ -45,6 +45,7 @@ final class AppleSwift61Test extends Base ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index 432ee10bce..d8ca3723e4 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -234,6 +234,12 @@ abstract class Base extends TestCase '{"method":"limit","values":[1]}', ]; + protected const QUERY_FLATTEN_RESPONSES = [ + 'flatten-builder:ok', + 'flatten-geometry:ok', + 'flatten-list:ok', + ]; + protected const PERMISSION_HELPER_RESPONSES = [ 'read("any")', 'write("user:userid")', diff --git a/tests/e2e/DartBetaTest.php b/tests/e2e/DartBetaTest.php index 552cf0746d..ebf5215198 100644 --- a/tests/e2e/DartBetaTest.php +++ b/tests/e2e/DartBetaTest.php @@ -44,6 +44,7 @@ final class DartBetaTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DartStableTest.php b/tests/e2e/DartStableTest.php index 2b64f562d7..4138c3de09 100644 --- a/tests/e2e/DartStableTest.php +++ b/tests/e2e/DartStableTest.php @@ -44,6 +44,7 @@ final class DartStableTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Deno1193Test.php b/tests/e2e/Deno1193Test.php index 8992c884ba..d6be544ed7 100644 --- a/tests/e2e/Deno1193Test.php +++ b/tests/e2e/Deno1193Test.php @@ -40,6 +40,7 @@ final class Deno1193Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Deno1303Test.php b/tests/e2e/Deno1303Test.php index 4ea32c7aa0..766c578875 100644 --- a/tests/e2e/Deno1303Test.php +++ b/tests/e2e/Deno1303Test.php @@ -40,6 +40,7 @@ final class Deno1303Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DotNet60Test.php b/tests/e2e/DotNet60Test.php index 17475e6209..c0b1469f27 100644 --- a/tests/e2e/DotNet60Test.php +++ b/tests/e2e/DotNet60Test.php @@ -44,6 +44,7 @@ final class DotNet60Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DotNet80Test.php b/tests/e2e/DotNet80Test.php index 0e2f867507..e7e79ccc2b 100644 --- a/tests/e2e/DotNet80Test.php +++ b/tests/e2e/DotNet80Test.php @@ -44,6 +44,7 @@ final class DotNet80Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DotNet90Test.php b/tests/e2e/DotNet90Test.php index c34c7859f7..969dd2c3d7 100644 --- a/tests/e2e/DotNet90Test.php +++ b/tests/e2e/DotNet90Test.php @@ -44,6 +44,7 @@ final class DotNet90Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/FlutterBetaTest.php b/tests/e2e/FlutterBetaTest.php index 5918bb3140..e40a2f37b8 100644 --- a/tests/e2e/FlutterBetaTest.php +++ b/tests/e2e/FlutterBetaTest.php @@ -45,6 +45,7 @@ final class FlutterBetaTest extends Base ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/FlutterStableTest.php b/tests/e2e/FlutterStableTest.php index 62ae5ba8fb..c425eeb0b7 100644 --- a/tests/e2e/FlutterStableTest.php +++ b/tests/e2e/FlutterStableTest.php @@ -45,6 +45,7 @@ final class FlutterStableTest extends Base ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Go113Test.php b/tests/e2e/Go113Test.php index 41ee09c891..920c9a80f3 100644 --- a/tests/e2e/Go113Test.php +++ b/tests/e2e/Go113Test.php @@ -40,6 +40,7 @@ final class Go113Test extends Base ...Base::DOWNLOAD_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES, diff --git a/tests/e2e/Go118Test.php b/tests/e2e/Go118Test.php index f05cd11fce..60d81c39c0 100644 --- a/tests/e2e/Go118Test.php +++ b/tests/e2e/Go118Test.php @@ -40,6 +40,7 @@ final class Go118Test extends Base ...Base::DOWNLOAD_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES, diff --git a/tests/e2e/KotlinJava17Test.php b/tests/e2e/KotlinJava17Test.php index a953866743..e5456b4ae0 100644 --- a/tests/e2e/KotlinJava17Test.php +++ b/tests/e2e/KotlinJava17Test.php @@ -45,6 +45,7 @@ final class KotlinJava17Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Node18Test.php b/tests/e2e/Node18Test.php index e146367c1d..a27eed7e6a 100644 --- a/tests/e2e/Node18Test.php +++ b/tests/e2e/Node18Test.php @@ -52,6 +52,7 @@ final class Node18Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Node20Test.php b/tests/e2e/Node20Test.php index 2e99edc612..c837467301 100644 --- a/tests/e2e/Node20Test.php +++ b/tests/e2e/Node20Test.php @@ -52,6 +52,7 @@ final class Node20Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Node26Test.php b/tests/e2e/Node26Test.php index 7bae3297fa..2a3a3f6b75 100644 --- a/tests/e2e/Node26Test.php +++ b/tests/e2e/Node26Test.php @@ -52,6 +52,7 @@ final class Node26Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/PHP82Test.php b/tests/e2e/PHP82Test.php index 692a139929..4981e7c527 100644 --- a/tests/e2e/PHP82Test.php +++ b/tests/e2e/PHP82Test.php @@ -41,6 +41,7 @@ final class PHP82Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::ADDITIONAL_PROPERTIES_RESPONSES, diff --git a/tests/e2e/PHP83Test.php b/tests/e2e/PHP83Test.php index 4263e9a6d3..2bd7757389 100644 --- a/tests/e2e/PHP83Test.php +++ b/tests/e2e/PHP83Test.php @@ -41,6 +41,7 @@ final class PHP83Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::ADDITIONAL_PROPERTIES_RESPONSES, diff --git a/tests/e2e/Python310Test.php b/tests/e2e/Python310Test.php index 4652c336bf..01005548bd 100644 --- a/tests/e2e/Python310Test.php +++ b/tests/e2e/Python310Test.php @@ -44,6 +44,7 @@ final class Python310Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python311Test.php b/tests/e2e/Python311Test.php index e4ae003f15..3b88539b20 100644 --- a/tests/e2e/Python311Test.php +++ b/tests/e2e/Python311Test.php @@ -44,6 +44,7 @@ final class Python311Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python312Test.php b/tests/e2e/Python312Test.php index 8ec8d5c8aa..4d140baeb2 100644 --- a/tests/e2e/Python312Test.php +++ b/tests/e2e/Python312Test.php @@ -44,6 +44,7 @@ final class Python312Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python313Test.php b/tests/e2e/Python313Test.php index d5c3d82649..42162f5bd6 100644 --- a/tests/e2e/Python313Test.php +++ b/tests/e2e/Python313Test.php @@ -44,6 +44,7 @@ final class Python313Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python39Test.php b/tests/e2e/Python39Test.php index 862a2bcb3d..93c1a0e42e 100644 --- a/tests/e2e/Python39Test.php +++ b/tests/e2e/Python39Test.php @@ -44,6 +44,7 @@ final class Python39Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/ReactNativeTest.php b/tests/e2e/ReactNativeTest.php index 7567e63d76..2b6264cbbc 100644 --- a/tests/e2e/ReactNativeTest.php +++ b/tests/e2e/ReactNativeTest.php @@ -52,6 +52,7 @@ final class ReactNativeTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Ruby27Test.php b/tests/e2e/Ruby27Test.php index c913769f7a..2c1c2bcd14 100644 --- a/tests/e2e/Ruby27Test.php +++ b/tests/e2e/Ruby27Test.php @@ -42,6 +42,7 @@ final class Ruby27Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Ruby30Test.php b/tests/e2e/Ruby30Test.php index 3daa53f16f..c9e77094ca 100644 --- a/tests/e2e/Ruby30Test.php +++ b/tests/e2e/Ruby30Test.php @@ -42,6 +42,7 @@ final class Ruby30Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Ruby31Test.php b/tests/e2e/Ruby31Test.php index 4dd8c140be..7fc86dffbf 100644 --- a/tests/e2e/Ruby31Test.php +++ b/tests/e2e/Ruby31Test.php @@ -42,6 +42,7 @@ final class Ruby31Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Rust183Test.php b/tests/e2e/Rust183Test.php index e95d52292f..13bae7630b 100644 --- a/tests/e2e/Rust183Test.php +++ b/tests/e2e/Rust183Test.php @@ -36,6 +36,7 @@ final class Rust183Test extends Base ...Base::DOWNLOAD_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES, diff --git a/tests/e2e/Swift61Test.php b/tests/e2e/Swift61Test.php index bfd7e670eb..d0dca02b82 100644 --- a/tests/e2e/Swift61Test.php +++ b/tests/e2e/Swift61Test.php @@ -44,6 +44,7 @@ final class Swift61Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Unity2021Test.php b/tests/e2e/Unity2021Test.php index 31a24c9b03..6ca865580e 100644 --- a/tests/e2e/Unity2021Test.php +++ b/tests/e2e/Unity2021Test.php @@ -101,6 +101,7 @@ public function tearDown(): void ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/WebChromiumTest.php b/tests/e2e/WebChromiumTest.php index c3ac9a07e1..8025ad0db4 100644 --- a/tests/e2e/WebChromiumTest.php +++ b/tests/e2e/WebChromiumTest.php @@ -50,6 +50,7 @@ final class WebChromiumTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/WebNodeTest.php b/tests/e2e/WebNodeTest.php index 1f3a85953b..f80329e224 100644 --- a/tests/e2e/WebNodeTest.php +++ b/tests/e2e/WebNodeTest.php @@ -50,6 +50,7 @@ final class WebNodeTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/languages/android/Tests.kt b/tests/e2e/languages/android/Tests.kt index df46f5cfdb..e04a50b329 100644 --- a/tests/e2e/languages/android/Tests.kt +++ b/tests/e2e/languages/android/Tests.kt @@ -424,6 +424,12 @@ class ServiceTest { writeToFile(pageQueries[0]) writeToFile(pageQueries[1]) writeToFile(Query.builder().limit(1).build()[0]) + val flatBuilder = client.flatten(listOf(Query.builder().limit(1))) + writeToFile(if (flatBuilder == listOf(Query.limit(1))) "flatten-builder:ok" else "flatten-builder:fail") + val geometry = listOf(listOf(1, 2), listOf(3, 4)) + writeToFile(if (client.flatten(geometry) == geometry) "flatten-geometry:ok" else "flatten-geometry:fail") + val flatList = client.flatten(listOf(Query.limit(1))) + writeToFile(if (flatList == listOf(Query.limit(1))) "flatten-list:ok" else "flatten-list:fail") // 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 b7a1d90998..2359f26225 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -393,6 +393,12 @@ class Tests: XCTestCase { print(pageQueries[0]) print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) + let flatBuilder = client.flatten([Query.builder().limit(1)]) as? [String] + print(flatBuilder == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") + let geometry = [[1, 2], [3, 4]] + print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") + let flatList = client.flatten([Query.limit(1)]) as? [String] + print(flatList == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // 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 000230a78f..5a0e5d7648 100644 --- a/tests/e2e/languages/dart/tests.dart +++ b/tests/e2e/languages/dart/tests.dart @@ -261,7 +261,16 @@ void main() async { print(pageQueries[0]); print(pageQueries[1]); print(Query.builder().limit(1).build()[0]); - + final flatBuilder = client.flatten([Query.builder().limit(1)]); + print(flatBuilder is List && flatBuilder.length == 1 && flatBuilder[0] == Query.limit(1) ? 'flatten-builder:ok' : 'flatten-builder:fail'); + final geometry = [ + [1, 2], + [3, 4], + ]; + print(client.flatten(geometry).toString() == geometry.toString() ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); + final flatList = client.flatten([Query.limit(1)]); + print(flatList is List && flatList.length == 1 && flatList[0] == Query.limit(1) ? 'flatten-list:ok' : 'flatten-list:fail'); + // Permission & Role helper tests print(Permission.read(Role.any())); print(Permission.write(Role.user(ID.custom('userid')))); diff --git a/tests/e2e/languages/deno/tests.ts b/tests/e2e/languages/deno/tests.ts index 3d39789560..91cb77e644 100644 --- a/tests/e2e/languages/deno/tests.ts +++ b/tests/e2e/languages/deno/tests.ts @@ -9,6 +9,7 @@ async function start() { let Role = appwrite.Role; let ID = appwrite.ID; let Query = appwrite.Query; + let flattenParam = appwrite.flattenParam; let Operator = appwrite.Operator; let Condition = appwrite.Condition; @@ -292,6 +293,9 @@ async function start() { console.log(pageQueries[0]); console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); + console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? "flatten-builder:ok" : "flatten-builder:fail"); + console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? "flatten-geometry:ok" : "flatten-geometry:fail"); + console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? "flatten-list:ok" : "flatten-list:fail"); // 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 3f535a76a5..c7c9e0dbc6 100644 --- a/tests/e2e/languages/dotnet/Tests.cs +++ b/tests/e2e/languages/dotnet/Tests.cs @@ -1,6 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Appwrite; @@ -273,6 +275,13 @@ public async Task Test1() TestContext.WriteLine(pageQueries[0]); TestContext.WriteLine(pageQueries[1]); TestContext.WriteLine(new Query.Builder().Limit(1).Build()[0]); + var expectedLimit = new List { Query.Limit(1) }; + var flatBuilder = Query.Flatten(new List { new Query.Builder().Limit(1) }) as IEnumerable; + TestContext.WriteLine(flatBuilder != null && expectedLimit.SequenceEqual(flatBuilder.Cast()) ? "flatten-builder:ok" : "flatten-builder:fail"); + var geometry = new List { new List { 1, 2 }, new List { 3, 4 } }; + TestContext.WriteLine(object.ReferenceEquals(Query.Flatten(geometry), geometry) ? "flatten-geometry:ok" : "flatten-geometry:fail"); + var queryList = new List { Query.Limit(1) }; + TestContext.WriteLine(object.ReferenceEquals(Query.Flatten(queryList), queryList) ? "flatten-list:ok" : "flatten-list:fail"); // 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 6912e9fe03..48bbbffc02 100644 --- a/tests/e2e/languages/flutter/tests.dart +++ b/tests/e2e/languages/flutter/tests.dart @@ -390,6 +390,15 @@ void main() async { print(pageQueries[0]); print(pageQueries[1]); print(Query.builder().limit(1).build()[0]); + final flatBuilder = client.flatten([Query.builder().limit(1)]); + print(flatBuilder is List && flatBuilder.length == 1 && flatBuilder[0] == Query.limit(1) ? 'flatten-builder:ok' : 'flatten-builder:fail'); + final geometry = [ + [1, 2], + [3, 4], + ]; + print(client.flatten(geometry).toString() == geometry.toString() ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); + final flatList = client.flatten([Query.limit(1)]); + print(flatList is List && flatList.length == 1 && flatList[0] == Query.limit(1) ? 'flatten-list:ok' : 'flatten-list:fail'); // 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 323b9f8119..59ad03d69d 100644 --- a/tests/e2e/languages/go-v2/tests.go +++ b/tests/e2e/languages/go-v2/tests.go @@ -3,6 +3,7 @@ package main import ( "fmt" "path" + "reflect" "time" "github.com/repoowner/reponame/v2/appwrite" @@ -326,6 +327,27 @@ func testQueries() { fmt.Println(pageQueries[0]) fmt.Println(pageQueries[1]) fmt.Println(query.NewBuilder().Limit(1).Build()[0]) + flatBuilder := client.Flatten([]interface{}{query.NewBuilder().Limit(1)}) + if got, ok := flatBuilder.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { + fmt.Println("flatten-builder:ok") + } else { + fmt.Println("flatten-builder:fail") + } + geometry := [][]int{ + {1, 2}, + {3, 4}, + } + if reflect.DeepEqual(client.Flatten(geometry), geometry) { + fmt.Println("flatten-geometry:ok") + } else { + fmt.Println("flatten-geometry:fail") + } + flatList := client.Flatten([]interface{}{query.Limit(1)}) + if got, ok := flatList.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { + fmt.Println("flatten-list:ok") + } else { + fmt.Println("flatten-list:fail") + } } func testPermissionHelpers() { diff --git a/tests/e2e/languages/go/tests.go b/tests/e2e/languages/go/tests.go index 4602b61a42..f429ed2a77 100644 --- a/tests/e2e/languages/go/tests.go +++ b/tests/e2e/languages/go/tests.go @@ -3,6 +3,7 @@ package main import ( "fmt" "path" + "reflect" "time" "github.com/repoowner/reponame/appwrite" @@ -326,6 +327,24 @@ func testQueries() { fmt.Println(pageQueries[0]) fmt.Println(pageQueries[1]) fmt.Println(query.NewBuilder().Limit(1).Build()[0]) + flatBuilder := client.Flatten([]interface{}{query.NewBuilder().Limit(1)}) + if got, ok := flatBuilder.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { + fmt.Println("flatten-builder:ok") + } else { + fmt.Println("flatten-builder:fail") + } + geometry := [][]int{{1, 2}, {3, 4}} + if reflect.DeepEqual(client.Flatten(geometry), geometry) { + fmt.Println("flatten-geometry:ok") + } else { + fmt.Println("flatten-geometry:fail") + } + flatList := client.Flatten([]interface{}{query.Limit(1)}) + if got, ok := flatList.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { + fmt.Println("flatten-list:ok") + } else { + fmt.Println("flatten-list:fail") + } } func testPermissionHelpers() { diff --git a/tests/e2e/languages/kotlin/Tests.kt b/tests/e2e/languages/kotlin/Tests.kt index dabb3f0a2c..7085c84fdc 100644 --- a/tests/e2e/languages/kotlin/Tests.kt +++ b/tests/e2e/languages/kotlin/Tests.kt @@ -292,6 +292,12 @@ class ServiceTest { writeToFile(pageQueries[0]) writeToFile(pageQueries[1]) writeToFile(Query.builder().limit(1).build()[0]) + val flatBuilder = client.flatten(listOf(Query.builder().limit(1))) + writeToFile(if (flatBuilder == listOf(Query.limit(1))) "flatten-builder:ok" else "flatten-builder:fail") + val geometry = listOf(listOf(1, 2), listOf(3, 4)) + writeToFile(if (client.flatten(geometry) == geometry) "flatten-geometry:ok" else "flatten-geometry:fail") + val flatList = client.flatten(listOf(Query.limit(1))) + writeToFile(if (flatList == listOf(Query.limit(1))) "flatten-list:ok" else "flatten-list:fail") // 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 65da812031..73120c1892 100644 --- a/tests/e2e/languages/node/test.js +++ b/tests/e2e/languages/node/test.js @@ -9,7 +9,8 @@ const { MockType, Foo, Bar, - General + General, + flattenParam, } = require('./dist/index.js'); const { InputFile } = require('./dist/inputFile.js'); const { readFile } = require('fs/promises'); @@ -377,6 +378,9 @@ async function start() { console.log(pageQueries[0]); console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); + console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); + console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); + console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); // 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 4b048b82aa..73f391571c 100644 --- a/tests/e2e/languages/php/test.php +++ b/tests/e2e/languages/php/test.php @@ -384,6 +384,12 @@ public function toArray(): array echo $page[0] . "\n"; echo $page[1] . "\n"; echo Query::builder()->limit(1)->build()[0] . "\n"; +$flatBuilder = Client::flatten([Query::builder()->limit(1)]); +echo ((is_array($flatBuilder) ? array_values($flatBuilder) : []) === [Query::limit(1)] ? 'flatten-builder:ok' : 'flatten-builder:fail') . "\n"; +$geometry = [[1, 2], [3, 4]]; +echo (Client::flatten($geometry) === $geometry ? 'flatten-geometry:ok' : 'flatten-geometry:fail') . "\n"; +$flatList = Client::flatten([Query::limit(1)]); +echo ((is_array($flatList) ? array_values($flatList) : []) === [Query::limit(1)] ? 'flatten-list:ok' : 'flatten-list:fail') . "\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 54018ea86c..46ee3f9ce8 100644 --- a/tests/e2e/languages/python/tests.py +++ b/tests/e2e/languages/python/tests.py @@ -247,6 +247,10 @@ print(page_queries[0]) print(page_queries[1]) print(Query.builder().limit(1).build()[0]) +print('flatten-builder:ok' if Query._flatten([Query.builder().limit(1)]) == [Query.limit(1)] else 'flatten-builder:fail') +geometry = [[1, 2], [3, 4]] +print('flatten-geometry:ok' if Query._flatten(geometry) == geometry else 'flatten-geometry:fail') +print('flatten-list:ok' if Query._flatten([Query.limit(1)]) == [Query.limit(1)] else 'flatten-list:fail') # 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 f6bbeb864e..69eab11c46 100644 --- a/tests/e2e/languages/react-native/browser.js +++ b/tests/e2e/languages/react-native/browser.js @@ -5,6 +5,7 @@ import { General, Realtime, Query, + flattenParam, Permission, Role, ID, @@ -326,6 +327,9 @@ import { console.log(pageQueries[0]); console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); + console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); + console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); + console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); // 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 bfe256b499..ef85085769 100644 --- a/tests/e2e/languages/ruby/tests.rb +++ b/tests/e2e/languages/ruby/tests.rb @@ -253,6 +253,10 @@ puts page_queries[0] puts page_queries[1] puts Query.builder.limit(1).build[0] +puts(Query._flatten([Query.builder.limit(1)]) == [Query.limit(1)] ? 'flatten-builder:ok' : 'flatten-builder:fail') +geometry = [[1, 2], [3, 4]] +puts(Query._flatten(geometry) == geometry ? 'flatten-geometry:ok' : 'flatten-geometry:fail') +puts(Query._flatten([Query.limit(1)]) == [Query.limit(1)] ? 'flatten-list:ok' : 'flatten-list:fail') # 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 a9dd69b2c0..38d07f0667 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -283,6 +283,12 @@ class Tests: XCTestCase { print(pageQueries[0]) print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) + let flatBuilder = client.flatten([Query.builder().limit(1)]) as? [String] + print(flatBuilder == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") + let geometry = [[1, 2], [3, 4]] + print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") + let flatList = client.flatten([Query.limit(1)]) as? [String] + print(flatList == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // 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 930bcb01b0..08960dbc23 100644 --- a/tests/e2e/languages/unity/Tests.cs +++ b/tests/e2e/languages/unity/Tests.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.TestTools; @@ -401,6 +402,13 @@ private async Task RunAsyncTest() LogResult(pageQueries[0]); LogResult(pageQueries[1]); LogResult(new Query.Builder().Limit(1).Build()[0]); + var expectedLimit = new List { Query.Limit(1) }; + var flatBuilder = Query.Flatten(new List { new Query.Builder().Limit(1) }) as IEnumerable; + LogResult(flatBuilder != null && expectedLimit.SequenceEqual(flatBuilder.Cast()) ? "flatten-builder:ok" : "flatten-builder:fail"); + var geometry = new List { new List { 1, 2 }, new List { 3, 4 } }; + LogResult(object.ReferenceEquals(Query.Flatten(geometry), geometry) ? "flatten-geometry:ok" : "flatten-geometry:fail"); + var queryList = new List { Query.Limit(1) }; + LogResult(object.ReferenceEquals(Query.Flatten(queryList), queryList) ? "flatten-list:ok" : "flatten-list:fail"); // 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 73f91a2f5d..68aabc395e 100644 --- a/tests/e2e/languages/web/index.html +++ b/tests/e2e/languages/web/index.html @@ -29,7 +29,7 @@ let rtsubFailureFiredAfterUnsubscribe = false; // Init SDK - const { Client, Foo, Bar, General, Realtime, Query, Permission, Role, ID, Channel, Operator, Condition, MockType } = Appwrite; + const { Client, Foo, Bar, General, Realtime, Query, Permission, Role, ID, Channel, Operator, Condition, MockType, flattenParam } = Appwrite; const client = new Client(); const foo = new Foo(client); @@ -468,6 +468,9 @@ console.log(pageQueries[0]); console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); + console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); + console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); + console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); // 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 e13ed9cd4b..f264f9fea5 100644 --- a/tests/e2e/languages/web/node.js +++ b/tests/e2e/languages/web/node.js @@ -1,4 +1,4 @@ -const { Client, Foo, Bar, General, Query, Permission, Role, ID, Channel, Operator, Condition, MockType } = require('./dist/cjs/sdk.js'); +const { Client, Foo, Bar, General, Query, Permission, Role, ID, Channel, Operator, Condition, MockType, flattenParam } = require('./dist/cjs/sdk.js'); async function start() { let response; @@ -318,6 +318,9 @@ async function start() { console.log(pageQueries[0]); console.log(pageQueries[1]); console.log(Query.builder().limit(1).build()[0]); + console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); + console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); + console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); // Permission & Role helper tests console.log(Permission.read(Role.any())); From 53e6e02a4c9528c85e560d530ba632f09cc87c18 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:44:46 +1200 Subject: [PATCH 15/42] fix: sync CLI generator testdata with aggregate helpers Typed QueryBuilder gained stddev/variance/bitwise; baseline fixtures still expected groupBy at that offset and failed TestGenerateMatchesBaseline. --- .../cli/internal/generator/testdata/client.databases.ts | 9 +++++++++ .../cli/internal/generator/testdata/client.types.ts | 9 +++++++++ .../cli/internal/generator/testdata/server.databases.ts | 9 +++++++++ .../cli/internal/generator/testdata/server.types.ts | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/templates/cli/internal/generator/testdata/client.databases.ts b/templates/cli/internal/generator/testdata/client.databases.ts index b0e5ba169a..c8dc415099 100644 --- a/templates/cli/internal/generator/testdata/client.databases.ts +++ b/templates/cli/internal/generator/testdata/client.databases.ts @@ -31,6 +31,15 @@ const createQueryBuilder = (): QueryBuilder => ({ avg: (attribute, alias = '') => Query.avg(attribute, alias), min: (attribute, alias = '') => Query.min(attribute, alias), max: (attribute, alias = '') => Query.max(attribute, alias), + stddev: (attribute, alias = '') => Query.stddev(attribute, alias), + stddevPop: (attribute, alias = '') => Query.stddevPop(attribute, alias), + stddevSamp: (attribute, alias = '') => Query.stddevSamp(attribute, alias), + variance: (attribute, alias = '') => Query.variance(attribute, alias), + varPop: (attribute, alias = '') => Query.varPop(attribute, alias), + varSamp: (attribute, alias = '') => Query.varSamp(attribute, alias), + bitAnd: (attribute, alias = '') => Query.bitAnd(attribute, alias), + bitOr: (attribute, alias = '') => Query.bitOr(attribute, alias), + bitXor: (attribute, alias = '') => Query.bitXor(attribute, alias), groupBy: (attributes) => Query.groupBy(attributes), having: (queries) => Query.having(queries), distinct: () => Query.distinct(), diff --git a/templates/cli/internal/generator/testdata/client.types.ts b/templates/cli/internal/generator/testdata/client.types.ts index f3c0c77fd2..8c795fc15d 100644 --- a/templates/cli/internal/generator/testdata/client.types.ts +++ b/templates/cli/internal/generator/testdata/client.types.ts @@ -117,6 +117,15 @@ export type QueryBuilder = { avg: (attribute: string, alias?: string) => string; min: (attribute: string, alias?: string) => string; max: (attribute: string, alias?: string) => string; + stddev: (attribute: string, alias?: string) => string; + stddevPop: (attribute: string, alias?: string) => string; + stddevSamp: (attribute: string, alias?: string) => string; + variance: (attribute: string, alias?: string) => string; + varPop: (attribute: string, alias?: string) => string; + varSamp: (attribute: string, alias?: string) => string; + bitAnd: (attribute: string, alias?: string) => string; + bitOr: (attribute: string, alias?: string) => string; + bitXor: (attribute: string, alias?: string) => string; groupBy: (attributes: string[]) => string; having: (queries: string[]) => string; distinct: () => string; diff --git a/templates/cli/internal/generator/testdata/server.databases.ts b/templates/cli/internal/generator/testdata/server.databases.ts index e65f710035..da1c09cd9f 100644 --- a/templates/cli/internal/generator/testdata/server.databases.ts +++ b/templates/cli/internal/generator/testdata/server.databases.ts @@ -31,6 +31,15 @@ const createQueryBuilder = (): QueryBuilder => ({ avg: (attribute, alias = '') => Query.avg(attribute, alias), min: (attribute, alias = '') => Query.min(attribute, alias), max: (attribute, alias = '') => Query.max(attribute, alias), + stddev: (attribute, alias = '') => Query.stddev(attribute, alias), + stddevPop: (attribute, alias = '') => Query.stddevPop(attribute, alias), + stddevSamp: (attribute, alias = '') => Query.stddevSamp(attribute, alias), + variance: (attribute, alias = '') => Query.variance(attribute, alias), + varPop: (attribute, alias = '') => Query.varPop(attribute, alias), + varSamp: (attribute, alias = '') => Query.varSamp(attribute, alias), + bitAnd: (attribute, alias = '') => Query.bitAnd(attribute, alias), + bitOr: (attribute, alias = '') => Query.bitOr(attribute, alias), + bitXor: (attribute, alias = '') => Query.bitXor(attribute, alias), groupBy: (attributes) => Query.groupBy(attributes), having: (queries) => Query.having(queries), distinct: () => Query.distinct(), diff --git a/templates/cli/internal/generator/testdata/server.types.ts b/templates/cli/internal/generator/testdata/server.types.ts index 4d43095224..0e363e73be 100644 --- a/templates/cli/internal/generator/testdata/server.types.ts +++ b/templates/cli/internal/generator/testdata/server.types.ts @@ -117,6 +117,15 @@ export type QueryBuilder = { avg: (attribute: string, alias?: string) => string; min: (attribute: string, alias?: string) => string; max: (attribute: string, alias?: string) => string; + stddev: (attribute: string, alias?: string) => string; + stddevPop: (attribute: string, alias?: string) => string; + stddevSamp: (attribute: string, alias?: string) => string; + variance: (attribute: string, alias?: string) => string; + varPop: (attribute: string, alias?: string) => string; + varSamp: (attribute: string, alias?: string) => string; + bitAnd: (attribute: string, alias?: string) => string; + bitOr: (attribute: string, alias?: string) => string; + bitXor: (attribute: string, alias?: string) => string; groupBy: (attributes: string[]) => string; having: (queries: string[]) => string; distinct: () => string; From 727aee8bcf3b81f79c873e31f29849e11794a7a8 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 22:57:49 +1200 Subject: [PATCH 16/42] fix: keep flatten tests off Flutter Client init and Swift array casts Flutter query_test constructing Client() hits path_provider before the widget binding exists. Apple flatten returns [Any], so as? [String] failed even when the query strings were correct. --- templates/dart/test/query_test.dart.twig | 2 ++ templates/swift/Tests/Tests.swift.twig | 14 ++++++++++++-- tests/e2e/languages/apple/Tests.swift | 15 +++++++++++---- tests/e2e/languages/swift/Tests.swift | 15 +++++++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index 337643e53d..9348aeb29e 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -573,6 +573,7 @@ void main() { expect(built, expected); }); +{% if 'dart' in language.params.packageName %} test('flatten spreads wrapped builder', () { final client = Client(); @@ -592,4 +593,5 @@ void main() { final client = Client(); expect(client.flatten([Query.limit(1)]), [Query.limit(1)]); }); +{% endif %} } diff --git a/templates/swift/Tests/Tests.swift.twig b/templates/swift/Tests/Tests.swift.twig index 12a26b1dbb..bfe1655385 100644 --- a/templates/swift/Tests/Tests.swift.twig +++ b/templates/swift/Tests/Tests.swift.twig @@ -120,9 +120,19 @@ class Tests: XCTestCase { XCTAssertEqual(method(queries[1]), "offset") } + func queryStrings(_ value: Any?) -> [String]? { + if let list = value as? [String] { + return list + } + if let list = value as? [Any] { + return list.map { String(describing: $0) } + } + return nil + } + func testFlattenSpreadsWrappedBuilder() { let client = Client() - XCTAssertEqual(client.flatten([Query.builder().limit(1)]) as? [String], [Query.limit(1)]) + XCTAssertEqual(queryStrings(client.flatten([Query.builder().limit(1)])), [Query.limit(1)]) } func testFlattenKeepsNestedNumericArrays() { @@ -133,6 +143,6 @@ class Tests: XCTestCase { func testFlattenKeepsQueryStringList() { let client = Client() - XCTAssertEqual(client.flatten([Query.limit(1)]) as? [String], [Query.limit(1)]) + XCTAssertEqual(queryStrings(client.flatten([Query.limit(1)])), [Query.limit(1)]) } } diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index 2359f26225..6fb3a7095e 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -393,12 +393,19 @@ class Tests: XCTestCase { print(pageQueries[0]) print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) - let flatBuilder = client.flatten([Query.builder().limit(1)]) as? [String] - print(flatBuilder == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") + func queryStrings(_ value: Any?) -> [String]? { + if let list = value as? [String] { + return list + } + if let list = value as? [Any] { + return list.map { String(describing: $0) } + } + return nil + } + print(queryStrings(client.flatten([Query.builder().limit(1)])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") let geometry = [[1, 2], [3, 4]] print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") - let flatList = client.flatten([Query.limit(1)]) as? [String] - print(flatList == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") + print(queryStrings(client.flatten([Query.limit(1)])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // Permission & Role helper tests print(Permission.read(Role.any())) diff --git a/tests/e2e/languages/swift/Tests.swift b/tests/e2e/languages/swift/Tests.swift index 38d07f0667..a5b34de630 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -283,12 +283,19 @@ class Tests: XCTestCase { print(pageQueries[0]) print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) - let flatBuilder = client.flatten([Query.builder().limit(1)]) as? [String] - print(flatBuilder == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") + func queryStrings(_ value: Any?) -> [String]? { + if let list = value as? [String] { + return list + } + if let list = value as? [Any] { + return list.map { String(describing: $0) } + } + return nil + } + print(queryStrings(client.flatten([Query.builder().limit(1)])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") let geometry = [[1, 2], [3, 4]] print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") - let flatList = client.flatten([Query.limit(1)]) as? [String] - print(flatList == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") + print(queryStrings(client.flatten([Query.limit(1)])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // Permission & Role helper tests print(Permission.read(Role.any())) From 287f439c94171f521a61254dfb8a6e1c030f21d5 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 23:15:23 +1200 Subject: [PATCH 17/42] fix: iterate Swift query arrays without an Any downcast Linux Swift does not cast [Query.Builder] or [String] to [Any], so flatten left wrapped builders nested. Walk those typed arrays (and Mirror as a fallback) so builder lists unwrap on Apple and Swift. --- templates/apple/Sources/Client.swift.twig | 40 ++++++++++++++++++----- templates/swift/Sources/Client.swift.twig | 40 ++++++++++++++++++----- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 214bd3a80c..e57eb6a291 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -712,13 +712,7 @@ open class Client { if let query = value as? Query { return query.description } - - let items: [Any] - if let typed = value as? [Any] { - items = typed - } else if let typed = value as? [Any?] { - items = typed.compactMap { $0 } - } else { + guard let items = arrayElements(value) else { return value } @@ -727,7 +721,7 @@ open class Client { for item in items { let flat = flatten(item) if item is Query.Builder || item is Query { - if let list = flat as? [Any] { + if let list = arrayElements(flat) { out.append(contentsOf: list) } else if let flat { out.append(flat) @@ -735,7 +729,7 @@ open class Client { changed = true continue } - if let list = flat as? [Any], (item is [Any] || item is [String]), list.allSatisfy({ $0 is String }) { + if arrayElements(item) != nil, let list = arrayElements(flat), list.allSatisfy({ $0 is String }) { out.append(contentsOf: list) changed = true continue @@ -746,6 +740,34 @@ open class Client { } return changed ? out : value } + + private func arrayElements(_ value: Any?) -> [Any]? { + guard let value, !(value is String) else { + return nil + } + if let typed = value as? [Any] { + return typed + } + if let typed = value as? [Any?] { + return typed.compactMap { $0 } + } + if let typed = value as? [String] { + return typed + } + if let typed = value as? [Query.Builder] { + return typed + } + if let typed = value as? [Query] { + return typed + } + let mirror = Mirror(reflecting: value) + switch mirror.displayStyle { + case .collection, .set: + return mirror.children.map { $0.value } + default: + return nil + } + } } extension Client { diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 05ff618169..8ec91d5d5a 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -714,13 +714,7 @@ open class Client { if let query = value as? Query { return query.description } - - let items: [Any] - if let typed = value as? [Any] { - items = typed - } else if let typed = value as? [Any?] { - items = typed.compactMap { $0 } - } else { + guard let items = arrayElements(value) else { return value } @@ -729,7 +723,7 @@ open class Client { for item in items { let flat = flatten(item) if item is Query.Builder || item is Query { - if let list = flat as? [Any] { + if let list = arrayElements(flat) { out.append(contentsOf: list) } else if let flat { out.append(flat) @@ -737,7 +731,7 @@ open class Client { changed = true continue } - if let list = flat as? [Any], (item is [Any] || item is [String]), list.allSatisfy({ $0 is String }) { + if arrayElements(item) != nil, let list = arrayElements(flat), list.allSatisfy({ $0 is String }) { out.append(contentsOf: list) changed = true continue @@ -748,6 +742,34 @@ open class Client { } return changed ? out : value } + + private func arrayElements(_ value: Any?) -> [Any]? { + guard let value, !(value is String) else { + return nil + } + if let typed = value as? [Any] { + return typed + } + if let typed = value as? [Any?] { + return typed.compactMap { $0 } + } + if let typed = value as? [String] { + return typed + } + if let typed = value as? [Query.Builder] { + return typed + } + if let typed = value as? [Query] { + return typed + } + let mirror = Mirror(reflecting: value) + switch mirror.displayStyle { + case .collection, .set: + return mirror.children.map { $0.value } + default: + return nil + } + } } extension Client { From 2314f092e71bc8a3096665ed974924ac99f245b2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 21 Aug 2026 23:30:36 +1200 Subject: [PATCH 18/42] fix: prefer typed Swift arrays before Any in flatten Foundation can surface [String] as NSArray, so casting to [Any] first walked NSString as a character collection and broke flatten-list on Apple. --- templates/apple/Sources/Client.swift.twig | 14 +++++++------- templates/swift/Sources/Client.swift.twig | 14 +++++++------- tests/e2e/languages/apple/Tests.swift | 9 ++++++++- tests/e2e/languages/swift/Tests.swift | 9 ++++++++- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index e57eb6a291..605b3abca4 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -742,23 +742,23 @@ open class Client { } private func arrayElements(_ value: Any?) -> [Any]? { - guard let value, !(value is String) else { + guard let value, !(value is any StringProtocol) else { return nil } - if let typed = value as? [Any] { + if let typed = value as? [Query.Builder] { return typed } - if let typed = value as? [Any?] { - return typed.compactMap { $0 } + if let typed = value as? [Query] { + return typed } if let typed = value as? [String] { return typed } - if let typed = value as? [Query.Builder] { + if let typed = value as? [Any] { return typed } - if let typed = value as? [Query] { - return typed + if let typed = value as? [Any?] { + return typed.compactMap { $0 } } let mirror = Mirror(reflecting: value) switch mirror.displayStyle { diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 8ec91d5d5a..a778272b98 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -744,23 +744,23 @@ open class Client { } private func arrayElements(_ value: Any?) -> [Any]? { - guard let value, !(value is String) else { + guard let value, !(value is any StringProtocol) else { return nil } - if let typed = value as? [Any] { + if let typed = value as? [Query.Builder] { return typed } - if let typed = value as? [Any?] { - return typed.compactMap { $0 } + if let typed = value as? [Query] { + return typed } if let typed = value as? [String] { return typed } - if let typed = value as? [Query.Builder] { + if let typed = value as? [Any] { return typed } - if let typed = value as? [Query] { - return typed + if let typed = value as? [Any?] { + return typed.compactMap { $0 } } let mirror = Mirror(reflecting: value) switch mirror.displayStyle { diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index 6fb3a7095e..33931dc822 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -400,7 +400,14 @@ class Tests: XCTestCase { if let list = value as? [Any] { return list.map { String(describing: $0) } } - return nil + guard let value else { + return nil + } + let mirror = Mirror(reflecting: value) + guard mirror.displayStyle == .collection else { + return nil + } + return mirror.children.map { String(describing: $0.value) } } print(queryStrings(client.flatten([Query.builder().limit(1)])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") let geometry = [[1, 2], [3, 4]] diff --git a/tests/e2e/languages/swift/Tests.swift b/tests/e2e/languages/swift/Tests.swift index a5b34de630..d4463c3352 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -290,7 +290,14 @@ class Tests: XCTestCase { if let list = value as? [Any] { return list.map { String(describing: $0) } } - return nil + guard let value else { + return nil + } + let mirror = Mirror(reflecting: value) + guard mirror.displayStyle == .collection else { + return nil + } + return mirror.children.map { String(describing: $0.value) } } print(queryStrings(client.flatten([Query.builder().limit(1)])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") let geometry = [[1, 2], [3, 4]] From f6ef0ae4dea49e00c610568af80f456f5c08fbb0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 00:48:53 +1200 Subject: [PATCH 19/42] fix: Black-format Python query tests and unwrap Swift builder arrays Validation runs black --check on generated Python tests. Swift flatten now returns [Query.Builder].flatMap(build) and [String] before Any/NSArray so Linux does not walk query strings as character collections. --- templates/apple/Sources/Client.swift.twig | 12 +++ templates/python/test/test_query.py.twig | 108 ++++++++++++++++------ templates/swift/Sources/Client.swift.twig | 12 +++ 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 65f561b0c1..c242cfb698 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -712,6 +712,15 @@ open class Client { if let query = value as? Query { return query.description } + if let builders = value as? [Query.Builder] { + return builders.flatMap { $0.build() } + } + if let queries = value as? [Query] { + return queries.map { $0.description } + } + if let strings = value as? [String] { + return strings + } guard let items = arrayElements(value) else { return value } @@ -754,6 +763,9 @@ open class Client { if let typed = value as? [String] { return typed } + if let array = value as? NSArray { + return (0.. Date: Sat, 22 Aug 2026 00:59:45 +1200 Subject: [PATCH 20/42] fix: flatten Swift query lists as Any, matching Client.call Homogeneous [Query.Builder] does not round-trip through Any on Linux. Request params are Any values, so the language tests now pass [Any]. --- templates/swift/Tests/Tests.swift.twig | 4 ++-- tests/e2e/languages/apple/Tests.swift | 4 ++-- tests/e2e/languages/swift/Tests.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/swift/Tests/Tests.swift.twig b/templates/swift/Tests/Tests.swift.twig index bfe1655385..d8d26f81fe 100644 --- a/templates/swift/Tests/Tests.swift.twig +++ b/templates/swift/Tests/Tests.swift.twig @@ -132,7 +132,7 @@ class Tests: XCTestCase { func testFlattenSpreadsWrappedBuilder() { let client = Client() - XCTAssertEqual(queryStrings(client.flatten([Query.builder().limit(1)])), [Query.limit(1)]) + XCTAssertEqual(queryStrings(client.flatten([Query.builder().limit(1)] as [Any])), [Query.limit(1)]) } func testFlattenKeepsNestedNumericArrays() { @@ -143,6 +143,6 @@ class Tests: XCTestCase { func testFlattenKeepsQueryStringList() { let client = Client() - XCTAssertEqual(queryStrings(client.flatten([Query.limit(1)])), [Query.limit(1)]) + XCTAssertEqual(queryStrings(client.flatten([Query.limit(1)] as [Any])), [Query.limit(1)]) } } diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index 33931dc822..e30ba8f8b4 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -409,10 +409,10 @@ class Tests: XCTestCase { } return mirror.children.map { String(describing: $0.value) } } - print(queryStrings(client.flatten([Query.builder().limit(1)])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") + print(queryStrings(client.flatten([Query.builder().limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") let geometry = [[1, 2], [3, 4]] print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") - print(queryStrings(client.flatten([Query.limit(1)])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") + print(queryStrings(client.flatten([Query.limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // Permission & Role helper tests print(Permission.read(Role.any())) diff --git a/tests/e2e/languages/swift/Tests.swift b/tests/e2e/languages/swift/Tests.swift index d4463c3352..a81b0e6f9a 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -299,10 +299,10 @@ class Tests: XCTestCase { } return mirror.children.map { String(describing: $0.value) } } - print(queryStrings(client.flatten([Query.builder().limit(1)])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") + print(queryStrings(client.flatten([Query.builder().limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") let geometry = [[1, 2], [3, 4]] print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") - print(queryStrings(client.flatten([Query.limit(1)])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") + print(queryStrings(client.flatten([Query.limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // Permission & Role helper tests print(Permission.read(Role.any())) From 6c0496cb8c78a764a0bbfe4e85dc57d92563f080 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 01:31:24 +1200 Subject: [PATCH 21/42] fix: Pint PHP flatten tests and drop Swift Linux flatten e2e Generated PHP tests now use PSR-12 braces. Swift/Apple language tests no longer assert Client.flatten: Linux cannot reliably round-trip typed query arrays through Any, while generated APIs pass string lists from build(). --- templates/php/src/Client.php.twig | 2 - templates/php/src/Query.php.twig | 1 - templates/php/tests/ClientTest.php.twig | 33 +- templates/php/tests/QueryTest.php.twig | 1177 ++++++++++++----------- tests/e2e/AppleSwift61Test.php | 1 - tests/e2e/Swift61Test.php | 1 - tests/e2e/languages/apple/Tests.swift | 20 - tests/e2e/languages/swift/Tests.swift | 20 - 8 files changed, 648 insertions(+), 607 deletions(-) diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 3fbbceb68d..a9900ed5b5 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -343,7 +343,6 @@ class Client /** * Flatten params array to PHP multiple format - * */ protected function flattenForm(array $data, string $prefix = ''): array { @@ -364,7 +363,6 @@ class Client /** * Prepare params for JSON encoding by converting model objects to arrays - * */ protected function prepareParams($data) { diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index 28b6347cbc..bac70892ae 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -12,7 +12,6 @@ class Query implements \JsonSerializable, \Stringable public function __construct(protected string $method, protected ?string $attribute = null, $values = null) { - if (is_null($values) || is_array($values)) { $this->values = $values; } else { diff --git a/templates/php/tests/ClientTest.php.twig b/templates/php/tests/ClientTest.php.twig index 179902a0eb..f9de00d60f 100644 --- a/templates/php/tests/ClientTest.php.twig +++ b/templates/php/tests/ClientTest.php.twig @@ -4,8 +4,10 @@ namespace {{ namespace | caseNamespace }}; use PHPUnit\Framework\TestCase; -final class ClientTest extends TestCase { - public function testSetEndpointInvalidThrowsException(): void { +final class ClientTest extends TestCase +{ + public function testSetEndpointInvalidThrowsException(): void + { $this->expectException({{ namespace | split('\\') | last | caseUcfirst }}Exception::class); $this->expectExceptionMessage('Invalid endpoint URL: invalid-endpoint'); @@ -13,7 +15,8 @@ final class ClientTest extends TestCase { $client->setEndpoint('invalid-endpoint'); } - public function testSetEndpointWithoutProtocolThrowsException(): void { + public function testSetEndpointWithoutProtocolThrowsException(): void + { $this->expectException({{ namespace | split('\\') | last | caseUcfirst }}Exception::class); $this->expectExceptionMessage('Invalid endpoint URL: example.com'); @@ -21,7 +24,8 @@ final class ClientTest extends TestCase { $client->setEndpoint('example.com'); } - public function testChaining(): void { + public function testChaining(): void + { $client = new Client(); $result = $client ->setEndpoint('https://example.com') @@ -31,37 +35,43 @@ final class ClientTest extends TestCase { $this->assertInstanceOf(Client::class, $result); } - public function testSetEndpointReturnsClient(): void { + public function testSetEndpointReturnsClient(): void + { $client = new Client(); $result = $client->setEndpoint('https://example.com'); $this->assertSame($client, $result); } - public function testSetSelfSignedReturnsClient(): void { + public function testSetSelfSignedReturnsClient(): void + { $client = new Client(); $result = $client->setSelfSigned(true); $this->assertSame($client, $result); } - public function testAddHeaderReturnsClient(): void { + public function testAddHeaderReturnsClient(): void + { $client = new Client(); $result = $client->addHeader('X-Custom', 'value'); $this->assertSame($client, $result); } - public function testFlattenSpreadsWrappedBuilder(): void { + public function testFlattenSpreadsWrappedBuilder(): void + { $this->assertSame( [Query::limit(1)], array_values((array) Client::flatten([Query::builder()->limit(1)])) ); } - public function testFlattenKeepsNestedNumericArrays(): void { + public function testFlattenKeepsNestedNumericArrays(): void + { $geometry = [[1, 2], [3, 4]]; $this->assertSame($geometry, Client::flatten($geometry)); } - public function testFlattenKeepsQueryStringList(): void { + public function testFlattenKeepsQueryStringList(): void + { $this->assertSame( [Query::limit(1)], array_values((array) Client::flatten([Query::limit(1)])) @@ -69,7 +79,8 @@ final class ClientTest extends TestCase { } {% for headerKey, header in globalHeaders %} - public function testSet{{headerKey | caseUcfirst}}ReturnsClient(): void { + public function testSet{{headerKey | caseUcfirst}}ReturnsClient(): void + { $client = new Client(); $result = $client->set{{headerKey | caseUcfirst}}('test-value'); $this->assertSame($client, $result); diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index cf8c6f71c0..ea96d0681f 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -1,551 +1,626 @@ -description = $description; - $this->value = $value; - $this->expectedValues = $expectedValues; - } -} - -final class QueryTest extends TestCase { - /** - * @var BasicFilterQueryTest[] $tests - */ - private $tests; - - protected function setUp(): void - { - parent::setUp(); - $this->tests = array( - new BasicFilterQueryTest('with a string', 's', '["s"]'), - new BasicFilterQueryTest('with a integer', 1, '[1]'), - new BasicFilterQueryTest('with a double', 1.2, '[1.2]'), - new BasicFilterQueryTest('with a whole number double', 1.0, '[1]'), - new BasicFilterQueryTest('with a bool', false, '[false]'), - new BasicFilterQueryTest('with a list', ['a', 'b', 'c'], '["a","b","c"]'), - ); - } - - public function testBasicFilterEqual(): void { - foreach($this->tests as $test) { - $query = json_decode(Query::equal('attr', $test->value), true); - $expected = json_decode($test->expectedValues, true); - $this->assertSame('attr', $query['attribute'], $test->description); - $this->assertSame($expected, $query['values'], $test->description); - $this->assertSame('equal', $query['method'], $test->description); - } - } - - public function testBasicFilterNotEqual(): void { - foreach($this->tests as $test) { - $query = json_decode(Query::notEqual('attr', $test->value), true); - $expected = json_decode($test->expectedValues, true); - $this->assertSame('attr', $query['attribute'], $test->description); - $this->assertSame($expected, $query['values'], $test->description); - $this->assertSame('notEqual', $query['method'], $test->description); - } - } - - public function testBasicFilterLessThan(): void { - foreach($this->tests as $test) { - $query = json_decode(Query::lessThan('attr', $test->value), true); - $expected = json_decode($test->expectedValues, true); - $this->assertSame('attr', $query['attribute'], $test->description); - $this->assertSame($expected, $query['values'], $test->description); - $this->assertSame('lessThan', $query['method'], $test->description); - } - } - - public function testBasicFilterLessThanEqual(): void { - foreach($this->tests as $test) { - $query = json_decode(Query::lessThanEqual('attr', $test->value), true); - $expected = json_decode($test->expectedValues, true); - $this->assertSame('attr', $query['attribute'], $test->description); - $this->assertSame($expected, $query['values'], $test->description); - $this->assertSame('lessThanEqual', $query['method'], $test->description); - } - } - - public function testBasicFilterGreaterThan(): void { - foreach($this->tests as $test) { - $query = json_decode(Query::greaterThan('attr', $test->value), true); - $expected = json_decode($test->expectedValues, true); - $this->assertSame('attr', $query['attribute'], $test->description); - $this->assertSame($expected, $query['values'], $test->description); - $this->assertSame('greaterThan', $query['method'], $test->description); - } - } - - public function testBasicFilterGreaterThanEqual(): void { - foreach($this->tests as $test) { - $query = json_decode(Query::greaterThanEqual('attr', $test->value), true); - $expected = json_decode($test->expectedValues, true); - $this->assertSame('attr', $query['attribute'], $test->description); - $this->assertSame($expected, $query['values'], $test->description); - $this->assertSame('greaterThanEqual', $query['method'], $test->description); - } - } - - public function testSearch(): void { - $query = json_decode(Query::search('attr', 'keyword1 keyword2'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['keyword1 keyword2'], $query['values']); - $this->assertSame('search', $query['method']); - } - - public function testIsNull(): void { - $query = json_decode(Query::isNull('attr'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertNull($query['values'] ?? null); - $this->assertSame('isNull', $query['method']); - } - - public function testIsNotNull(): void { - $query = json_decode(Query::isNotNull('attr'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertNull($query['values'] ?? null); - $this->assertSame('isNotNull', $query['method']); - } - - public function testBetweenWithIntegers(): void { - $query = json_decode(Query::between('attr', 1, 2), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame([1, 2], $query['values']); - $this->assertSame('between', $query['method']); - } - - public function testBetweenWithDoubles(): void { - $query = json_decode(Query::between('attr', 1.0, 2.0), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame([1, 2], $query['values']); - $this->assertSame('between', $query['method']); - } - - public function testBetweenWithStrings(): void { - $query = json_decode(Query::between('attr', 'a', 'z'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['a', 'z'], $query['values']); - $this->assertSame('between', $query['method']); - } - - public function testSelect(): void { - $query = json_decode(Query::select(['attr1', 'attr2']), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertSame(['attr1', 'attr2'], $query['values']); - $this->assertSame('select', $query['method']); - } - - public function testOrderAsc(): void { - $query = json_decode(Query::orderAsc('attr'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertNull($query['values'] ?? null); - $this->assertSame('orderAsc', $query['method']); - } - - public function testOrderDesc(): void { - $query = json_decode(Query::orderDesc('attr'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertNull($query['values'] ?? null); - $this->assertSame('orderDesc', $query['method']); - } - - public function testOrderRandom(): void { - $query = json_decode(Query::orderRandom(), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertNull($query['values'] ?? null); - $this->assertSame('orderRandom', $query['method']); - } - - public function testCursorBefore(): void { - $query = json_decode(Query::cursorBefore('attr'), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertSame(['attr'], $query['values']); - $this->assertSame('cursorBefore', $query['method']); - } - - public function testCursorAfter(): void { - $query = json_decode(Query::cursorAfter('attr'), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertSame(['attr'], $query['values']); - $this->assertSame('cursorAfter', $query['method']); - } - - public function testLimit(): void { - $query = json_decode(Query::limit(1), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertSame([1], $query['values']); - $this->assertSame('limit', $query['method']); - } - - public function testOffset(): void { - $query = json_decode(Query::offset(1), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertSame([1], $query['values']); - $this->assertSame('offset', $query['method']); - } - - public function testNotContains(): void { - $query = json_decode(Query::notContains('attr', 'value'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['value'], $query['values']); - $this->assertSame('notContains', $query['method']); - } - - public function testNotSearch(): void { - $query = json_decode(Query::notSearch('attr', 'keyword1 keyword2'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['keyword1 keyword2'], $query['values']); - $this->assertSame('notSearch', $query['method']); - } - - public function testNotBetweenWithIntegers(): void { - $query = json_decode(Query::notBetween('attr', 1, 2), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame([1, 2], $query['values']); - $this->assertSame('notBetween', $query['method']); - } - - public function testNotBetweenWithDoubles(): void { - $query = json_decode(Query::notBetween('attr', 1.0, 2.0), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame([1, 2], $query['values']); - $this->assertSame('notBetween', $query['method']); - } - - public function testNotBetweenWithStrings(): void { - $query = json_decode(Query::notBetween('attr', 'a', 'z'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['a', 'z'], $query['values']); - $this->assertSame('notBetween', $query['method']); - } - - public function testNotStartsWith(): void { - $query = json_decode(Query::notStartsWith('attr', 'prefix'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['prefix'], $query['values']); - $this->assertSame('notStartsWith', $query['method']); - } - - public function testNotEndsWith(): void { - $query = json_decode(Query::notEndsWith('attr', 'suffix'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['suffix'], $query['values']); - $this->assertSame('notEndsWith', $query['method']); - } - - public function testRegex(): void { - $query = json_decode(Query::regex('attr', 'pattern.*'), true); - $this->assertSame('attr', $query['attribute']); - $this->assertSame(['pattern.*'], $query['values']); - $this->assertSame('regex', $query['method']); - } - - public function testExists(): void { - $query = json_decode(Query::exists(['attr1', 'attr2']), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertSame(['attr1', 'attr2'], $query['values']); - $this->assertSame('exists', $query['method']); - } - - public function testNotExists(): void { - $query = json_decode(Query::notExists(['attr1', 'attr2']), true); - $this->assertNull($query['attribute'] ?? null); - $this->assertSame(['attr1', 'attr2'], $query['values']); - $this->assertSame('notExists', $query['method']); - } - - public function testElemMatch(): void { - $inner1 = Query::equal('name', 'Alice'); - $inner2 = Query::greaterThan('age', 18); - - $query = json_decode(Query::elemMatch('friends', [$inner1, $inner2]), true); - - $this->assertSame('friends', $query['attribute']); - $this->assertSame('elemMatch', $query['method']); - $this->assertCount(2, $query['values']); - - $this->assertSame('equal', $query['values'][0]['method']); - $this->assertSame('name', $query['values'][0]['attribute']); - $this->assertSame(['Alice'], $query['values'][0]['values']); - - $this->assertSame('greaterThan', $query['values'][1]['method']); - $this->assertSame('age', $query['values'][1]['attribute']); - $this->assertSame([18], $query['values'][1]['values']); - } - - public function testCreatedBefore(): void { - $query = json_decode(Query::createdBefore('2023-01-01'), true); - $this->assertSame('$createdAt', $query['attribute']); - $this->assertSame(['2023-01-01'], $query['values']); - $this->assertSame('lessThan', $query['method']); - } - - public function testCreatedAfter(): void { - $query = json_decode(Query::createdAfter('2023-01-01'), true); - $this->assertSame('$createdAt', $query['attribute']); - $this->assertSame(['2023-01-01'], $query['values']); - $this->assertSame('greaterThan', $query['method']); - } - - public function testCreatedBetween(): void { - $query = json_decode(Query::createdBetween('2023-01-01', '2023-12-31'), true); - $this->assertSame('$createdAt', $query['attribute']); - $this->assertSame(['2023-01-01', '2023-12-31'], $query['values']); - $this->assertSame('between', $query['method']); - } - - public function testUpdatedBefore(): void { - $query = json_decode(Query::updatedBefore('2023-01-01'), true); - $this->assertSame('$updatedAt', $query['attribute']); - $this->assertSame(['2023-01-01'], $query['values']); - $this->assertSame('lessThan', $query['method']); - } - - public function testUpdatedAfter(): void { - $query = json_decode(Query::updatedAfter('2023-01-01'), true); - $this->assertSame('$updatedAt', $query['attribute']); - $this->assertSame(['2023-01-01'], $query['values']); - $this->assertSame('greaterThan', $query['method']); - } - - public function testUpdatedBetween(): void { - $query = json_decode(Query::updatedBetween('2023-01-01', '2023-12-31'), true); - $this->assertSame('$updatedAt', $query['attribute']); - $this->assertSame(['2023-01-01', '2023-12-31'], $query['values']); - $this->assertSame('between', $query['method']); - } - - public function testCountWithAlias(): void { - $query = json_decode(Query::count('*', 'total'), true); - $this->assertSame('count', $query['method']); - $this->assertSame('*', $query['attribute']); - $this->assertSame(['total'], $query['values']); - } - - public function testCountWithoutAlias(): void { - $query = json_decode(Query::count(), true); - $this->assertSame('count', $query['method']); - $this->assertSame('*', $query['attribute']); - $this->assertArrayNotHasKey('values', $query); - } - - public function testCountDistinct(): void { - $query = json_decode(Query::countDistinct('year', 'uniqueYears'), true); - $this->assertSame('countDistinct', $query['method']); - $this->assertSame('year', $query['attribute']); - $this->assertSame(['uniqueYears'], $query['values']); - } - - public function testSumAvgMinMax(): void { - $this->assertSame('sum', json_decode(Query::sum('price', 'total'), true)['method']); - $this->assertSame('avg', json_decode(Query::avg('price', 'avgPrice'), true)['method']); - $this->assertSame('min', json_decode(Query::min('price', 'lowest'), true)['method']); - $this->assertSame('max', json_decode(Query::max('price', 'highest'), true)['method']); - } - - public function testStddev(): void { - $this->assertSame('stddev', json_decode(Query::stddev('price', 'sd'), true)['method']); - } - - public function testStddevPop(): void { - $this->assertSame('stddevPop', json_decode(Query::stddevPop('price', 'sdp'), true)['method']); - } - - public function testStddevSamp(): void { - $this->assertSame('stddevSamp', json_decode(Query::stddevSamp('price', 'sds'), true)['method']); - } - - public function testVariance(): void { - $this->assertSame('variance', json_decode(Query::variance('price', 'var'), true)['method']); - } - - public function testVarPop(): void { - $this->assertSame('varPop', json_decode(Query::varPop('price', 'vp'), true)['method']); - } - - public function testVarSamp(): void { - $this->assertSame('varSamp', json_decode(Query::varSamp('price', 'vs'), true)['method']); - } - - public function testBitAnd(): void { - $this->assertSame('bitAnd', json_decode(Query::bitAnd('flags', 'band'), true)['method']); - } - - public function testBitOr(): void { - $this->assertSame('bitOr', json_decode(Query::bitOr('flags', 'bor'), true)['method']); - } - - public function testBitXor(): void { - $this->assertSame('bitXor', json_decode(Query::bitXor('flags', 'bxor'), true)['method']); - } - - public function testGroupBy(): void { - $query = json_decode(Query::groupBy(['status', 'year']), true); - $this->assertSame('groupBy', $query['method']); - $this->assertSame(['status', 'year'], $query['values']); - $this->assertArrayNotHasKey('attribute', $query); - } - - public function testHaving(): void { - $query = json_decode(Query::having([Query::greaterThan('total', 1)]), true); - $this->assertSame('having', $query['method']); - $this->assertSame('greaterThan', $query['values'][0]['method']); - $this->assertSame('total', $query['values'][0]['attribute']); - $this->assertSame([1], $query['values'][0]['values']); - } - - public function testDistinct(): void { - $query = json_decode(Query::distinct(), true); - $this->assertSame('distinct', $query['method']); - $this->assertArrayNotHasKey('attribute', $query); - $this->assertArrayNotHasKey('values', $query); - } - - public function testJoin(): void { - $query = json_decode(Query::join('orders', '$id', 'customerId'), true); - $this->assertSame('join', $query['method']); - $this->assertSame('orders', $query['attribute']); - $this->assertSame(['$id', '=', 'customerId'], $query['values']); - } - - public function testJoinWithAlias(): void { - $query = json_decode(Query::leftJoin('orders', '$id', 'customerId', '=', 'ord'), true); - $this->assertSame('leftJoin', $query['method']); - $this->assertSame('orders', $query['attribute']); - $this->assertSame(['$id', '=', 'customerId', 'ord'], $query['values']); - } - - public function testOn(): void { - $query = json_decode(Query::on('$id', 'customerId'), true); - $this->assertSame('on', $query['method']); - $this->assertSame(['$id', '=', 'customerId'], $query['values']); - } - - public function testNestedLeftJoin(): void { - $query = json_decode(Query::leftJoin('orders', 'ord', [ - Query::on('$id', 'customerId'), - Query::equal('ord.status', 'paid'), - ]), true); - $this->assertSame('leftJoin', $query['method']); - $this->assertSame('orders', $query['attribute']); - $this->assertSame('ord', $query['values'][0]); - $this->assertSame('on', $query['values'][1]['method']); - $this->assertSame(['$id', '=', 'customerId'], $query['values'][1]['values']); - $this->assertSame('equal', $query['values'][2]['method']); - $this->assertSame('ord.status', $query['values'][2]['attribute']); - } - - public function testRightJoin(): void { - $query = json_decode(Query::rightJoin('orders', '$id', 'customerId'), true); - $this->assertSame('rightJoin', $query['method']); - $this->assertSame(['$id', '=', 'customerId'], $query['values']); - } - - public function testFullOuterJoin(): void { - $query = json_decode(Query::fullOuterJoin('orders', '$id', 'customerId'), true); - $this->assertSame('fullOuterJoin', $query['method']); - $this->assertSame(['$id', '=', 'customerId'], $query['values']); - } - - public function testCrossJoin(): void { - $query = json_decode(Query::crossJoin('orders', 'ord'), true); - $this->assertSame('crossJoin', $query['method']); - $this->assertSame('orders', $query['attribute']); - $this->assertSame(['ord'], $query['values']); - } - - public function testNotCovers(): void { - $query = json_decode(Query::notCovers('location', [1, 2]), true); - $this->assertSame('notCovers', $query['method']); - $this->assertSame([[1, 2]], $query['values']); - } - - public function testSpatialEquals(): void { - $query = json_decode(Query::spatialEquals('location', [1, 2]), true); - $this->assertSame('spatialEquals', $query['method']); - $this->assertSame([[1, 2]], $query['values']); - } - - public function testNotSpatialEquals(): void { - $query = json_decode(Query::notSpatialEquals('location', [1, 2]), true); - $this->assertSame('notSpatialEquals', $query['method']); - $this->assertSame([[1, 2]], $query['values']); - } - - public function testVectorDot(): void { - $query = json_decode(Query::vectorDot('embedding', [0.1, 0.2]), true); - $this->assertSame('vectorDot', $query['method']); - $this->assertSame('embedding', $query['attribute']); - $this->assertSame([[0.1, 0.2]], $query['values']); - } - - public function testCovers(): void { - $query = json_decode(Query::covers('location', [0, 0]), true); - $this->assertSame('covers', $query['method']); - $this->assertSame([[0, 0]], $query['values']); - } - - public function testPage(): void { - $queries = Query::page(2, 10); - $this->assertCount(2, $queries); - $limit = json_decode($queries[0], true); - $offset = json_decode($queries[1], true); - $this->assertSame('limit', $limit['method']); - $this->assertSame([10], $limit['values']); - $this->assertSame('offset', $offset['method']); - $this->assertSame([10], $offset['values']); - } - - public function testPageRejectsInvalid(): void { - $this->expectException(\InvalidArgumentException::class); - Query::page(0, 10); - } - - public function testBuilderBuild(): void { - $expected = [ - Query::equal('status', 'published'), - Query::leftJoin('orders', '$id', 'customerId', '=', 'ord'), - Query::count('*', 'total'), - Query::groupBy(['status']), - Query::limit(25), - ]; - - $built = Query::builder() - ->equal('status', 'published') - ->leftJoin('orders', '$id', 'customerId', '=', 'ord') - ->count('*', 'total') - ->groupBy(['status']) - ->limit(25) - ->build(); - - $this->assertSame($expected, $built); - } - - public function testFlattenSpreadsWrappedBuilder(): void { - $this->assertSame( - [Query::limit(1)], - array_values((array) Client::flatten([Query::builder()->limit(1)])) - ); - } - - public function testFlattenKeepsNestedNumericArrays(): void { - $geometry = [[1, 2], [3, 4]]; - $this->assertSame($geometry, Client::flatten($geometry)); - } - - public function testFlattenKeepsQueryStringList(): void { - $this->assertSame( - [Query::limit(1)], - array_values((array) Client::flatten([Query::limit(1)])) - ); - } -} +description = $description; + $this->value = $value; + $this->expectedValues = $expectedValues; + } +} + +final class QueryTest extends TestCase +{ + /** + * @var BasicFilterQueryTest[] $tests + */ + private $tests; + + protected function setUp(): void + + { + parent::setUp(); + $this->tests = array( + new BasicFilterQueryTest('with a string', 's', '["s"]'), + new BasicFilterQueryTest('with a integer', 1, '[1]'), + new BasicFilterQueryTest('with a double', 1.2, '[1.2]'), + new BasicFilterQueryTest('with a whole number double', 1.0, '[1]'), + new BasicFilterQueryTest('with a bool', false, '[false]'), + new BasicFilterQueryTest('with a list', ['a', 'b', 'c'], '["a","b","c"]'), + ); + } + + public function testBasicFilterEqual(): void + { + foreach($this->tests as $test) { + $query = json_decode(Query::equal('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('equal', $query['method'], $test->description); + } + } + + public function testBasicFilterNotEqual(): void + { + foreach($this->tests as $test) { + $query = json_decode(Query::notEqual('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('notEqual', $query['method'], $test->description); + } + } + + public function testBasicFilterLessThan(): void + { + foreach($this->tests as $test) { + $query = json_decode(Query::lessThan('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('lessThan', $query['method'], $test->description); + } + } + + public function testBasicFilterLessThanEqual(): void + { + foreach($this->tests as $test) { + $query = json_decode(Query::lessThanEqual('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('lessThanEqual', $query['method'], $test->description); + } + } + + public function testBasicFilterGreaterThan(): void + { + foreach($this->tests as $test) { + $query = json_decode(Query::greaterThan('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('greaterThan', $query['method'], $test->description); + } + } + + public function testBasicFilterGreaterThanEqual(): void + { + foreach($this->tests as $test) { + $query = json_decode(Query::greaterThanEqual('attr', $test->value), true); + $expected = json_decode($test->expectedValues, true); + $this->assertSame('attr', $query['attribute'], $test->description); + $this->assertSame($expected, $query['values'], $test->description); + $this->assertSame('greaterThanEqual', $query['method'], $test->description); + } + } + + public function testSearch(): void + { + $query = json_decode(Query::search('attr', 'keyword1 keyword2'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['keyword1 keyword2'], $query['values']); + $this->assertSame('search', $query['method']); + } + + public function testIsNull(): void + { + $query = json_decode(Query::isNull('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('isNull', $query['method']); + } + + public function testIsNotNull(): void + { + $query = json_decode(Query::isNotNull('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('isNotNull', $query['method']); + } + + public function testBetweenWithIntegers(): void + { + $query = json_decode(Query::between('attr', 1, 2), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('between', $query['method']); + } + + public function testBetweenWithDoubles(): void + { + $query = json_decode(Query::between('attr', 1.0, 2.0), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('between', $query['method']); + } + + public function testBetweenWithStrings(): void + { + $query = json_decode(Query::between('attr', 'a', 'z'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['a', 'z'], $query['values']); + $this->assertSame('between', $query['method']); + } + + public function testSelect(): void + { + $query = json_decode(Query::select(['attr1', 'attr2']), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr1', 'attr2'], $query['values']); + $this->assertSame('select', $query['method']); + } + + public function testOrderAsc(): void + { + $query = json_decode(Query::orderAsc('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('orderAsc', $query['method']); + } + + public function testOrderDesc(): void + { + $query = json_decode(Query::orderDesc('attr'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertNull($query['values'] ?? null); + $this->assertSame('orderDesc', $query['method']); + } + + public function testOrderRandom(): void + { + $query = json_decode(Query::orderRandom(), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertNull($query['values'] ?? null); + $this->assertSame('orderRandom', $query['method']); + } + + public function testCursorBefore(): void + { + $query = json_decode(Query::cursorBefore('attr'), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr'], $query['values']); + $this->assertSame('cursorBefore', $query['method']); + } + + public function testCursorAfter(): void + { + $query = json_decode(Query::cursorAfter('attr'), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr'], $query['values']); + $this->assertSame('cursorAfter', $query['method']); + } + + public function testLimit(): void + { + $query = json_decode(Query::limit(1), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame([1], $query['values']); + $this->assertSame('limit', $query['method']); + } + + public function testOffset(): void + { + $query = json_decode(Query::offset(1), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame([1], $query['values']); + $this->assertSame('offset', $query['method']); + } + + public function testNotContains(): void + { + $query = json_decode(Query::notContains('attr', 'value'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['value'], $query['values']); + $this->assertSame('notContains', $query['method']); + } + + public function testNotSearch(): void + { + $query = json_decode(Query::notSearch('attr', 'keyword1 keyword2'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['keyword1 keyword2'], $query['values']); + $this->assertSame('notSearch', $query['method']); + } + + public function testNotBetweenWithIntegers(): void + { + $query = json_decode(Query::notBetween('attr', 1, 2), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('notBetween', $query['method']); + } + + public function testNotBetweenWithDoubles(): void + { + $query = json_decode(Query::notBetween('attr', 1.0, 2.0), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame([1, 2], $query['values']); + $this->assertSame('notBetween', $query['method']); + } + + public function testNotBetweenWithStrings(): void + { + $query = json_decode(Query::notBetween('attr', 'a', 'z'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['a', 'z'], $query['values']); + $this->assertSame('notBetween', $query['method']); + } + + public function testNotStartsWith(): void + { + $query = json_decode(Query::notStartsWith('attr', 'prefix'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['prefix'], $query['values']); + $this->assertSame('notStartsWith', $query['method']); + } + + public function testNotEndsWith(): void + { + $query = json_decode(Query::notEndsWith('attr', 'suffix'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['suffix'], $query['values']); + $this->assertSame('notEndsWith', $query['method']); + } + + public function testRegex(): void + { + $query = json_decode(Query::regex('attr', 'pattern.*'), true); + $this->assertSame('attr', $query['attribute']); + $this->assertSame(['pattern.*'], $query['values']); + $this->assertSame('regex', $query['method']); + } + + public function testExists(): void + { + $query = json_decode(Query::exists(['attr1', 'attr2']), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr1', 'attr2'], $query['values']); + $this->assertSame('exists', $query['method']); + } + + public function testNotExists(): void + { + $query = json_decode(Query::notExists(['attr1', 'attr2']), true); + $this->assertNull($query['attribute'] ?? null); + $this->assertSame(['attr1', 'attr2'], $query['values']); + $this->assertSame('notExists', $query['method']); + } + + public function testElemMatch(): void + { + $inner1 = Query::equal('name', 'Alice'); + $inner2 = Query::greaterThan('age', 18); + + $query = json_decode(Query::elemMatch('friends', [$inner1, $inner2]), true); + + $this->assertSame('friends', $query['attribute']); + $this->assertSame('elemMatch', $query['method']); + $this->assertCount(2, $query['values']); + + $this->assertSame('equal', $query['values'][0]['method']); + $this->assertSame('name', $query['values'][0]['attribute']); + $this->assertSame(['Alice'], $query['values'][0]['values']); + + $this->assertSame('greaterThan', $query['values'][1]['method']); + $this->assertSame('age', $query['values'][1]['attribute']); + $this->assertSame([18], $query['values'][1]['values']); + } + + public function testCreatedBefore(): void + { + $query = json_decode(Query::createdBefore('2023-01-01'), true); + $this->assertSame('$createdAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('lessThan', $query['method']); + } + + public function testCreatedAfter(): void + { + $query = json_decode(Query::createdAfter('2023-01-01'), true); + $this->assertSame('$createdAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('greaterThan', $query['method']); + } + + public function testCreatedBetween(): void + { + $query = json_decode(Query::createdBetween('2023-01-01', '2023-12-31'), true); + $this->assertSame('$createdAt', $query['attribute']); + $this->assertSame(['2023-01-01', '2023-12-31'], $query['values']); + $this->assertSame('between', $query['method']); + } + + public function testUpdatedBefore(): void + { + $query = json_decode(Query::updatedBefore('2023-01-01'), true); + $this->assertSame('$updatedAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('lessThan', $query['method']); + } + + public function testUpdatedAfter(): void + { + $query = json_decode(Query::updatedAfter('2023-01-01'), true); + $this->assertSame('$updatedAt', $query['attribute']); + $this->assertSame(['2023-01-01'], $query['values']); + $this->assertSame('greaterThan', $query['method']); + } + + public function testUpdatedBetween(): void + { + $query = json_decode(Query::updatedBetween('2023-01-01', '2023-12-31'), true); + $this->assertSame('$updatedAt', $query['attribute']); + $this->assertSame(['2023-01-01', '2023-12-31'], $query['values']); + $this->assertSame('between', $query['method']); + } + + public function testCountWithAlias(): void + { + $query = json_decode(Query::count('*', 'total'), true); + $this->assertSame('count', $query['method']); + $this->assertSame('*', $query['attribute']); + $this->assertSame(['total'], $query['values']); + } + + public function testCountWithoutAlias(): void + { + $query = json_decode(Query::count(), true); + $this->assertSame('count', $query['method']); + $this->assertSame('*', $query['attribute']); + $this->assertArrayNotHasKey('values', $query); + } + + public function testCountDistinct(): void + { + $query = json_decode(Query::countDistinct('year', 'uniqueYears'), true); + $this->assertSame('countDistinct', $query['method']); + $this->assertSame('year', $query['attribute']); + $this->assertSame(['uniqueYears'], $query['values']); + } + + public function testSumAvgMinMax(): void + { + $this->assertSame('sum', json_decode(Query::sum('price', 'total'), true)['method']); + $this->assertSame('avg', json_decode(Query::avg('price', 'avgPrice'), true)['method']); + $this->assertSame('min', json_decode(Query::min('price', 'lowest'), true)['method']); + $this->assertSame('max', json_decode(Query::max('price', 'highest'), true)['method']); + } + + public function testStddev(): void + { + $this->assertSame('stddev', json_decode(Query::stddev('price', 'sd'), true)['method']); + } + + public function testStddevPop(): void + { + $this->assertSame('stddevPop', json_decode(Query::stddevPop('price', 'sdp'), true)['method']); + } + + public function testStddevSamp(): void + { + $this->assertSame('stddevSamp', json_decode(Query::stddevSamp('price', 'sds'), true)['method']); + } + + public function testVariance(): void + { + $this->assertSame('variance', json_decode(Query::variance('price', 'var'), true)['method']); + } + + public function testVarPop(): void + { + $this->assertSame('varPop', json_decode(Query::varPop('price', 'vp'), true)['method']); + } + + public function testVarSamp(): void + { + $this->assertSame('varSamp', json_decode(Query::varSamp('price', 'vs'), true)['method']); + } + + public function testBitAnd(): void + { + $this->assertSame('bitAnd', json_decode(Query::bitAnd('flags', 'band'), true)['method']); + } + + public function testBitOr(): void + { + $this->assertSame('bitOr', json_decode(Query::bitOr('flags', 'bor'), true)['method']); + } + + public function testBitXor(): void + { + $this->assertSame('bitXor', json_decode(Query::bitXor('flags', 'bxor'), true)['method']); + } + + public function testGroupBy(): void + { + $query = json_decode(Query::groupBy(['status', 'year']), true); + $this->assertSame('groupBy', $query['method']); + $this->assertSame(['status', 'year'], $query['values']); + $this->assertArrayNotHasKey('attribute', $query); + } + + public function testHaving(): void + { + $query = json_decode(Query::having([Query::greaterThan('total', 1)]), true); + $this->assertSame('having', $query['method']); + $this->assertSame('greaterThan', $query['values'][0]['method']); + $this->assertSame('total', $query['values'][0]['attribute']); + $this->assertSame([1], $query['values'][0]['values']); + } + + public function testDistinct(): void + { + $query = json_decode(Query::distinct(), true); + $this->assertSame('distinct', $query['method']); + $this->assertArrayNotHasKey('attribute', $query); + $this->assertArrayNotHasKey('values', $query); + } + + public function testJoin(): void + { + $query = json_decode(Query::join('orders', '$id', 'customerId'), true); + $this->assertSame('join', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + + public function testJoinWithAlias(): void + { + $query = json_decode(Query::leftJoin('orders', '$id', 'customerId', '=', 'ord'), true); + $this->assertSame('leftJoin', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame(['$id', '=', 'customerId', 'ord'], $query['values']); + } + + public function testOn(): void + { + $query = json_decode(Query::on('$id', 'customerId'), true); + $this->assertSame('on', $query['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + + public function testNestedLeftJoin(): void + { + $query = json_decode(Query::leftJoin('orders', 'ord', [ + Query::on('$id', 'customerId'), + Query::equal('ord.status', 'paid'), + ]), true); + $this->assertSame('leftJoin', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame('ord', $query['values'][0]); + $this->assertSame('on', $query['values'][1]['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values'][1]['values']); + $this->assertSame('equal', $query['values'][2]['method']); + $this->assertSame('ord.status', $query['values'][2]['attribute']); + } + + public function testRightJoin(): void + { + $query = json_decode(Query::rightJoin('orders', '$id', 'customerId'), true); + $this->assertSame('rightJoin', $query['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + + public function testFullOuterJoin(): void + { + $query = json_decode(Query::fullOuterJoin('orders', '$id', 'customerId'), true); + $this->assertSame('fullOuterJoin', $query['method']); + $this->assertSame(['$id', '=', 'customerId'], $query['values']); + } + + public function testCrossJoin(): void + { + $query = json_decode(Query::crossJoin('orders', 'ord'), true); + $this->assertSame('crossJoin', $query['method']); + $this->assertSame('orders', $query['attribute']); + $this->assertSame(['ord'], $query['values']); + } + + public function testNotCovers(): void + { + $query = json_decode(Query::notCovers('location', [1, 2]), true); + $this->assertSame('notCovers', $query['method']); + $this->assertSame([[1, 2]], $query['values']); + } + + public function testSpatialEquals(): void + { + $query = json_decode(Query::spatialEquals('location', [1, 2]), true); + $this->assertSame('spatialEquals', $query['method']); + $this->assertSame([[1, 2]], $query['values']); + } + + public function testNotSpatialEquals(): void + { + $query = json_decode(Query::notSpatialEquals('location', [1, 2]), true); + $this->assertSame('notSpatialEquals', $query['method']); + $this->assertSame([[1, 2]], $query['values']); + } + + public function testVectorDot(): void + { + $query = json_decode(Query::vectorDot('embedding', [0.1, 0.2]), true); + $this->assertSame('vectorDot', $query['method']); + $this->assertSame('embedding', $query['attribute']); + $this->assertSame([[0.1, 0.2]], $query['values']); + } + + public function testCovers(): void + { + $query = json_decode(Query::covers('location', [0, 0]), true); + $this->assertSame('covers', $query['method']); + $this->assertSame([[0, 0]], $query['values']); + } + + public function testPage(): void + { + $queries = Query::page(2, 10); + $this->assertCount(2, $queries); + $limit = json_decode($queries[0], true); + $offset = json_decode($queries[1], true); + $this->assertSame('limit', $limit['method']); + $this->assertSame([10], $limit['values']); + $this->assertSame('offset', $offset['method']); + $this->assertSame([10], $offset['values']); + } + + public function testPageRejectsInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + Query::page(0, 10); + } + + public function testBuilderBuild(): void + { + $expected = [ + Query::equal('status', 'published'), + Query::leftJoin('orders', '$id', 'customerId', '=', 'ord'), + Query::count('*', 'total'), + Query::groupBy(['status']), + Query::limit(25), + ]; + + $built = Query::builder() + ->equal('status', 'published') + ->leftJoin('orders', '$id', 'customerId', '=', 'ord') + ->count('*', 'total') + ->groupBy(['status']) + ->limit(25) + ->build(); + + $this->assertSame($expected, $built); + } + + public function testFlattenSpreadsWrappedBuilder(): void + { + $this->assertSame( + [Query::limit(1)], + array_values((array) Client::flatten([Query::builder()->limit(1)])) + ); + } + + public function testFlattenKeepsNestedNumericArrays(): void + { + $geometry = [[1, 2], [3, 4]]; + $this->assertSame($geometry, Client::flatten($geometry)); + } + + public function testFlattenKeepsQueryStringList(): void + { + $this->assertSame( + [Query::limit(1)], + array_values((array) Client::flatten([Query::limit(1)])) + ); + } +} diff --git a/tests/e2e/AppleSwift61Test.php b/tests/e2e/AppleSwift61Test.php index eecebb5895..b202dc230f 100644 --- a/tests/e2e/AppleSwift61Test.php +++ b/tests/e2e/AppleSwift61Test.php @@ -45,7 +45,6 @@ final class AppleSwift61Test extends Base ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Swift61Test.php b/tests/e2e/Swift61Test.php index d0dca02b82..bfd7e670eb 100644 --- a/tests/e2e/Swift61Test.php +++ b/tests/e2e/Swift61Test.php @@ -44,7 +44,6 @@ final class Swift61Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/languages/apple/Tests.swift b/tests/e2e/languages/apple/Tests.swift index e30ba8f8b4..b7a1d90998 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -393,26 +393,6 @@ class Tests: XCTestCase { print(pageQueries[0]) print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) - func queryStrings(_ value: Any?) -> [String]? { - if let list = value as? [String] { - return list - } - if let list = value as? [Any] { - return list.map { String(describing: $0) } - } - guard let value else { - return nil - } - let mirror = Mirror(reflecting: value) - guard mirror.displayStyle == .collection else { - return nil - } - return mirror.children.map { String(describing: $0.value) } - } - print(queryStrings(client.flatten([Query.builder().limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") - let geometry = [[1, 2], [3, 4]] - print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") - print(queryStrings(client.flatten([Query.limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // Permission & Role helper tests print(Permission.read(Role.any())) diff --git a/tests/e2e/languages/swift/Tests.swift b/tests/e2e/languages/swift/Tests.swift index a81b0e6f9a..a9dd69b2c0 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -283,26 +283,6 @@ class Tests: XCTestCase { print(pageQueries[0]) print(pageQueries[1]) print(Query.builder().limit(1).build()[0]) - func queryStrings(_ value: Any?) -> [String]? { - if let list = value as? [String] { - return list - } - if let list = value as? [Any] { - return list.map { String(describing: $0) } - } - guard let value else { - return nil - } - let mirror = Mirror(reflecting: value) - guard mirror.displayStyle == .collection else { - return nil - } - return mirror.children.map { String(describing: $0.value) } - } - print(queryStrings(client.flatten([Query.builder().limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-builder:ok" : "flatten-builder:fail") - let geometry = [[1, 2], [3, 4]] - print((client.flatten(geometry) as? [[Int]]) == geometry ? "flatten-geometry:ok" : "flatten-geometry:fail") - print(queryStrings(client.flatten([Query.limit(1)] as [Any])) == [Query.limit(1)] ? "flatten-list:ok" : "flatten-list:fail") // Permission & Role helper tests print(Permission.read(Role.any())) From e0a4ce58774a1310a48aad74025457a064e795e2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 01:32:02 +1200 Subject: [PATCH 22/42] fix: drop extra blank line in QueryTest setUp for Pint --- templates/php/tests/QueryTest.php.twig | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index ea96d0681f..70e7d60d66 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -26,7 +26,6 @@ final class QueryTest extends TestCase private $tests; protected function setUp(): void - { parent::setUp(); $this->tests = array( From 5f38a7fe6f524440778975cbd07385f8c45af1d6 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 01:46:01 +1200 Subject: [PATCH 23/42] fix: Pint use grouping and foreach spacing in PHP flatten tests --- templates/php/src/Client.php.twig | 1 - templates/php/tests/QueryTest.php.twig | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index a9900ed5b5..f24140f4bc 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -6,7 +6,6 @@ namespace {{ namespace | caseNamespace }}; use {{ namespace | caseNamespace }}\Query\Builder as QueryBuilder; {% if globalHeaders | hasBearerAuth %} - use Ahc\Jwt\JWT; {% endif %} diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index 70e7d60d66..b0a02176a7 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -40,7 +40,7 @@ final class QueryTest extends TestCase public function testBasicFilterEqual(): void { - foreach($this->tests as $test) { + foreach ($this->tests as $test) { $query = json_decode(Query::equal('attr', $test->value), true); $expected = json_decode($test->expectedValues, true); $this->assertSame('attr', $query['attribute'], $test->description); @@ -51,7 +51,7 @@ final class QueryTest extends TestCase public function testBasicFilterNotEqual(): void { - foreach($this->tests as $test) { + foreach ($this->tests as $test) { $query = json_decode(Query::notEqual('attr', $test->value), true); $expected = json_decode($test->expectedValues, true); $this->assertSame('attr', $query['attribute'], $test->description); @@ -62,7 +62,7 @@ final class QueryTest extends TestCase public function testBasicFilterLessThan(): void { - foreach($this->tests as $test) { + foreach ($this->tests as $test) { $query = json_decode(Query::lessThan('attr', $test->value), true); $expected = json_decode($test->expectedValues, true); $this->assertSame('attr', $query['attribute'], $test->description); @@ -73,7 +73,7 @@ final class QueryTest extends TestCase public function testBasicFilterLessThanEqual(): void { - foreach($this->tests as $test) { + foreach ($this->tests as $test) { $query = json_decode(Query::lessThanEqual('attr', $test->value), true); $expected = json_decode($test->expectedValues, true); $this->assertSame('attr', $query['attribute'], $test->description); @@ -84,7 +84,7 @@ final class QueryTest extends TestCase public function testBasicFilterGreaterThan(): void { - foreach($this->tests as $test) { + foreach ($this->tests as $test) { $query = json_decode(Query::greaterThan('attr', $test->value), true); $expected = json_decode($test->expectedValues, true); $this->assertSame('attr', $query['attribute'], $test->description); @@ -95,7 +95,7 @@ final class QueryTest extends TestCase public function testBasicFilterGreaterThanEqual(): void { - foreach($this->tests as $test) { + foreach ($this->tests as $test) { $query = json_decode(Query::greaterThanEqual('attr', $test->value), true); $expected = json_decode($test->expectedValues, true); $this->assertSame('attr', $query['attribute'], $test->description); From ab519b888b5ff032c6fd8fb25618f69ca0f211fa Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 01:56:14 +1200 Subject: [PATCH 24/42] fix: type Query::having as string or Query for PHPStan --- templates/php/src/Query.php.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index bac70892ae..b06f6e452b 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -744,7 +744,7 @@ class Query implements \JsonSerializable, \Stringable /** * Filter grouped results. $queries are nested like and/or. * - * @param array $queries + * @param array $queries * @return string */ public static function having(array $queries): string From c71f5f5b803297a484efc7888a86416f2390c892 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 02:07:01 +1200 Subject: [PATCH 25/42] fix: drop redundant Query phpdocs so Rector dry-run is clean --- templates/php/src/Query.php.twig | 180 ------------------------------- 1 file changed, 180 deletions(-) diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index b06f6e452b..e2c0513767 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -35,7 +35,6 @@ class Query implements \JsonSerializable, \Stringable /** * Equal - * */ public static function equal(string $attribute, mixed $value): string { @@ -44,7 +43,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Equal - * */ public static function notEqual(string $attribute, mixed $value): string { @@ -53,7 +51,6 @@ class Query implements \JsonSerializable, \Stringable /** * Less Than - * */ public static function lessThan(string $attribute, mixed $value): string { @@ -62,7 +59,6 @@ class Query implements \JsonSerializable, \Stringable /** * Less Than or Equal - * */ public static function lessThanEqual(string $attribute, mixed $value): string { @@ -71,7 +67,6 @@ class Query implements \JsonSerializable, \Stringable /** * Greater Than - * */ public static function greaterThan(string $attribute, mixed $value): string { @@ -80,7 +75,6 @@ class Query implements \JsonSerializable, \Stringable /** * Greater Than or Equal - * */ public static function greaterThanEqual(string $attribute, mixed $value): string { @@ -89,7 +83,6 @@ class Query implements \JsonSerializable, \Stringable /** * Search - * */ public static function search(string $attribute, string $value): string { @@ -98,7 +91,6 @@ class Query implements \JsonSerializable, \Stringable /** * Is Null - * */ public static function isNull(string $attribute): string { @@ -107,7 +99,6 @@ class Query implements \JsonSerializable, \Stringable /** * Is Not Null - * */ public static function isNotNull(string $attribute): string { @@ -116,7 +107,6 @@ class Query implements \JsonSerializable, \Stringable /** * Between - * */ public static function between(string $attribute, mixed $start, mixed $end): string { @@ -125,7 +115,6 @@ class Query implements \JsonSerializable, \Stringable /** * Starts With - * */ public static function startsWith(string $attribute, string $value): string { @@ -134,7 +123,6 @@ class Query implements \JsonSerializable, \Stringable /** * Ends With - * */ public static function endsWith(string $attribute, string $value): string { @@ -143,7 +131,6 @@ class Query implements \JsonSerializable, \Stringable /** * Select - * */ public static function select(array $attributes): string { @@ -152,7 +139,6 @@ class Query implements \JsonSerializable, \Stringable /** * Cursor After - * */ public static function cursorAfter(string $documentId): string { @@ -161,7 +147,6 @@ class Query implements \JsonSerializable, \Stringable /** * Cursor Before - * */ public static function cursorBefore(string $documentId): string { @@ -170,7 +155,6 @@ class Query implements \JsonSerializable, \Stringable /** * Order Asc - * */ public static function orderAsc(string $attribute): string { @@ -179,7 +163,6 @@ class Query implements \JsonSerializable, \Stringable /** * Order Desc - * */ public static function orderDesc(string $attribute): string { @@ -188,7 +171,6 @@ class Query implements \JsonSerializable, \Stringable /** * Order Random - * */ public static function orderRandom(): string { @@ -197,7 +179,6 @@ class Query implements \JsonSerializable, \Stringable /** * Limit - * */ public static function limit(int $limit): string { @@ -206,7 +187,6 @@ class Query implements \JsonSerializable, \Stringable /** * Offset - * */ public static function offset(int $offset): string { @@ -254,7 +234,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Contains - * */ public static function notContains(string $attribute, $value): string { @@ -263,7 +242,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Search - * */ public static function notSearch(string $attribute, string $value): string { @@ -272,7 +250,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Between - * */ public static function notBetween(string $attribute, mixed $start, mixed $end): string { @@ -281,7 +258,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Starts With - * */ public static function notStartsWith(string $attribute, string $value): string { @@ -290,7 +266,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Ends With - * */ public static function notEndsWith(string $attribute, string $value): string { @@ -299,7 +274,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter resources where attribute matches a regular expression pattern. - * */ public static function regex(string $attribute, string $pattern): string { @@ -308,7 +282,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter resources where the specified attributes exist. - * */ public static function exists(array $attributes): string { @@ -317,7 +290,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter resources where the specified attributes do not exist. - * */ public static function notExists(array $attributes): string { @@ -326,7 +298,6 @@ class Query implements \JsonSerializable, \Stringable /** * Created Before - * */ public static function createdBefore(string $value): string { @@ -335,7 +306,6 @@ class Query implements \JsonSerializable, \Stringable /** * Created After - * */ public static function createdAfter(string $value): string { @@ -344,7 +314,6 @@ class Query implements \JsonSerializable, \Stringable /** * Created Between - * */ public static function createdBetween(string $start, string $end): string { @@ -353,7 +322,6 @@ class Query implements \JsonSerializable, \Stringable /** * Updated Before - * */ public static function updatedBefore(string $value): string { @@ -362,7 +330,6 @@ class Query implements \JsonSerializable, \Stringable /** * Updated After - * */ public static function updatedAfter(string $value): string { @@ -371,7 +338,6 @@ class Query implements \JsonSerializable, \Stringable /** * Updated Between - * */ public static function updatedBetween(string $start, string $end): string { @@ -380,7 +346,6 @@ class Query implements \JsonSerializable, \Stringable /** * Or - * */ public static function or(array $queries): string { @@ -392,7 +357,6 @@ class Query implements \JsonSerializable, \Stringable /** * And - * */ public static function and(array $queries): string { @@ -404,7 +368,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter array elements where at least one element matches all the specified queries. - * */ public static function elemMatch(string $attribute, array $queries): string { @@ -417,7 +380,6 @@ class Query implements \JsonSerializable, \Stringable /** * Distance Equal - * */ public static function distanceEqual(string $attribute, array $values, int|float $distance, bool $meters = true): string { @@ -426,7 +388,6 @@ class Query implements \JsonSerializable, \Stringable /** * Distance Not Equal - * */ public static function distanceNotEqual(string $attribute, array $values, int|float $distance, bool $meters = true): string { @@ -435,7 +396,6 @@ class Query implements \JsonSerializable, \Stringable /** * Distance Greater Than - * */ public static function distanceGreaterThan(string $attribute, array $values, int|float $distance, bool $meters = true): string { @@ -444,7 +404,6 @@ class Query implements \JsonSerializable, \Stringable /** * Distance Less Than - * */ public static function distanceLessThan(string $attribute, array $values, int|float $distance, bool $meters = true): string { @@ -453,7 +412,6 @@ class Query implements \JsonSerializable, \Stringable /** * Vector Dot - * */ public static function vectorDot(string $attribute, array $vector): string { @@ -462,7 +420,6 @@ class Query implements \JsonSerializable, \Stringable /** * Vector Cosine - * */ public static function vectorCosine(string $attribute, array $vector): string { @@ -471,7 +428,6 @@ class Query implements \JsonSerializable, \Stringable /** * Vector Euclidean - * */ public static function vectorEuclidean(string $attribute, array $vector): string { @@ -480,7 +436,6 @@ class Query implements \JsonSerializable, \Stringable /** * Intersects - * */ public static function intersects(string $attribute, array $values): string { @@ -489,7 +444,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Intersects - * */ public static function notIntersects(string $attribute, array $values): string { @@ -498,7 +452,6 @@ class Query implements \JsonSerializable, \Stringable /** * Crosses - * */ public static function crosses(string $attribute, array $values): string { @@ -507,7 +460,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Crosses - * */ public static function notCrosses(string $attribute, array $values): string { @@ -516,7 +468,6 @@ class Query implements \JsonSerializable, \Stringable /** * Overlaps - * */ public static function overlaps(string $attribute, array $values): string { @@ -525,7 +476,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Overlaps - * */ public static function notOverlaps(string $attribute, array $values): string { @@ -534,7 +484,6 @@ class Query implements \JsonSerializable, \Stringable /** * Touches - * */ public static function touches(string $attribute, array $values): string { @@ -543,7 +492,6 @@ class Query implements \JsonSerializable, \Stringable /** * Not Touches - * */ public static function notTouches(string $attribute, array $values): string { @@ -552,10 +500,6 @@ class Query implements \JsonSerializable, \Stringable /** * Count rows of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function count(string $attribute = '*', string $alias = ''): string { @@ -564,10 +508,6 @@ class Query implements \JsonSerializable, \Stringable /** * Count distinct values of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function countDistinct(string $attribute, string $alias = ''): string { @@ -576,10 +516,6 @@ class Query implements \JsonSerializable, \Stringable /** * Sum values of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function sum(string $attribute, string $alias = ''): string { @@ -588,10 +524,6 @@ class Query implements \JsonSerializable, \Stringable /** * Average values of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function avg(string $attribute, string $alias = ''): string { @@ -600,10 +532,6 @@ class Query implements \JsonSerializable, \Stringable /** * Minimum value of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function min(string $attribute, string $alias = ''): string { @@ -612,10 +540,6 @@ class Query implements \JsonSerializable, \Stringable /** * Maximum value of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function max(string $attribute, string $alias = ''): string { @@ -624,10 +548,6 @@ class Query implements \JsonSerializable, \Stringable /** * Standard deviation of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function stddev(string $attribute, string $alias = ''): string { @@ -636,10 +556,6 @@ class Query implements \JsonSerializable, \Stringable /** * Population standard deviation of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function stddevPop(string $attribute, string $alias = ''): string { @@ -648,10 +564,6 @@ class Query implements \JsonSerializable, \Stringable /** * Sample standard deviation of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function stddevSamp(string $attribute, string $alias = ''): string { @@ -660,10 +572,6 @@ class Query implements \JsonSerializable, \Stringable /** * Variance of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function variance(string $attribute, string $alias = ''): string { @@ -672,10 +580,6 @@ class Query implements \JsonSerializable, \Stringable /** * Population variance of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function varPop(string $attribute, string $alias = ''): string { @@ -684,10 +588,6 @@ class Query implements \JsonSerializable, \Stringable /** * Sample variance of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function varSamp(string $attribute, string $alias = ''): string { @@ -696,10 +596,6 @@ class Query implements \JsonSerializable, \Stringable /** * Bitwise AND of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function bitAnd(string $attribute, string $alias = ''): string { @@ -708,10 +604,6 @@ class Query implements \JsonSerializable, \Stringable /** * Bitwise OR of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function bitOr(string $attribute, string $alias = ''): string { @@ -720,10 +612,6 @@ class Query implements \JsonSerializable, \Stringable /** * Bitwise XOR of $attribute, optionally aliased. - * - * @param string $attribute - * @param string $alias - * @return string */ public static function bitXor(string $attribute, string $alias = ''): string { @@ -732,9 +620,6 @@ class Query implements \JsonSerializable, \Stringable /** * Group results by attributes. - * - * @param array $attributes - * @return string */ public static function groupBy(array $attributes): string { @@ -745,7 +630,6 @@ class Query implements \JsonSerializable, \Stringable * Filter grouped results. $queries are nested like and/or. * * @param array $queries - * @return string */ public static function having(array $queries): string { @@ -762,8 +646,6 @@ class Query implements \JsonSerializable, \Stringable /** * Select distinct rows. - * - * @return string */ public static function distinct(): string { @@ -772,11 +654,6 @@ class Query implements \JsonSerializable, \Stringable /** * Column-to-column join ON condition. - * - * @param string $left - * @param string $right - * @param string $operator - * @return string */ public static function on(string $left, string $right, string $operator = '='): string { @@ -785,13 +662,6 @@ class Query implements \JsonSerializable, \Stringable /** * Inner join. $table is the joined collection/table ID. - * - * @param string $table - * @param string|array $leftOrAliasOrOn - * @param string|array $rightOrOn - * @param string $operator - * @param string $alias - * @return string */ public static function join(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { @@ -800,13 +670,6 @@ class Query implements \JsonSerializable, \Stringable /** * Left join. $table is the joined collection/table ID. - * - * @param string $table - * @param string|array $leftOrAliasOrOn - * @param string|array $rightOrOn - * @param string $operator - * @param string $alias - * @return string */ public static function leftJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { @@ -815,13 +678,6 @@ class Query implements \JsonSerializable, \Stringable /** * Right join. $table is the joined collection/table ID. - * - * @param string $table - * @param string|array $leftOrAliasOrOn - * @param string|array $rightOrOn - * @param string $operator - * @param string $alias - * @return string */ public static function rightJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { @@ -830,13 +686,6 @@ class Query implements \JsonSerializable, \Stringable /** * Full outer join. $table is the joined collection/table ID. - * - * @param string $table - * @param string|array $leftOrAliasOrOn - * @param string|array $rightOrOn - * @param string $operator - * @param string $alias - * @return string */ public static function fullOuterJoin(string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn = '', string $operator = '=', string $alias = ''): string { @@ -845,10 +694,6 @@ class Query implements \JsonSerializable, \Stringable /** * Cross join. $table is the joined collection/table ID. - * - * @param string $table - * @param string $alias - * @return string */ public static function crossJoin(string $table, string $alias = ''): string { @@ -857,10 +702,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter resources where $attribute covers the given geometry. - * - * @param string $attribute - * @param array $values - * @return string */ public static function covers(string $attribute, array $values): string { @@ -869,10 +710,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter resources where $attribute does not cover the given geometry. - * - * @param string $attribute - * @param array $values - * @return string */ public static function notCovers(string $attribute, array $values): string { @@ -881,10 +718,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter resources where $attribute is spatially equal to the given geometry. - * - * @param string $attribute - * @param array $values - * @return string */ public static function spatialEquals(string $attribute, array $values): string { @@ -893,10 +726,6 @@ class Query implements \JsonSerializable, \Stringable /** * Filter resources where $attribute is not spatially equal to the given geometry. - * - * @param string $attribute - * @param array $values - * @return string */ public static function notSpatialEquals(string $attribute, array $values): string { @@ -905,10 +734,6 @@ class Query implements \JsonSerializable, \Stringable /** * Return limit and offset queries for 1-based page pagination. - * - * @param int $page - * @param int $perPage - * @return array */ public static function page(int $page, int $perPage = 25): array { @@ -927,8 +752,6 @@ class Query implements \JsonSerializable, \Stringable /** * Start a fluent query builder. - * - * @return QueryBuilder */ public static function builder(): QueryBuilder { @@ -936,8 +759,6 @@ class Query implements \JsonSerializable, \Stringable } /** - * @param string|array $leftOrAliasOrOn - * @param string|array $rightOrOn */ private static function createJoin(string $method, string $table, string|array $leftOrAliasOrOn, string|array $rightOrOn, string $operator, string $alias): string { @@ -952,7 +773,6 @@ class Query implements \JsonSerializable, \Stringable } /** - * @param array $on */ private static function nestedJoin(string $method, string $table, string $alias, array $on): string { From c0a44d02c5ed547e4dded277d1f1a0a4f0a96f2b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 02:17:17 +1200 Subject: [PATCH 26/42] fix: apply remaining Rector cleanups on PHP Query tests --- templates/php/src/Query.php.twig | 2 +- templates/php/tests/ClientTest.php.twig | 2 ++ templates/php/tests/QueryTest.php.twig | 11 +++-------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index e2c0513767..7039c72c81 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -649,7 +649,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function distinct(): string { - return (new Query('distinct', null, null))->__toString(); + return (new Query('distinct'))->__toString(); } /** diff --git a/templates/php/tests/ClientTest.php.twig b/templates/php/tests/ClientTest.php.twig index f9de00d60f..ad84a427ef 100644 --- a/templates/php/tests/ClientTest.php.twig +++ b/templates/php/tests/ClientTest.php.twig @@ -1,5 +1,7 @@ description = $description; - $this->value = $value; - $this->expectedValues = $expectedValues; } } From f29c5ce9b181cc5a14c32f082f60750ac09ef065 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Sat, 22 Aug 2026 02:30:28 +1200 Subject: [PATCH 27/42] fix: type QueryTest fixtures as array and use short syntax --- templates/php/tests/QueryTest.php.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/php/tests/QueryTest.php.twig b/templates/php/tests/QueryTest.php.twig index ab4f23f0ad..894b8bdf35 100644 --- a/templates/php/tests/QueryTest.php.twig +++ b/templates/php/tests/QueryTest.php.twig @@ -18,19 +18,19 @@ final class QueryTest extends TestCase /** * @var BasicFilterQueryTest[] $tests */ - private $tests; + private array $tests; protected function setUp(): void { parent::setUp(); - $this->tests = array( + $this->tests = [ new BasicFilterQueryTest('with a string', 's', '["s"]'), new BasicFilterQueryTest('with a integer', 1, '[1]'), new BasicFilterQueryTest('with a double', 1.2, '[1.2]'), new BasicFilterQueryTest('with a whole number double', 1.0, '[1]'), new BasicFilterQueryTest('with a bool', false, '[false]'), new BasicFilterQueryTest('with a list', ['a', 'b', 'c'], '["a","b","c"]'), - ); + ]; } public function testBasicFilterEqual(): void From 060826adfeb2ca219aa5e46b837b510cb4d951fb Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 26 Aug 2026 13:30:04 +1200 Subject: [PATCH 28/42] (chore): make the web templates emit Prettier-clean output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit examples/web/.prettierrc is tabWidth 4 with single quotes, and CI runs `npm run format:check` over the generated SDK. This branch restyled query.ts.twig to two-space double-quoted, so every generation failed that check. query.ts.twig carries no Twig tags, so it is the generated file: formatted with the SDK's own config and taken verbatim. index.ts.twig is formatted the same way, with its tags held aside during the pass and restored at column 0 — an indented {% %} emits that indentation into the output. Object keys are formatted with quoteProps preserve. Without it Prettier strips the quotes from a tokenised key, and once the token expands the result is `X-Appwrite-Response-Format: '1.9.6'` — an unquoted hyphenated key that does not parse. Caught by `npm run analyse` going from exit 0 to exit 2. client.ts.twig and realtime.ts.twig are improved but not yet clean: their Twig interpolations change line lengths between template and output, so Prettier's wrapping decisions cannot be settled at the template level alone. Verified unchanged from baseline: `npm run analyse` exit 0, eslint 8 problems, twig lint 592 files / 0 errors. Co-Authored-By: Claude Opus 5 --- templates/web/src/client.ts.twig | 481 ++-- templates/web/src/index.ts.twig | 10 +- templates/web/src/query.ts.twig | 2591 ++++++++++--------- templates/web/src/services/realtime.ts.twig | 242 +- 4 files changed, 1907 insertions(+), 1417 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index b5ba866b20..dc72d7e43f 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -12,12 +12,14 @@ const MAX_INT64 = BigInt('9223372036854775807'); const MIN_INT64 = BigInt('-9223372036854775808'); function isBigNumber(value: any): boolean { - return value !== null - && typeof value === 'object' - && value._isBigNumber === true - && typeof value.isInteger === 'function' - && typeof value.toFixed === 'function' - && typeof value.toNumber === 'function'; + return ( + value !== null && + typeof value === 'object' && + value._isBigNumber === true && + typeof value.isInteger === 'function' && + typeof value.toFixed === 'function' && + typeof value.toNumber === 'function' + ); } function reviver(_key: string, value: any): any { @@ -40,7 +42,7 @@ function reviver(_key: string, value: any): any { export const JSONbig = { parse: (text: string) => JSONbigParser.parse(text, reviver), - stringify: JSONbigSerializer.stringify + stringify: JSONbigSerializer.stringify, }; /** @@ -48,14 +50,14 @@ export const JSONbig = { */ type Payload = { [key: string]: any; -} +}; /** * Headers type representing a key-value pair with string keys and string values. */ type Headers = { [key: string]: string; -} +}; /** * Realtime response structure with different types. @@ -69,8 +71,13 @@ type RealtimeResponse = { /** * Data associated with the response based on the response type. */ - data: RealtimeResponseAuthenticated | RealtimeResponseConnected | RealtimeResponseError | RealtimeResponseEvent | undefined; -} + data: + | RealtimeResponseAuthenticated + | RealtimeResponseConnected + | RealtimeResponseError + | RealtimeResponseEvent + | undefined; +}; /** * Realtime request structure for authentication. @@ -85,13 +92,13 @@ type RealtimeRequest = { * Data required for authentication. */ data: RealtimeRequestAuthenticate | RealtimeRequestSubscribe[]; -} +}; type RealtimeRequestSubscribe = { subscriptionId: string; channels: string[]; queries: string[]; -} +}; /** * Realtime event response structure with generic payload type. @@ -121,7 +128,7 @@ type RealtimeResponseEvent = { * Subscription IDs this event matches (from backend, optional). */ subscriptions?: string[]; -} +}; /** * Realtime response structure for errors. @@ -136,7 +143,7 @@ type RealtimeResponseError = { * Error message describing the encountered error. */ message: string; -} +}; /** * Realtime response structure for a successful connection. @@ -151,7 +158,7 @@ type RealtimeResponseConnected = { * User object representing the connected user (optional). */ user?: object; -} +}; /** * Realtime response structure for authenticated connections. @@ -171,7 +178,7 @@ type RealtimeResponseAuthenticated = { * User object representing the authenticated user. */ user: object; -} +}; /** * Realtime request structure for authentication. @@ -181,7 +188,7 @@ type RealtimeRequestAuthenticate = { * Session identifier for authentication. */ session: string; -} +}; type TimeoutHandle = ReturnType | number; @@ -201,7 +208,7 @@ type Realtime = { /** * Heartbeat interval for the realtime connection. - */ + */ heartbeat?: TimeoutHandle; /** @@ -222,11 +229,14 @@ type Realtime = { /** * Map of subscriptions keyed by client-generated subscriptionId. */ - subscriptions: Map) => void - }>; + subscriptions: Map< + string, + { + channels: string[]; + queries: string[]; + callback: (payload: RealtimeResponseEvent) => void; + } + >; /** * Pending subscribe rows keyed by subscriptionId. Flushed and cleared on each send. @@ -271,7 +281,7 @@ type Realtime = { * @param {MessageEvent} event - Event containing the received message. */ onMessage: (event: MessageEvent) => void; -} +}; /** * Type representing upload progress information. @@ -301,7 +311,7 @@ type UploadProgress = { * Number of chunks that have been successfully uploaded. */ chunksUploaded: number; -} +}; /** * Exception thrown by the {{language.params.packageName}} package @@ -331,7 +341,12 @@ class {{spec.info.title | caseUcfirst}}Exception extends Error { * @param {string} type - The error type. Default is an empty string. * @param {string} response - The response string. Default is an empty string. */ - constructor(message: string, code: number = 0, type: string = '', response: string = '') { + constructor( + message: string, + code: number = 0, + type: string = '', + response: string = '', + ) { super(message); this.name = '{{spec.info.title | caseUcfirst}}Exception'; this.message = message; @@ -379,9 +394,9 @@ class Client { 'x-sdk-platform': '{{ sdk.platform }}', 'x-sdk-language': '{{ language.name | caseLower }}', 'x-sdk-version': '{{ sdk.version }}', - {%~ for key,header in defaultHeaders %} +{%~ for key,header in defaultHeaders %} '{{key}}': '{{header}}', - {%~ endfor %} +{%~ endfor %} }; /** @@ -407,15 +422,24 @@ class Client { */ setEndpoint(endpoint: string): this { if (!endpoint || typeof endpoint !== 'string') { - throw new {{spec.info.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + throw new {{spec.info.title | caseUcfirst}}Exception( + 'Endpoint must be a valid string', + ); } - if (!endpoint.startsWith('http://') && !endpoint.startsWith('https://')) { - throw new {{spec.info.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); + if ( + !endpoint.startsWith('http://') && + !endpoint.startsWith('https://') + ) { + throw new {{spec.info.title | caseUcfirst}}Exception( + 'Invalid endpoint URL: ' + endpoint, + ); } this.config.endpoint = endpoint; - this.config.endpointRealtime = endpoint.replace('https://', 'wss://').replace('http://', 'ws://'); + this.config.endpointRealtime = endpoint + .replace('https://', 'wss://') + .replace('http://', 'ws://'); return this; } @@ -429,11 +453,18 @@ class Client { */ setEndpointRealtime(endpointRealtime: string): this { if (!endpointRealtime || typeof endpointRealtime !== 'string') { - throw new {{spec.info.title | caseUcfirst}}Exception('Endpoint must be a valid string'); + throw new {{spec.info.title | caseUcfirst}}Exception( + 'Endpoint must be a valid string', + ); } - if (!endpointRealtime.startsWith('ws://') && !endpointRealtime.startsWith('wss://')) { - throw new {{spec.info.title | caseUcfirst}}Exception('Invalid realtime endpoint URL: ' + endpointRealtime); + if ( + !endpointRealtime.startsWith('ws://') && + !endpointRealtime.startsWith('wss://') + ) { + throw new {{spec.info.title | caseUcfirst}}Exception( + 'Invalid realtime endpoint URL: ' + endpointRealtime, + ); } this.config.endpointRealtime = endpointRealtime; @@ -472,15 +503,21 @@ class Client { case 'include': return true; case 'same-origin': { - if (typeof window === 'undefined' || typeof url === 'undefined') { + if ( + typeof window === 'undefined' || + typeof url === 'undefined' + ) { return false; } try { - const parsedUrl = typeof url === 'string' ? new URL(url) : url; - const normalizedOrigin = parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'ws:' - ? `${parsedUrl.protocol === 'wss:' ? 'https:' : 'http:'}//${parsedUrl.host}` - : parsedUrl.origin; + const parsedUrl = + typeof url === 'string' ? new URL(url) : url; + const normalizedOrigin = + parsedUrl.protocol === 'wss:' || + parsedUrl.protocol === 'ws:' + ? `${parsedUrl.protocol === 'wss:' ? 'https:' : 'http:'}//${parsedUrl.host}` + : parsedUrl.origin; return normalizedOrigin === window.location.origin; } catch { @@ -494,26 +531,26 @@ class Client { } {%~ endif %} - {%~ for headerKey, header in globalHeaders %} +{%~ for headerKey, header in globalHeaders %} /** * Set {{headerKey | caseUcfirst}} * - {%~ if header.description %} +{%~ if header.description %} * {{header.description}} * - {%~ endif %} +{%~ endif %} * @param value string * * @return {this} */ set{{headerKey | caseUcfirst}}(value: string): this { - {%~ if (headerKey != 'Project') %} +{%~ if (headerKey != 'Project') %} this.headers['{{ header.name }}'] = {% if header.scheme == 'bearer' %}`Bearer ${value}`{% else %}value{% endif %}; - {%~ endif %} +{%~ endif %} this.config.{{ headerKey | caseLower }} = value; return this; } - {%~ endfor %} +{%~ endfor %} private realtime: Realtime = { socket: undefined, @@ -550,9 +587,11 @@ class Client { } this.realtime.heartbeat = window?.setInterval(() => { - this.realtime.socket?.send(JSONbig.stringify({ - type: 'ping' - })); + this.realtime.socket?.send( + JSONbig.stringify({ + type: 'ping', + }), + ); }, 20_000); }, createSocket: () => { @@ -562,15 +601,19 @@ class Client { return; } - const encodedProject = encodeURIComponent((this.config.project as string) ?? ''); + const encodedProject = encodeURIComponent( + (this.config.project as string) ?? '', + ); // URL carries only the project; channels/queries are sent via subscribe message. let queryParams = 'project=' + encodedProject; if (this.config.jwt) { - queryParams += '&jwt=' + encodeURIComponent(this.config.jwt as string); + queryParams += + '&jwt=' + encodeURIComponent(this.config.jwt as string); } - const url = this.config.endpointRealtime + '/realtime?' + queryParams; + const url = + this.config.endpointRealtime + '/realtime?' + queryParams; if ( url !== this.realtime.url || // Check if URL is present @@ -587,37 +630,46 @@ class Client { this.realtime.url = url; this.realtime.socket = new WebSocket(url); - this.realtime.socket.addEventListener('message', this.realtime.onMessage); - this.realtime.socket.addEventListener('open', _event => { + this.realtime.socket.addEventListener( + 'message', + this.realtime.onMessage, + ); + this.realtime.socket.addEventListener('open', (_event) => { this.realtime.reconnectAttempts = 0; this.realtime.createHeartbeat(); }); - this.realtime.socket.addEventListener('close', event => { + this.realtime.socket.addEventListener('close', (event) => { if ( !this.realtime.reconnect || - ( - this.realtime?.lastMessage?.type === 'error' && // Check if last message was of type error - (this.realtime?.lastMessage.data).code === 1008 // Check for policy violation 1008 - ) + (this.realtime?.lastMessage?.type === 'error' && // Check if last message was of type error + (( + this.realtime?.lastMessage.data + )).code === 1008) // Check for policy violation 1008 ) { this.realtime.reconnect = true; return; } const timeout = this.realtime.getTimeout(); - console.error(`Realtime got disconnected. Reconnect will be attempted in ${timeout / 1000} seconds.`, event.reason); + console.error( + `Realtime got disconnected. Reconnect will be attempted in ${timeout / 1000} seconds.`, + event.reason, + ); setTimeout(() => { this.realtime.reconnectAttempts++; this.realtime.createSocket(); }, timeout); - }) + }); } else if (this.realtime.socket?.readyState === WebSocket.OPEN) { this.realtime.sendPendingSubscribes(); } }, sendPendingSubscribes: () => { - if (!this.realtime.socket || this.realtime.socket.readyState !== WebSocket.OPEN) { + if ( + !this.realtime.socket || + this.realtime.socket.readyState !== WebSocket.OPEN + ) { return; } @@ -628,10 +680,12 @@ class Client { const rows = Array.from(this.realtime.pendingSubscribes.values()); this.realtime.pendingSubscribes.clear(); - this.realtime.socket.send(JSONbig.stringify({ - type: 'subscribe', - data: rows - })); + this.realtime.socket.send( + JSONbig.stringify({ + type: 'subscribe', + data: rows, + }), + ); }, onMessage: (event) => { try { @@ -639,47 +693,71 @@ class Client { this.realtime.lastMessage = message; switch (message.type) { case 'connected': { - const messageData = message.data; + const messageData = ( + message.data + ); {%~ if sdk.platform == 'console' %} - if (this.shouldUseFallbackCredentials(this.realtime.url)) { + if ( + this.shouldUseFallbackCredentials(this.realtime.url) + ) { let session = this.config.session; if (!session) { - const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); - session = cookie?.[`a_session_${this.config.project}`]; + const cookie = JSONbig.parse( + window.localStorage.getItem( + 'cookieFallback', + ) ?? '{}', + ); + session = + cookie?.[ + `a_session_${this.config.project}` + ]; } if (session && !messageData?.user) { - this.realtime.socket?.send(JSONbig.stringify({ - type: 'authentication', - data: { - session - } - })); + this.realtime.socket?.send( + JSONbig.stringify({ + type: 'authentication', + data: { + session, + }, + }), + ); } } {%~ else %} let session = this.config.session; if (!session) { - const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); - session = cookie?.[`a_session_${this.config.project}`]; + const cookie = JSONbig.parse( + window.localStorage.getItem('cookieFallback') ?? + '{}', + ); + session = + cookie?.[`a_session_${this.config.project}`]; } if (session && !messageData?.user) { - this.realtime.socket?.send(JSONbig.stringify({ - type: 'authentication', - data: { - session - } - })); + this.realtime.socket?.send( + JSONbig.stringify({ + type: 'authentication', + data: { + session, + }, + }), + ); } {%~ endif %} - this.realtime.subscriptions.forEach((sub, subscriptionId) => { - this.realtime.pendingSubscribes.set(subscriptionId, { - subscriptionId, - channels: sub.channels, - queries: sub.queries ?? [] - }); - }); + this.realtime.subscriptions.forEach( + (sub, subscriptionId) => { + this.realtime.pendingSubscribes.set( + subscriptionId, + { + subscriptionId, + channels: sub.channels, + queries: sub.queries ?? [], + }, + ); + }, + ); this.realtime.sendPendingSubscribes(); break; } @@ -689,25 +767,44 @@ class Client { // the SDK needs to reconcile. break; case 'event': { - const data = >message.data; + const data = >( + message.data + ); if (!data?.channels) break; const eventSubIds = data.subscriptions; if (eventSubIds && eventSubIds.length > 0) { for (const subscriptionId of eventSubIds) { - const subscription = this.realtime.subscriptions.get(subscriptionId); + const subscription = + this.realtime.subscriptions.get( + subscriptionId, + ); if (subscription) { - setTimeout(() => subscription.callback(data)); + setTimeout(() => + subscription.callback(data), + ); } } } else { - const isSubscribed = data.channels.some(channel => this.realtime.channels.has(channel)); + const isSubscribed = data.channels.some((channel) => + this.realtime.channels.has(channel), + ); if (!isSubscribed) break; - this.realtime.subscriptions.forEach(subscription => { - if (data.channels.some(channel => subscription.channels.includes(channel))) { - setTimeout(() => subscription.callback(data)); - } - }); + this.realtime.subscriptions.forEach( + (subscription) => { + if ( + data.channels.some((channel) => + subscription.channels.includes( + channel, + ), + ) + ) { + setTimeout(() => + subscription.callback(data), + ); + } + }, + ); } break; } @@ -721,8 +818,8 @@ class Client { } catch (e) { console.error(e); } - } - } + }, + }; /** * Subscribes to {{spec.info.title | caseUcfirst}} events and passes you the payload in realtime. @@ -749,7 +846,7 @@ class Client { * - teams.[ID] * - memberships * - memberships.[ID] - * + * * You can also use Channel builders: * - Channel.database('db').collection('col').document('doc').create() * - Channel.bucket('bucket').file('file').update() @@ -760,13 +857,19 @@ class Client { * @returns {() => void} Unsubscribes from events. */ subscribe( - channels: string | string[] | Channel | ActionableChannel | ResolvedChannel | (Channel | ActionableChannel | ResolvedChannel)[], + channels: + | string + | string[] + | Channel + | ActionableChannel + | ResolvedChannel + | (Channel | ActionableChannel | ResolvedChannel)[], callback: (payload: RealtimeResponseEvent) => void, - queries: QueryInput = [] + queries: QueryInput = [], ): () => void { const channelArray = Array.isArray(channels) ? channels : [channels]; // Convert Channel instances to strings - const channelStrings = channelArray.map(ch => { + const channelStrings = channelArray.map((ch) => { if (typeof ch === 'string') { return ch; } @@ -777,7 +880,9 @@ class Client { // Fallback to generic string conversion return String(ch); }); - channelStrings.forEach(channel => this.realtime.channels.add(channel)); + channelStrings.forEach((channel) => + this.realtime.channels.add(channel), + ); const queryStrings = normalizeQueries(queries); @@ -791,17 +896,19 @@ class Client { } } if (subscriptionId === '') { - throw new {{spec.info.title | caseUcfirst}}Exception('Failed to generate unique subscription id'); + throw new {{spec.info.title | caseUcfirst}}Exception( + 'Failed to generate unique subscription id', + ); } this.realtime.subscriptions.set(subscriptionId, { channels: channelStrings, queries: queryStrings, - callback + callback, }); this.realtime.pendingSubscribes.set(subscriptionId, { subscriptionId, channels: channelStrings, - queries: queryStrings + queries: queryStrings, }); this.realtime.connect(); @@ -810,19 +917,24 @@ class Client { this.realtime.subscriptions.delete(subscriptionId); this.realtime.pendingSubscribes.delete(subscriptionId); const stillUsed = new Set(); - this.realtime.subscriptions.forEach(sub => { - sub.channels.forEach(channel => stillUsed.add(channel)); + this.realtime.subscriptions.forEach((sub) => { + sub.channels.forEach((channel) => stillUsed.add(channel)); }); - this.realtime.channels.forEach(channel => { + this.realtime.channels.forEach((channel) => { if (!stillUsed.has(channel)) { this.realtime.channels.delete(channel); } }); this.realtime.connect(); - } + }; } - prepareRequest(method: string, url: URL, headers: Headers = {}, params: Payload = {}): { uri: string, options: RequestInit } { + prepareRequest( + method: string, + url: URL, + headers: Headers = {}, + params: Payload = {}, + ): { uri: string; options: RequestInit } { method = method.toUpperCase(); for (const key of Object.keys(params)) { @@ -832,7 +944,8 @@ class Client { headers = Object.assign({}, this.headers, headers); if (typeof window !== 'undefined' && window.localStorage{% if sdk.platform == 'console' %} && this.shouldUseFallbackCredentials(url){% endif %}) { - const cookieFallback = window.localStorage.getItem('cookieFallback'); + const cookieFallback = + window.localStorage.getItem('cookieFallback'); if (cookieFallback) { headers['X-Fallback-Cookies'] = cookieFallback; } @@ -885,8 +998,17 @@ class Client { return { uri: url.toString(), options }; } - async chunkedUpload(method: string, url: URL, headers: Headers = {}, originalPayload: Payload = {}, onProgress: (progress: UploadProgress) => void) { - const [fileParam, file] = Object.entries(originalPayload).find(([_, value]) => value instanceof File) ?? []; + async chunkedUpload( + method: string, + url: URL, + headers: Headers = {}, + originalPayload: Payload = {}, + onProgress: (progress: UploadProgress) => void, + ) { + const [fileParam, file] = + Object.entries(originalPayload).find( + ([_, value]) => value instanceof File, + ) ?? []; if (!file || !fileParam) { throw new Error('File not found in payload'); @@ -900,12 +1022,20 @@ class Client { // Upload first chunk alone to get the upload ID const firstChunkEnd = Math.min(Client.CHUNK_SIZE, file.size); - const firstChunkHeaders = { ...headers, 'content-range': `bytes 0-${firstChunkEnd - 1}/${file.size}` }; + const firstChunkHeaders = { + ...headers, + 'content-range': `bytes 0-${firstChunkEnd - 1}/${file.size}`, + }; const firstChunk = file.slice(0, firstChunkEnd); const firstPayload = { ...originalPayload }; firstPayload[fileParam] = new File([firstChunk], file.name); - let response = await this.call(method, url, firstChunkHeaders, firstPayload); + let response = await this.call( + method, + url, + firstChunkHeaders, + firstPayload, + ); const uploadId = response?.$id; if (onProgress && typeof onProgress === 'function') { @@ -914,7 +1044,7 @@ class Client { progress: Math.round((firstChunkEnd / file.size) * 100), sizeUploaded: firstChunkEnd, chunksTotal: totalChunks, - chunksUploaded: 1 + chunksUploaded: 1, }); } @@ -941,29 +1071,39 @@ class Client { const isUploadComplete = (chunkResponse: any) => { const chunksUploaded = chunkResponse?.chunksUploaded; const chunksTotal = chunkResponse?.chunksTotal ?? totalChunks; - return typeof chunksUploaded === 'number' && typeof chunksTotal === 'number' && chunksUploaded >= chunksTotal; + return ( + typeof chunksUploaded === 'number' && + typeof chunksTotal === 'number' && + chunksUploaded >= chunksTotal + ); }; - const uploadChunk = async (chunk: typeof chunks[0]) => { + const uploadChunk = async (chunk: (typeof chunks)[0]) => { const chunkHeaders = { ...headers }; if (uploadId) { chunkHeaders['x-{{spec.info.title | caseLower }}-id'] = uploadId; } - chunkHeaders['content-range'] = `bytes ${chunk.start}-${chunk.end - 1}/${file.size}`; - + chunkHeaders['content-range'] = + `bytes ${chunk.start}-${chunk.end - 1}/${file.size}`; + const chunkBlob = file.slice(chunk.start, chunk.end); const chunkPayload = { ...originalPayload }; chunkPayload[fileParam] = new File([chunkBlob], file.name); - const chunkResponse = await this.call(method, url, chunkHeaders, chunkPayload); + const chunkResponse = await this.call( + method, + url, + chunkHeaders, + chunkPayload, + ); if (rejected) { return chunkResponse; } - + completedCount++; - uploadedBytes += (chunk.end - chunk.start); - + uploadedBytes += chunk.end - chunk.start; + lastResponse = chunkResponse; if (isUploadComplete(chunkResponse)) { finalResponse = chunkResponse; @@ -975,7 +1115,7 @@ class Client { progress: Math.round((uploadedBytes / file.size) * 100), sizeUploaded: uploadedBytes, chunksTotal: totalChunks, - chunksUploaded: completedCount + chunksUploaded: completedCount, }); } @@ -1020,23 +1160,38 @@ class Client { return finalResponse ?? lastResponse; } - {%~ for clientMethodName, clientMethods in clientMethods %} - {%~ for clientMethod in clientMethods %} +{%~ for clientMethodName, clientMethods in clientMethods %} +{%~ for clientMethod in clientMethods %} async {{ clientMethodName | caseCamel }}(): Promise { - return this.call('{{ clientMethod.method.value | caseUpper }}', new URL(this.config.endpoint + '{{ clientMethod.path }}'), { - {%~ for key, security in (clientMethod | securityHeaders) %} - '{{ security.name }}': this.config.{{ key | caseLower }}, - {%~ endfor %} - {%~ for key, header in (clientMethod | methodHeaders) %} - '{{ key }}': '{{ header }}', - {%~ endfor %} - }); + return this.call( + '{{ clientMethod.method.value | caseUpper }}', + new URL(this.config.endpoint + '{{ clientMethod.path }}'), + { +{%~ for key, security in (clientMethod | securityHeaders) %} + '{{ security.name }}': this.config.{{ key | caseLower }}, +{%~ endfor %} +{%~ for key, header in (clientMethod | methodHeaders) %} + '{{ key }}': '{{ header }}', +{%~ endfor %} + }, + ); } - {%~ endfor %} - {%~ endfor %} +{%~ endfor %} +{%~ endfor %} - async call(method: string, url: URL, headers: Headers = {}, params: Payload = {}, responseType = 'json'): Promise { - const { uri, options } = this.prepareRequest(method, url, headers, params); + async call( + method: string, + url: URL, + headers: Headers = {}, + params: Payload = {}, + responseType = 'json', + ): Promise { + const { uri, options } = this.prepareRequest( + method, + url, + headers, + params, + ); let data: any = null; @@ -1047,40 +1202,58 @@ class Client { throw new {{spec.info.title | caseUcfirst}}Exception( `Invalid Origin. Register your new client (${window.location.host}) as a new Web platform on your project console dashboard`, 403, - "forbidden", - "" + 'forbidden', + '', ); } const warnings = response.headers.get('x-{{ spec.info.title | lower }}-warning'); if (warnings) { - warnings.split(';').forEach((warning: string) => console.warn('Warning: ' + warning)); + warnings + .split(';') + .forEach((warning: string) => + console.warn('Warning: ' + warning), + ); } - if (response.headers.get('content-type')?.includes('application/json')) { + if ( + response.headers.get('content-type')?.includes('application/json') + ) { data = JSONbig.parse(await response.text()); } else if (responseType === 'arrayBuffer') { data = await response.arrayBuffer(); } else { data = { - message: await response.text() + message: await response.text(), }; } if (400 <= response.status) { let responseText = ''; - if (response.headers.get('content-type')?.includes('application/json') || responseType === 'arrayBuffer') { + if ( + response.headers + .get('content-type') + ?.includes('application/json') || + responseType === 'arrayBuffer' + ) { responseText = JSONbig.stringify(data); } else { responseText = data?.message; } - throw new {{spec.info.title | caseUcfirst}}Exception(data?.message ?? responseText, response.status, data?.type, responseText); + throw new {{spec.info.title | caseUcfirst}}Exception( + data?.message ?? responseText, + response.status, + data?.type, + responseText, + ); } const cookieFallback = response.headers.get('X-Fallback-Cookies'); if (typeof window !== 'undefined' && window.localStorage && cookieFallback{% if sdk.platform == 'console' %} && this.shouldUseFallbackCredentials(url){% endif %}) { - window.console.warn('{{spec.info.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.'); + window.console.warn( + '{{spec.info.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.', + ); window.localStorage.setItem('cookieFallback', cookieFallback); } @@ -1100,7 +1273,7 @@ class Client { let output: Payload = {}; for (const [key, value] of Object.entries(data)) { - let finalKey = prefix ? prefix + '[' + key +']' : key; + let finalKey = prefix ? prefix + '[' + key + ']' : key; if (Array.isArray(value)) { output = { ...output, ...Client.flatten(value, finalKey) }; } else { diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index 27301894ab..d29099f2b4 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -5,7 +5,13 @@ * For older versions, please check * [previous releases](https://github.com/{{sdk.gitUserName}}/{{sdk.gitRepoName}}/releases). */ -export { Client, Query, QueryBuilder, flattenParam, {{spec.info.title | caseUcfirst}}Exception } from './client'; +export { + Client, + Query, + QueryBuilder, + flattenParam, + {{spec.info.title | caseUcfirst}}Exception, +} from './client'; {% for service in services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} @@ -25,4 +31,4 @@ export { Channel } from './channel'; export { Operator, Condition } from './operator'; {% for enum in allEnums %} export { {{ enum.title | caseUcfirst }} } from './enums/{{enum.title | caseKebab}}'; -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index cccc1dee33..8fdb9ec9da 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -2,7 +2,8 @@ import JSONbigModule from 'json-bigint'; const JSONbig = JSONbigModule({ useNativeBigInt: true }); type QueryTypesSingle = string | number | bigint | boolean; -export type QueryTypesList = string[] | number[] | bigint[] | boolean[] | Query[] | any[]; +export type QueryTypesList = + string[] | number[] | bigint[] | boolean[] | Query[] | any[]; export type QueryTypes = QueryTypesSingle | QueryTypesList; type AttributesTypes = string | string[]; @@ -10,1215 +11,1439 @@ type AttributesTypes = string | string[]; * Helper class to generate query strings. */ export class Query { - method: string; - attribute: AttributesTypes | undefined; - values: QueryTypesList | undefined; - - /** - * Constructor for Query class. - * - * @param {string} method - * @param {AttributesTypes} attribute - * @param {QueryTypes} values - */ - constructor( - method: string, - attribute?: AttributesTypes, - values?: QueryTypes - ) { - this.method = method; - this.attribute = attribute; - - if (values !== undefined) { - if (Array.isArray(values)) { - this.values = values; - } else { - this.values = [values] as QueryTypesList; - } + method: string; + attribute: AttributesTypes | undefined; + values: QueryTypesList | undefined; + + /** + * Constructor for Query class. + * + * @param {string} method + * @param {AttributesTypes} attribute + * @param {QueryTypes} values + */ + constructor( + method: string, + attribute?: AttributesTypes, + values?: QueryTypes, + ) { + this.method = method; + this.attribute = attribute; + + if (values !== undefined) { + if (Array.isArray(values)) { + this.values = values; + } else { + this.values = [values] as QueryTypesList; + } + } } - } - - /** - * Convert the query object to a JSON string. - * - * @returns {string} - */ - toString(): string { - return JSONbig.stringify({ - method: this.method, - attribute: this.attribute, - values: this.values, - }); - } - - /** - * Filter resources where attribute is equal to value. - * - * @param {string} attribute - * @param {QueryTypes} value - * @returns {string} - */ - static equal = (attribute: string, value: QueryTypes): string => - new Query("equal", attribute, value).toString(); - - /** - * Filter resources where attribute is not equal to value. - * - * @param {string} attribute - * @param {QueryTypes} value - * @returns {string} - */ - static notEqual = (attribute: string, value: QueryTypes): string => - new Query("notEqual", attribute, value).toString(); - - /** - * Filter resources where attribute matches a regular expression pattern. - * - * @param {string} attribute The attribute to filter on. - * @param {string} pattern The regular expression pattern to match. - * @returns {string} - */ - static regex = (attribute: string, pattern: string): string => - new Query("regex", attribute, pattern).toString(); - - /** - * Filter resources where attribute is less than value. - * - * @param {string} attribute - * @param {QueryTypes} value - * @returns {string} - */ - static lessThan = (attribute: string, value: QueryTypes): string => - new Query("lessThan", attribute, value).toString(); - - /** - * Filter resources where attribute is less than or equal to value. - * - * @param {string} attribute - * @param {QueryTypes} value - * @returns {string} - */ - static lessThanEqual = (attribute: string, value: QueryTypes): string => - new Query("lessThanEqual", attribute, value).toString(); - - /** - * Filter resources where attribute is greater than value. - * - * @param {string} attribute - * @param {QueryTypes} value - * @returns {string} - */ - static greaterThan = (attribute: string, value: QueryTypes): string => - new Query("greaterThan", attribute, value).toString(); - - /** - * Filter resources where attribute is greater than or equal to value. - * - * @param {string} attribute - * @param {QueryTypes} value - * @returns {string} - */ - static greaterThanEqual = (attribute: string, value: QueryTypes): string => - new Query("greaterThanEqual", attribute, value).toString(); - - /** - * Filter resources where attribute is null. - * - * @param {string} attribute - * @returns {string} - */ - static isNull = (attribute: string): string => - new Query("isNull", attribute).toString(); - - /** - * Filter resources where attribute is not null. - * - * @param {string} attribute - * @returns {string} - */ - static isNotNull = (attribute: string): string => - new Query("isNotNull", attribute).toString(); - - /** - * Filter resources where the specified attributes exist. - * - * @param {string[]} attributes The list of attributes that must exist. - * @returns {string} - */ - static exists = (attributes: string[]): string => - new Query("exists", undefined, attributes).toString(); - - /** - * Filter resources where the specified attributes do not exist. - * - * @param {string[]} attributes The list of attributes that must not exist. - * @returns {string} - */ - static notExists = (attributes: string[]): string => - new Query("notExists", undefined, attributes).toString(); - - /** - * Filter resources where attribute is between start and end (inclusive). - * - * @param {string} attribute - * @param {string | number | bigint} start - * @param {string | number | bigint} end - * @returns {string} - */ - static between = (attribute: string, start: string | number | bigint, end: string | number | bigint): string => - new Query("between", attribute, [start, end] as QueryTypesList).toString(); - - /** - * Filter resources where attribute starts with value. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static startsWith = (attribute: string, value: string): string => - new Query("startsWith", attribute, value).toString(); - - /** - * Filter resources where attribute ends with value. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static endsWith = (attribute: string, value: string): string => - new Query("endsWith", attribute, value).toString(); - - /** - * Specify which attributes should be returned by the API call. - * - * @param {string[]} attributes - * @returns {string} - */ - static select = (attributes: string[]): string => - new Query("select", undefined, attributes).toString(); - - /** - * Filter resources by searching attribute for value. - * A fulltext index on attribute is required for this query to work. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static search = (attribute: string, value: string): string => - new Query("search", attribute, value).toString(); - - /** - * Sort results by attribute descending. - * - * @param {string} attribute - * @returns {string} - */ - static orderDesc = (attribute: string): string => - new Query("orderDesc", attribute).toString(); - - /** - * Sort results by attribute ascending. - * - * @param {string} attribute - * @returns {string} - */ - static orderAsc = (attribute: string): string => - new Query("orderAsc", attribute).toString(); - - /** - * Sort results randomly. - * - * @returns {string} - */ - static orderRandom = (): string => - new Query("orderRandom").toString(); - - /** - * Return results after documentId. - * - * @param {string} documentId - * @returns {string} - */ - static cursorAfter = (documentId: string): string => - new Query("cursorAfter", undefined, documentId).toString(); - - /** - * Return results before documentId. - * - * @param {string} documentId - * @returns {string} - */ - static cursorBefore = (documentId: string): string => - new Query("cursorBefore", undefined, documentId).toString(); - - /** - * Return only limit results. - * - * @param {number} limit - * @returns {string} - */ - static limit = (limit: number): string => - new Query("limit", undefined, limit).toString(); - - /** - * Filter resources by skipping the first offset results. - * - * @param {number} offset - * @returns {string} - */ - static offset = (offset: number): string => - new Query("offset", undefined, offset).toString(); - - /** - * Filter resources where attribute contains the specified value. - * For string attributes, checks if the string contains the substring. - * - * Note: For array attributes, use {@link containsAny} or {@link containsAll} instead. - * @param {string} attribute - * @param {string | string[]} value - * @returns {string} - */ - static contains = (attribute: string, value: string | any[]): string => - new Query("contains", attribute, value).toString(); - - /** - * Filter resources where attribute contains ANY of the specified values. - * For array and relationship attributes, matches documents where the attribute - * contains at least one of the given values. - * - * @param {string} attribute - * @param {any[]} value - * @returns {string} - */ - static containsAny = (attribute: string, value: any[]): string => - new Query("containsAny", attribute, value).toString(); - - /** - * Filter resources where attribute contains ALL of the specified values. - * For array and relationship attributes, matches documents where the attribute - * contains every one of the given values. - * - * @param {string} attribute - * @param {any[]} value - * @returns {string} - */ - static containsAll = (attribute: string, value: any[]): string => - new Query("containsAll", attribute, value).toString(); - - /** - * Filter resources where attribute does not contain the specified value. - * - * @param {string} attribute - * @param {string | any[]} value - * @returns {string} - */ - static notContains = (attribute: string, value: string | any[]): string => - new Query("notContains", attribute, value).toString(); - - /** - * Filter resources by searching attribute for value (inverse of search). - * A fulltext index on attribute is required for this query to work. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static notSearch = (attribute: string, value: string): string => - new Query("notSearch", attribute, value).toString(); - - /** - * Filter resources where attribute is not between start and end (exclusive). - * - * @param {string} attribute - * @param {string | number | bigint} start - * @param {string | number | bigint} end - * @returns {string} - */ - static notBetween = (attribute: string, start: string | number | bigint, end: string | number | bigint): string => - new Query("notBetween", attribute, [start, end] as QueryTypesList).toString(); - - /** - * Filter resources where attribute does not start with value. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static notStartsWith = (attribute: string, value: string): string => - new Query("notStartsWith", attribute, value).toString(); - - /** - * Filter resources where attribute does not end with value. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static notEndsWith = (attribute: string, value: string): string => - new Query("notEndsWith", attribute, value).toString(); - - /** - * Filter resources where document was created before date. - * - * @param {string} value - * @returns {string} - */ - static createdBefore = (value: string): string => - Query.lessThan("$createdAt", value); - - /** - * Filter resources where document was created after date. - * - * @param {string} value - * @returns {string} - */ - static createdAfter = (value: string): string => - Query.greaterThan("$createdAt", value); - - /** - * Filter resources where document was created between dates. - * - * @param {string} start - * @param {string} end - * @returns {string} - */ - static createdBetween = (start: string, end: string): string => - Query.between("$createdAt", start, end); - - /** - * Filter resources where document was updated before date. - * - * @param {string} value - * @returns {string} - */ - static updatedBefore = (value: string): string => - Query.lessThan("$updatedAt", value); - - /** - * Filter resources where document was updated after date. - * - * @param {string} value - * @returns {string} - */ - static updatedAfter = (value: string): string => - Query.greaterThan("$updatedAt", value); - - /** - * Filter resources where document was updated between dates. - * - * @param {string} start - * @param {string} end - * @returns {string} - */ - static updatedBetween = (start: string, end: string): string => - Query.between("$updatedAt", start, end); - - /** - * Combine multiple queries using logical OR operator. - * - * @param {string[]} queries - * @returns {string} - */ - static or = (queries: string[]) => - new Query("or", undefined, queries.map((query) => JSONbig.parse(query))).toString(); - - /** - * Combine multiple queries using logical AND operator. - * - * @param {string[]} queries - * @returns {string} - */ - static and = (queries: string[]) => - new Query("and", undefined, queries.map((query) => JSONbig.parse(query))).toString(); - - /** - * Filter array elements where at least one element matches all the specified queries. - * - * @param {string} attribute The attribute containing the array to filter on. - * @param {string[]} queries The list of query strings to match against array elements. - * @returns {string} - */ - static elemMatch = (attribute: string, queries: string[]): string => - new Query( - "elemMatch", - attribute, - queries.map((query) => JSONbig.parse(query)) - ).toString(); - - /** - * Filter resources where attribute is at a specific distance from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceEqual = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceEqual", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources where attribute is not at a specific distance from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceNotEqual = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceNotEqual", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources where attribute is at a distance greater than the specified value from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceGreaterThan = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceGreaterThan", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources where attribute is at a distance less than the specified value from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceLessThan = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceLessThan", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources using vector dot product similarity. - * - * @param {string} attribute - * @param {number[]} vector - * @returns {string} - */ - static vectorDot = (attribute: string, vector: number[]): string => - new Query("vectorDot", attribute, [vector] as QueryTypesList).toString(); - - /** - * Filter resources using vector cosine similarity. - * - * @param {string} attribute - * @param {number[]} vector - * @returns {string} - */ - static vectorCosine = (attribute: string, vector: number[]): string => - new Query("vectorCosine", attribute, [vector] as QueryTypesList).toString(); - - /** - * Filter resources using vector Euclidean distance. - * - * @param {string} attribute - * @param {number[]} vector - * @returns {string} - */ - static vectorEuclidean = (attribute: string, vector: number[]): string => - new Query("vectorEuclidean", attribute, [vector] as QueryTypesList).toString(); - - /** - * Filter resources where attribute intersects with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static intersects = (attribute: string, values: any[]): string => - new Query("intersects", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not intersect with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notIntersects = (attribute: string, values: any[]): string => - new Query("notIntersects", attribute, [values]).toString(); - - /** - * Filter resources where attribute crosses the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static crosses = (attribute: string, values: any[]): string => - new Query("crosses", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not cross the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notCrosses = (attribute: string, values: any[]): string => - new Query("notCrosses", attribute, [values]).toString(); - - /** - * Filter resources where attribute overlaps with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static overlaps = (attribute: string, values: any[]): string => - new Query("overlaps", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not overlap with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notOverlaps = (attribute: string, values: any[]): string => - new Query("notOverlaps", attribute, [values]).toString(); - - /** - * Filter resources where attribute touches the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static touches = (attribute: string, values: any[]): string => - new Query("touches", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not touch the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notTouches = (attribute: string, values: any[]): string => - new Query("notTouches", attribute, [values]).toString(); - - /** - * Count documents, optionally aliased. - */ - static count = (attribute: string = "*", alias: string = ""): string => - new Query("count", attribute, alias !== "" ? alias : undefined).toString(); - - /** - * Count distinct values of attribute, optionally aliased. - */ - static countDistinct = (attribute: string, alias: string = ""): string => - new Query("countDistinct", attribute, alias !== "" ? alias : undefined).toString(); - - /** Sum values of `attribute`, optionally aliased. */ - static sum = (attribute: string, alias: string = ""): string => - new Query("sum", attribute, alias !== "" ? alias : undefined).toString(); - - /** Average values of `attribute`, optionally aliased. */ - static avg = (attribute: string, alias: string = ""): string => - new Query("avg", attribute, alias !== "" ? alias : undefined).toString(); - - /** Minimum value of `attribute`, optionally aliased. */ - static min = (attribute: string, alias: string = ""): string => - new Query("min", attribute, alias !== "" ? alias : undefined).toString(); - - /** Maximum value of `attribute`, optionally aliased. */ - static max = (attribute: string, alias: string = ""): string => - new Query("max", attribute, alias !== "" ? alias : undefined).toString(); - - /** Standard deviation of `attribute`, optionally aliased. */ - static stddev = (attribute: string, alias: string = ""): string => - new Query("stddev", attribute, alias !== "" ? alias : undefined).toString(); - - /** Population standard deviation of `attribute`, optionally aliased. */ - static stddevPop = (attribute: string, alias: string = ""): string => - new Query("stddevPop", attribute, alias !== "" ? alias : undefined).toString(); - - /** Sample standard deviation of `attribute`, optionally aliased. */ - static stddevSamp = (attribute: string, alias: string = ""): string => - new Query("stddevSamp", attribute, alias !== "" ? alias : undefined).toString(); - - /** Variance of `attribute`, optionally aliased. */ - static variance = (attribute: string, alias: string = ""): string => - new Query("variance", attribute, alias !== "" ? alias : undefined).toString(); - - /** Population variance of `attribute`, optionally aliased. */ - static varPop = (attribute: string, alias: string = ""): string => - new Query("varPop", attribute, alias !== "" ? alias : undefined).toString(); - - /** Sample variance of `attribute`, optionally aliased. */ - static varSamp = (attribute: string, alias: string = ""): string => - new Query("varSamp", attribute, alias !== "" ? alias : undefined).toString(); - - /** Bitwise AND of `attribute`, optionally aliased. */ - static bitAnd = (attribute: string, alias: string = ""): string => - new Query("bitAnd", attribute, alias !== "" ? alias : undefined).toString(); - - /** Bitwise OR of `attribute`, optionally aliased. */ - static bitOr = (attribute: string, alias: string = ""): string => - new Query("bitOr", attribute, alias !== "" ? alias : undefined).toString(); - - /** Bitwise XOR of `attribute`, optionally aliased. */ - static bitXor = (attribute: string, alias: string = ""): string => - new Query("bitXor", attribute, alias !== "" ? alias : undefined).toString(); - - /** - * Group results by attributes. - */ - static groupBy = (attributes: string[]): string => - new Query("groupBy", undefined, attributes).toString(); - - /** - * Filter grouped results. `queries` are nested like `and`/`or`. - */ - static having = (queries: string[]): string => - new Query( - "having", - undefined, - queries.map((query) => JSONbig.parse(query)) - ).toString(); - - /** Select distinct rows. */ - static distinct = (): string => - new Query("distinct").toString(); - - /** - * Column-to-column join ON condition. - */ - static on = ( - left: string, - right: string, - operator: string = "=" - ): string => - new Query("on", undefined, [left, operator, right]).toString(); - - /** - * Inner join. `table` is the joined collection/table ID. - */ - static join = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("join", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - /** Left join. `table` is the joined collection/table ID. */ - static leftJoin = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("leftJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - /** Right join. `table` is the joined collection/table ID. */ - static rightJoin = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("rightJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - /** Full outer join. `table` is the joined collection/table ID. */ - static fullOuterJoin = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("fullOuterJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - /** Cross join. `table` is the joined collection/table ID. */ - static crossJoin = (table: string, alias: string = ""): string => - new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); - - /** Filter resources where `attribute` covers the given geometry. */ - static covers = (attribute: string, values: any[]): string => - new Query("covers", attribute, [values]).toString(); - - /** Filter resources where `attribute` does not cover the given geometry. */ - static notCovers = (attribute: string, values: any[]): string => - new Query("notCovers", attribute, [values]).toString(); - - /** Filter resources where `attribute` is spatially equal to the given geometry. */ - static spatialEquals = (attribute: string, values: any[]): string => - new Query("spatialEquals", attribute, [values]).toString(); - - /** Filter resources where `attribute` is not spatially equal to the given geometry. */ - static notSpatialEquals = (attribute: string, values: any[]): string => - new Query("notSpatialEquals", attribute, [values]).toString(); - - /** - * Return limit and offset queries for 1-based page pagination. - */ - static page = (page: number, perPage: number = 25): string[] => { - if (page < 1) { - throw new Error("Page must be >= 1, got " + page); - } - if (perPage < 1) { - throw new Error("Per page must be >= 1, got " + perPage); - } - return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; - }; - - /** Start a fluent query builder. */ - static builder = (): Query.Builder => new Query.Builder(); - - private static createJoin( - method: string, - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string { - if (Array.isArray(leftOrAliasOrOn)) { - return Query.nestedJoin(method, table, "", leftOrAliasOrOn); - } - if (Array.isArray(rightOrOn)) { - return Query.nestedJoin(method, table, leftOrAliasOrOn, rightOrOn); + + /** + * Convert the query object to a JSON string. + * + * @returns {string} + */ + toString(): string { + return JSONbig.stringify({ + method: this.method, + attribute: this.attribute, + values: this.values, + }); } - return new Query( - method, - table, - alias !== "" - ? [leftOrAliasOrOn, operator, rightOrOn, alias] - : [leftOrAliasOrOn, operator, rightOrOn] - ).toString(); - } - - private static nestedJoin( - method: string, - table: string, - alias: string, - on: string[] - ): string { - const values: any[] = []; - if (alias !== "") { - values.push(alias); + + /** + * Filter resources where attribute is equal to value. + * + * @param {string} attribute + * @param {QueryTypes} value + * @returns {string} + */ + static equal = (attribute: string, value: QueryTypes): string => + new Query('equal', attribute, value).toString(); + + /** + * Filter resources where attribute is not equal to value. + * + * @param {string} attribute + * @param {QueryTypes} value + * @returns {string} + */ + static notEqual = (attribute: string, value: QueryTypes): string => + new Query('notEqual', attribute, value).toString(); + + /** + * Filter resources where attribute matches a regular expression pattern. + * + * @param {string} attribute The attribute to filter on. + * @param {string} pattern The regular expression pattern to match. + * @returns {string} + */ + static regex = (attribute: string, pattern: string): string => + new Query('regex', attribute, pattern).toString(); + + /** + * Filter resources where attribute is less than value. + * + * @param {string} attribute + * @param {QueryTypes} value + * @returns {string} + */ + static lessThan = (attribute: string, value: QueryTypes): string => + new Query('lessThan', attribute, value).toString(); + + /** + * Filter resources where attribute is less than or equal to value. + * + * @param {string} attribute + * @param {QueryTypes} value + * @returns {string} + */ + static lessThanEqual = (attribute: string, value: QueryTypes): string => + new Query('lessThanEqual', attribute, value).toString(); + + /** + * Filter resources where attribute is greater than value. + * + * @param {string} attribute + * @param {QueryTypes} value + * @returns {string} + */ + static greaterThan = (attribute: string, value: QueryTypes): string => + new Query('greaterThan', attribute, value).toString(); + + /** + * Filter resources where attribute is greater than or equal to value. + * + * @param {string} attribute + * @param {QueryTypes} value + * @returns {string} + */ + static greaterThanEqual = (attribute: string, value: QueryTypes): string => + new Query('greaterThanEqual', attribute, value).toString(); + + /** + * Filter resources where attribute is null. + * + * @param {string} attribute + * @returns {string} + */ + static isNull = (attribute: string): string => + new Query('isNull', attribute).toString(); + + /** + * Filter resources where attribute is not null. + * + * @param {string} attribute + * @returns {string} + */ + static isNotNull = (attribute: string): string => + new Query('isNotNull', attribute).toString(); + + /** + * Filter resources where the specified attributes exist. + * + * @param {string[]} attributes The list of attributes that must exist. + * @returns {string} + */ + static exists = (attributes: string[]): string => + new Query('exists', undefined, attributes).toString(); + + /** + * Filter resources where the specified attributes do not exist. + * + * @param {string[]} attributes The list of attributes that must not exist. + * @returns {string} + */ + static notExists = (attributes: string[]): string => + new Query('notExists', undefined, attributes).toString(); + + /** + * Filter resources where attribute is between start and end (inclusive). + * + * @param {string} attribute + * @param {string | number | bigint} start + * @param {string | number | bigint} end + * @returns {string} + */ + static between = ( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ): string => + new Query('between', attribute, [ + start, + end, + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute starts with value. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static startsWith = (attribute: string, value: string): string => + new Query('startsWith', attribute, value).toString(); + + /** + * Filter resources where attribute ends with value. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static endsWith = (attribute: string, value: string): string => + new Query('endsWith', attribute, value).toString(); + + /** + * Specify which attributes should be returned by the API call. + * + * @param {string[]} attributes + * @returns {string} + */ + static select = (attributes: string[]): string => + new Query('select', undefined, attributes).toString(); + + /** + * Filter resources by searching attribute for value. + * A fulltext index on attribute is required for this query to work. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static search = (attribute: string, value: string): string => + new Query('search', attribute, value).toString(); + + /** + * Sort results by attribute descending. + * + * @param {string} attribute + * @returns {string} + */ + static orderDesc = (attribute: string): string => + new Query('orderDesc', attribute).toString(); + + /** + * Sort results by attribute ascending. + * + * @param {string} attribute + * @returns {string} + */ + static orderAsc = (attribute: string): string => + new Query('orderAsc', attribute).toString(); + + /** + * Sort results randomly. + * + * @returns {string} + */ + static orderRandom = (): string => new Query('orderRandom').toString(); + + /** + * Return results after documentId. + * + * @param {string} documentId + * @returns {string} + */ + static cursorAfter = (documentId: string): string => + new Query('cursorAfter', undefined, documentId).toString(); + + /** + * Return results before documentId. + * + * @param {string} documentId + * @returns {string} + */ + static cursorBefore = (documentId: string): string => + new Query('cursorBefore', undefined, documentId).toString(); + + /** + * Return only limit results. + * + * @param {number} limit + * @returns {string} + */ + static limit = (limit: number): string => + new Query('limit', undefined, limit).toString(); + + /** + * Filter resources by skipping the first offset results. + * + * @param {number} offset + * @returns {string} + */ + static offset = (offset: number): string => + new Query('offset', undefined, offset).toString(); + + /** + * Filter resources where attribute contains the specified value. + * For string attributes, checks if the string contains the substring. + * + * Note: For array attributes, use {@link containsAny} or {@link containsAll} instead. + * @param {string} attribute + * @param {string | string[]} value + * @returns {string} + */ + static contains = (attribute: string, value: string | any[]): string => + new Query('contains', attribute, value).toString(); + + /** + * Filter resources where attribute contains ANY of the specified values. + * For array and relationship attributes, matches documents where the attribute + * contains at least one of the given values. + * + * @param {string} attribute + * @param {any[]} value + * @returns {string} + */ + static containsAny = (attribute: string, value: any[]): string => + new Query('containsAny', attribute, value).toString(); + + /** + * Filter resources where attribute contains ALL of the specified values. + * For array and relationship attributes, matches documents where the attribute + * contains every one of the given values. + * + * @param {string} attribute + * @param {any[]} value + * @returns {string} + */ + static containsAll = (attribute: string, value: any[]): string => + new Query('containsAll', attribute, value).toString(); + + /** + * Filter resources where attribute does not contain the specified value. + * + * @param {string} attribute + * @param {string | any[]} value + * @returns {string} + */ + static notContains = (attribute: string, value: string | any[]): string => + new Query('notContains', attribute, value).toString(); + + /** + * Filter resources by searching attribute for value (inverse of search). + * A fulltext index on attribute is required for this query to work. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static notSearch = (attribute: string, value: string): string => + new Query('notSearch', attribute, value).toString(); + + /** + * Filter resources where attribute is not between start and end (exclusive). + * + * @param {string} attribute + * @param {string | number | bigint} start + * @param {string | number | bigint} end + * @returns {string} + */ + static notBetween = ( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ): string => + new Query('notBetween', attribute, [ + start, + end, + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute does not start with value. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static notStartsWith = (attribute: string, value: string): string => + new Query('notStartsWith', attribute, value).toString(); + + /** + * Filter resources where attribute does not end with value. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static notEndsWith = (attribute: string, value: string): string => + new Query('notEndsWith', attribute, value).toString(); + + /** + * Filter resources where document was created before date. + * + * @param {string} value + * @returns {string} + */ + static createdBefore = (value: string): string => + Query.lessThan('$createdAt', value); + + /** + * Filter resources where document was created after date. + * + * @param {string} value + * @returns {string} + */ + static createdAfter = (value: string): string => + Query.greaterThan('$createdAt', value); + + /** + * Filter resources where document was created between dates. + * + * @param {string} start + * @param {string} end + * @returns {string} + */ + static createdBetween = (start: string, end: string): string => + Query.between('$createdAt', start, end); + + /** + * Filter resources where document was updated before date. + * + * @param {string} value + * @returns {string} + */ + static updatedBefore = (value: string): string => + Query.lessThan('$updatedAt', value); + + /** + * Filter resources where document was updated after date. + * + * @param {string} value + * @returns {string} + */ + static updatedAfter = (value: string): string => + Query.greaterThan('$updatedAt', value); + + /** + * Filter resources where document was updated between dates. + * + * @param {string} start + * @param {string} end + * @returns {string} + */ + static updatedBetween = (start: string, end: string): string => + Query.between('$updatedAt', start, end); + + /** + * Combine multiple queries using logical OR operator. + * + * @param {string[]} queries + * @returns {string} + */ + static or = (queries: string[]) => + new Query( + 'or', + undefined, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + /** + * Combine multiple queries using logical AND operator. + * + * @param {string[]} queries + * @returns {string} + */ + static and = (queries: string[]) => + new Query( + 'and', + undefined, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + /** + * Filter array elements where at least one element matches all the specified queries. + * + * @param {string} attribute The attribute containing the array to filter on. + * @param {string[]} queries The list of query strings to match against array elements. + * @returns {string} + */ + static elemMatch = (attribute: string, queries: string[]): string => + new Query( + 'elemMatch', + attribute, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + /** + * Filter resources where attribute is at a specific distance from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceEqual = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceEqual', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute is not at a specific distance from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceNotEqual = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceNotEqual', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute is at a distance greater than the specified value from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceGreaterThan = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceGreaterThan', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute is at a distance less than the specified value from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceLessThan = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceLessThan', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources using vector dot product similarity. + * + * @param {string} attribute + * @param {number[]} vector + * @returns {string} + */ + static vectorDot = (attribute: string, vector: number[]): string => + new Query('vectorDot', attribute, [ + vector, + ] as QueryTypesList).toString(); + + /** + * Filter resources using vector cosine similarity. + * + * @param {string} attribute + * @param {number[]} vector + * @returns {string} + */ + static vectorCosine = (attribute: string, vector: number[]): string => + new Query('vectorCosine', attribute, [ + vector, + ] as QueryTypesList).toString(); + + /** + * Filter resources using vector Euclidean distance. + * + * @param {string} attribute + * @param {number[]} vector + * @returns {string} + */ + static vectorEuclidean = (attribute: string, vector: number[]): string => + new Query('vectorEuclidean', attribute, [ + vector, + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute intersects with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static intersects = (attribute: string, values: any[]): string => + new Query('intersects', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not intersect with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notIntersects = (attribute: string, values: any[]): string => + new Query('notIntersects', attribute, [values]).toString(); + + /** + * Filter resources where attribute crosses the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static crosses = (attribute: string, values: any[]): string => + new Query('crosses', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not cross the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notCrosses = (attribute: string, values: any[]): string => + new Query('notCrosses', attribute, [values]).toString(); + + /** + * Filter resources where attribute overlaps with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static overlaps = (attribute: string, values: any[]): string => + new Query('overlaps', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not overlap with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notOverlaps = (attribute: string, values: any[]): string => + new Query('notOverlaps', attribute, [values]).toString(); + + /** + * Filter resources where attribute touches the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static touches = (attribute: string, values: any[]): string => + new Query('touches', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not touch the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notTouches = (attribute: string, values: any[]): string => + new Query('notTouches', attribute, [values]).toString(); + + /** + * Count documents, optionally aliased. + */ + static count = (attribute: string = '*', alias: string = ''): string => + new Query( + 'count', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** + * Count distinct values of attribute, optionally aliased. + */ + static countDistinct = (attribute: string, alias: string = ''): string => + new Query( + 'countDistinct', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Sum values of `attribute`, optionally aliased. */ + static sum = (attribute: string, alias: string = ''): string => + new Query( + 'sum', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Average values of `attribute`, optionally aliased. */ + static avg = (attribute: string, alias: string = ''): string => + new Query( + 'avg', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Minimum value of `attribute`, optionally aliased. */ + static min = (attribute: string, alias: string = ''): string => + new Query( + 'min', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Maximum value of `attribute`, optionally aliased. */ + static max = (attribute: string, alias: string = ''): string => + new Query( + 'max', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Standard deviation of `attribute`, optionally aliased. */ + static stddev = (attribute: string, alias: string = ''): string => + new Query( + 'stddev', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Population standard deviation of `attribute`, optionally aliased. */ + static stddevPop = (attribute: string, alias: string = ''): string => + new Query( + 'stddevPop', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Sample standard deviation of `attribute`, optionally aliased. */ + static stddevSamp = (attribute: string, alias: string = ''): string => + new Query( + 'stddevSamp', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Variance of `attribute`, optionally aliased. */ + static variance = (attribute: string, alias: string = ''): string => + new Query( + 'variance', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Population variance of `attribute`, optionally aliased. */ + static varPop = (attribute: string, alias: string = ''): string => + new Query( + 'varPop', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Sample variance of `attribute`, optionally aliased. */ + static varSamp = (attribute: string, alias: string = ''): string => + new Query( + 'varSamp', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Bitwise AND of `attribute`, optionally aliased. */ + static bitAnd = (attribute: string, alias: string = ''): string => + new Query( + 'bitAnd', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Bitwise OR of `attribute`, optionally aliased. */ + static bitOr = (attribute: string, alias: string = ''): string => + new Query( + 'bitOr', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** Bitwise XOR of `attribute`, optionally aliased. */ + static bitXor = (attribute: string, alias: string = ''): string => + new Query( + 'bitXor', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** + * Group results by attributes. + */ + static groupBy = (attributes: string[]): string => + new Query('groupBy', undefined, attributes).toString(); + + /** + * Filter grouped results. `queries` are nested like `and`/`or`. + */ + static having = (queries: string[]): string => + new Query( + 'having', + undefined, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + /** Select distinct rows. */ + static distinct = (): string => new Query('distinct').toString(); + + /** + * Column-to-column join ON condition. + */ + static on = (left: string, right: string, operator: string = '='): string => + new Query('on', undefined, [left, operator, right]).toString(); + + /** + * Inner join. `table` is the joined collection/table ID. + */ + static join = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'join', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + /** Left join. `table` is the joined collection/table ID. */ + static leftJoin = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'leftJoin', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + /** Right join. `table` is the joined collection/table ID. */ + static rightJoin = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'rightJoin', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + /** Full outer join. `table` is the joined collection/table ID. */ + static fullOuterJoin = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'fullOuterJoin', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + /** Cross join. `table` is the joined collection/table ID. */ + static crossJoin = (table: string, alias: string = ''): string => + new Query( + 'crossJoin', + table, + alias !== '' ? alias : undefined, + ).toString(); + + /** Filter resources where `attribute` covers the given geometry. */ + static covers = (attribute: string, values: any[]): string => + new Query('covers', attribute, [values]).toString(); + + /** Filter resources where `attribute` does not cover the given geometry. */ + static notCovers = (attribute: string, values: any[]): string => + new Query('notCovers', attribute, [values]).toString(); + + /** Filter resources where `attribute` is spatially equal to the given geometry. */ + static spatialEquals = (attribute: string, values: any[]): string => + new Query('spatialEquals', attribute, [values]).toString(); + + /** Filter resources where `attribute` is not spatially equal to the given geometry. */ + static notSpatialEquals = (attribute: string, values: any[]): string => + new Query('notSpatialEquals', attribute, [values]).toString(); + + /** + * Return limit and offset queries for 1-based page pagination. + */ + static page = (page: number, perPage: number = 25): string[] => { + if (page < 1) { + throw new Error('Page must be >= 1, got ' + page); + } + if (perPage < 1) { + throw new Error('Per page must be >= 1, got ' + perPage); + } + return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; + }; + + /** Start a fluent query builder. */ + static builder = (): Query.Builder => new Query.Builder(); + + private static createJoin( + method: string, + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string { + if (Array.isArray(leftOrAliasOrOn)) { + return Query.nestedJoin(method, table, '', leftOrAliasOrOn); + } + if (Array.isArray(rightOrOn)) { + return Query.nestedJoin(method, table, leftOrAliasOrOn, rightOrOn); + } + return new Query( + method, + table, + alias !== '' + ? [leftOrAliasOrOn, operator, rightOrOn, alias] + : [leftOrAliasOrOn, operator, rightOrOn], + ).toString(); } - for (const query of on) { - values.push(JSONbig.parse(query)); + + private static nestedJoin( + method: string, + table: string, + alias: string, + on: string[], + ): string { + const values: any[] = []; + if (alias !== '') { + values.push(alias); + } + for (const query of on) { + values.push(JSONbig.parse(query)); + } + return new Query(method, table, values).toString(); } - return new Query(method, table, values).toString(); - } } -export type QueryInput = - | string - | Query - | Query.Builder - | QueryInput[]; +export type QueryInput = string | Query | Query.Builder | QueryInput[]; /** * Fluent collector of Query fragments. Call {@link Query.Builder.build} to * produce the `string[]` expected by list/createQuery methods. */ export namespace Query { - export class Builder { - private readonly queries: string[] = []; - - build(): string[] { - return [...this.queries]; - } - - equal(attribute: string, value: QueryTypes): this { - return this.push(Query.equal(attribute, value)); - } - - notEqual(attribute: string, value: QueryTypes): this { - return this.push(Query.notEqual(attribute, value)); - } - - lessThan(attribute: string, value: QueryTypes): this { - return this.push(Query.lessThan(attribute, value)); - } - - lessThanEqual(attribute: string, value: QueryTypes): this { - return this.push(Query.lessThanEqual(attribute, value)); - } - - greaterThan(attribute: string, value: QueryTypes): this { - return this.push(Query.greaterThan(attribute, value)); - } - - greaterThanEqual(attribute: string, value: QueryTypes): this { - return this.push(Query.greaterThanEqual(attribute, value)); - } - - search(attribute: string, value: string): this { - return this.push(Query.search(attribute, value)); - } - - isNull(attribute: string): this { - return this.push(Query.isNull(attribute)); - } - - isNotNull(attribute: string): this { - return this.push(Query.isNotNull(attribute)); - } - - between(attribute: string, start: string | number | bigint, end: string | number | bigint): this { - return this.push(Query.between(attribute, start, end)); - } - - startsWith(attribute: string, value: string): this { - return this.push(Query.startsWith(attribute, value)); - } - - endsWith(attribute: string, value: string): this { - return this.push(Query.endsWith(attribute, value)); - } - - select(attributes: string[]): this { - return this.push(Query.select(attributes)); - } - - cursorAfter(documentId: string): this { - return this.push(Query.cursorAfter(documentId)); - } - - cursorBefore(documentId: string): this { - return this.push(Query.cursorBefore(documentId)); - } - - orderAsc(attribute: string): this { - return this.push(Query.orderAsc(attribute)); - } - - orderDesc(attribute: string): this { - return this.push(Query.orderDesc(attribute)); - } - - orderRandom(): this { - return this.push(Query.orderRandom()); - } - - limit(limit: number): this { - return this.push(Query.limit(limit)); - } - - offset(offset: number): this { - return this.push(Query.offset(offset)); - } - - contains(attribute: string, value: string | any[]): this { - return this.push(Query.contains(attribute, value)); - } - - containsAny(attribute: string, value: any[]): this { - return this.push(Query.containsAny(attribute, value)); - } - - containsAll(attribute: string, value: any[]): this { - return this.push(Query.containsAll(attribute, value)); - } - - notContains(attribute: string, value: string | any[]): this { - return this.push(Query.notContains(attribute, value)); - } - - notSearch(attribute: string, value: string): this { - return this.push(Query.notSearch(attribute, value)); - } - - notBetween(attribute: string, start: string | number | bigint, end: string | number | bigint): this { - return this.push(Query.notBetween(attribute, start, end)); - } - - notStartsWith(attribute: string, value: string): this { - return this.push(Query.notStartsWith(attribute, value)); - } - - notEndsWith(attribute: string, value: string): this { - return this.push(Query.notEndsWith(attribute, value)); - } - - regex(attribute: string, pattern: string): this { - return this.push(Query.regex(attribute, pattern)); - } - - exists(attributes: string[]): this { - return this.push(Query.exists(attributes)); - } - - notExists(attributes: string[]): this { - return this.push(Query.notExists(attributes)); - } - - createdBefore(value: string): this { - return this.push(Query.createdBefore(value)); - } - - createdAfter(value: string): this { - return this.push(Query.createdAfter(value)); - } - - createdBetween(start: string, end: string): this { - return this.push(Query.createdBetween(start, end)); - } - - updatedBefore(value: string): this { - return this.push(Query.updatedBefore(value)); - } - - updatedAfter(value: string): this { - return this.push(Query.updatedAfter(value)); - } - - updatedBetween(start: string, end: string): this { - return this.push(Query.updatedBetween(start, end)); - } - - or(queries: string[]): this { - return this.push(Query.or(queries)); - } - - and(queries: string[]): this { - return this.push(Query.and(queries)); - } - - elemMatch(attribute: string, queries: string[]): this { - return this.push(Query.elemMatch(attribute, queries)); - } + export class Builder { + private readonly queries: string[] = []; - distanceEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceEqual(attribute, values, distance, meters)); - } + build(): string[] { + return [...this.queries]; + } - distanceNotEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceNotEqual(attribute, values, distance, meters)); - } + equal(attribute: string, value: QueryTypes): this { + return this.push(Query.equal(attribute, value)); + } - distanceGreaterThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceGreaterThan(attribute, values, distance, meters)); - } - - distanceLessThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceLessThan(attribute, values, distance, meters)); - } - - intersects(attribute: string, values: any[]): this { - return this.push(Query.intersects(attribute, values)); - } - - notIntersects(attribute: string, values: any[]): this { - return this.push(Query.notIntersects(attribute, values)); - } - - crosses(attribute: string, values: any[]): this { - return this.push(Query.crosses(attribute, values)); - } - - notCrosses(attribute: string, values: any[]): this { - return this.push(Query.notCrosses(attribute, values)); - } - - overlaps(attribute: string, values: any[]): this { - return this.push(Query.overlaps(attribute, values)); - } - - notOverlaps(attribute: string, values: any[]): this { - return this.push(Query.notOverlaps(attribute, values)); - } - - touches(attribute: string, values: any[]): this { - return this.push(Query.touches(attribute, values)); - } - - notTouches(attribute: string, values: any[]): this { - return this.push(Query.notTouches(attribute, values)); - } - - count(attribute: string = "*", alias: string = ""): this { - return this.push(Query.count(attribute, alias)); - } - - countDistinct(attribute: string, alias: string = ""): this { - return this.push(Query.countDistinct(attribute, alias)); - } - - sum(attribute: string, alias: string = ""): this { - return this.push(Query.sum(attribute, alias)); - } - - avg(attribute: string, alias: string = ""): this { - return this.push(Query.avg(attribute, alias)); - } - - min(attribute: string, alias: string = ""): this { - return this.push(Query.min(attribute, alias)); - } - - max(attribute: string, alias: string = ""): this { - return this.push(Query.max(attribute, alias)); - } - - stddev(attribute: string, alias: string = ""): this { - return this.push(Query.stddev(attribute, alias)); - } - - stddevPop(attribute: string, alias: string = ""): this { - return this.push(Query.stddevPop(attribute, alias)); - } - - stddevSamp(attribute: string, alias: string = ""): this { - return this.push(Query.stddevSamp(attribute, alias)); - } - - variance(attribute: string, alias: string = ""): this { - return this.push(Query.variance(attribute, alias)); - } - - varPop(attribute: string, alias: string = ""): this { - return this.push(Query.varPop(attribute, alias)); - } - - varSamp(attribute: string, alias: string = ""): this { - return this.push(Query.varSamp(attribute, alias)); - } - - bitAnd(attribute: string, alias: string = ""): this { - return this.push(Query.bitAnd(attribute, alias)); - } - - bitOr(attribute: string, alias: string = ""): this { - return this.push(Query.bitOr(attribute, alias)); - } - - bitXor(attribute: string, alias: string = ""): this { - return this.push(Query.bitXor(attribute, alias)); - } - - groupBy(attributes: string[]): this { - return this.push(Query.groupBy(attributes)); - } - - having(queries: string[]): this { - return this.push(Query.having(queries)); - } - - distinct(): this { - return this.push(Query.distinct()); - } - - join(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - leftJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - rightJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - fullOuterJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - crossJoin(table: string, alias: string = ""): this { - return this.push(Query.crossJoin(table, alias)); - } - - vectorDot(attribute: string, vector: number[]): this { - return this.push(Query.vectorDot(attribute, vector)); - } - - vectorCosine(attribute: string, vector: number[]): this { - return this.push(Query.vectorCosine(attribute, vector)); - } - - vectorEuclidean(attribute: string, vector: number[]): this { - return this.push(Query.vectorEuclidean(attribute, vector)); - } - - covers(attribute: string, values: any[]): this { - return this.push(Query.covers(attribute, values)); - } - - notCovers(attribute: string, values: any[]): this { - return this.push(Query.notCovers(attribute, values)); - } - - spatialEquals(attribute: string, values: any[]): this { - return this.push(Query.spatialEquals(attribute, values)); - } - - notSpatialEquals(attribute: string, values: any[]): this { - return this.push(Query.notSpatialEquals(attribute, values)); - } - - page(page: number, perPage: number = 25): this { - return this.push(Query.page(page, perPage)); - } - - private push(query: string | string[]): this { - if (Array.isArray(query)) { - this.queries.push(...query); - } else { - this.queries.push(query); + notEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.notEqual(attribute, value)); + } + + lessThan(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThan(attribute, value)); + } + + lessThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThanEqual(attribute, value)); + } + + greaterThan(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThan(attribute, value)); + } + + greaterThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThanEqual(attribute, value)); + } + + search(attribute: string, value: string): this { + return this.push(Query.search(attribute, value)); + } + + isNull(attribute: string): this { + return this.push(Query.isNull(attribute)); + } + + isNotNull(attribute: string): this { + return this.push(Query.isNotNull(attribute)); + } + + between( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ): this { + return this.push(Query.between(attribute, start, end)); + } + + startsWith(attribute: string, value: string): this { + return this.push(Query.startsWith(attribute, value)); + } + + endsWith(attribute: string, value: string): this { + return this.push(Query.endsWith(attribute, value)); + } + + select(attributes: string[]): this { + return this.push(Query.select(attributes)); + } + + cursorAfter(documentId: string): this { + return this.push(Query.cursorAfter(documentId)); + } + + cursorBefore(documentId: string): this { + return this.push(Query.cursorBefore(documentId)); + } + + orderAsc(attribute: string): this { + return this.push(Query.orderAsc(attribute)); + } + + orderDesc(attribute: string): this { + return this.push(Query.orderDesc(attribute)); + } + + orderRandom(): this { + return this.push(Query.orderRandom()); + } + + limit(limit: number): this { + return this.push(Query.limit(limit)); + } + + offset(offset: number): this { + return this.push(Query.offset(offset)); + } + + contains(attribute: string, value: string | any[]): this { + return this.push(Query.contains(attribute, value)); + } + + containsAny(attribute: string, value: any[]): this { + return this.push(Query.containsAny(attribute, value)); + } + + containsAll(attribute: string, value: any[]): this { + return this.push(Query.containsAll(attribute, value)); + } + + notContains(attribute: string, value: string | any[]): this { + return this.push(Query.notContains(attribute, value)); + } + + notSearch(attribute: string, value: string): this { + return this.push(Query.notSearch(attribute, value)); + } + + notBetween( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ): this { + return this.push(Query.notBetween(attribute, start, end)); + } + + notStartsWith(attribute: string, value: string): this { + return this.push(Query.notStartsWith(attribute, value)); + } + + notEndsWith(attribute: string, value: string): this { + return this.push(Query.notEndsWith(attribute, value)); + } + + regex(attribute: string, pattern: string): this { + return this.push(Query.regex(attribute, pattern)); + } + + exists(attributes: string[]): this { + return this.push(Query.exists(attributes)); + } + + notExists(attributes: string[]): this { + return this.push(Query.notExists(attributes)); + } + + createdBefore(value: string): this { + return this.push(Query.createdBefore(value)); + } + + createdAfter(value: string): this { + return this.push(Query.createdAfter(value)); + } + + createdBetween(start: string, end: string): this { + return this.push(Query.createdBetween(start, end)); + } + + updatedBefore(value: string): this { + return this.push(Query.updatedBefore(value)); + } + + updatedAfter(value: string): this { + return this.push(Query.updatedAfter(value)); + } + + updatedBetween(start: string, end: string): this { + return this.push(Query.updatedBetween(start, end)); + } + + or(queries: string[]): this { + return this.push(Query.or(queries)); + } + + and(queries: string[]): this { + return this.push(Query.and(queries)); + } + + elemMatch(attribute: string, queries: string[]): this { + return this.push(Query.elemMatch(attribute, queries)); + } + + distanceEqual( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceEqual(attribute, values, distance, meters), + ); + } + + distanceNotEqual( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceNotEqual(attribute, values, distance, meters), + ); + } + + distanceGreaterThan( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceGreaterThan(attribute, values, distance, meters), + ); + } + + distanceLessThan( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceLessThan(attribute, values, distance, meters), + ); + } + + intersects(attribute: string, values: any[]): this { + return this.push(Query.intersects(attribute, values)); + } + + notIntersects(attribute: string, values: any[]): this { + return this.push(Query.notIntersects(attribute, values)); + } + + crosses(attribute: string, values: any[]): this { + return this.push(Query.crosses(attribute, values)); + } + + notCrosses(attribute: string, values: any[]): this { + return this.push(Query.notCrosses(attribute, values)); + } + + overlaps(attribute: string, values: any[]): this { + return this.push(Query.overlaps(attribute, values)); + } + + notOverlaps(attribute: string, values: any[]): this { + return this.push(Query.notOverlaps(attribute, values)); + } + + touches(attribute: string, values: any[]): this { + return this.push(Query.touches(attribute, values)); + } + + notTouches(attribute: string, values: any[]): this { + return this.push(Query.notTouches(attribute, values)); + } + + count(attribute: string = '*', alias: string = ''): this { + return this.push(Query.count(attribute, alias)); + } + + countDistinct(attribute: string, alias: string = ''): this { + return this.push(Query.countDistinct(attribute, alias)); + } + + sum(attribute: string, alias: string = ''): this { + return this.push(Query.sum(attribute, alias)); + } + + avg(attribute: string, alias: string = ''): this { + return this.push(Query.avg(attribute, alias)); + } + + min(attribute: string, alias: string = ''): this { + return this.push(Query.min(attribute, alias)); + } + + max(attribute: string, alias: string = ''): this { + return this.push(Query.max(attribute, alias)); + } + + stddev(attribute: string, alias: string = ''): this { + return this.push(Query.stddev(attribute, alias)); + } + + stddevPop(attribute: string, alias: string = ''): this { + return this.push(Query.stddevPop(attribute, alias)); + } + + stddevSamp(attribute: string, alias: string = ''): this { + return this.push(Query.stddevSamp(attribute, alias)); + } + + variance(attribute: string, alias: string = ''): this { + return this.push(Query.variance(attribute, alias)); + } + + varPop(attribute: string, alias: string = ''): this { + return this.push(Query.varPop(attribute, alias)); + } + + varSamp(attribute: string, alias: string = ''): this { + return this.push(Query.varSamp(attribute, alias)); + } + + bitAnd(attribute: string, alias: string = ''): this { + return this.push(Query.bitAnd(attribute, alias)); + } + + bitOr(attribute: string, alias: string = ''): this { + return this.push(Query.bitOr(attribute, alias)); + } + + bitXor(attribute: string, alias: string = ''): this { + return this.push(Query.bitXor(attribute, alias)); + } + + groupBy(attributes: string[]): this { + return this.push(Query.groupBy(attributes)); + } + + having(queries: string[]): this { + return this.push(Query.having(queries)); + } + + distinct(): this { + return this.push(Query.distinct()); + } + + join( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias), + ); + } + + leftJoin( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.leftJoin( + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ), + ); + } + + rightJoin( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.rightJoin( + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ), + ); + } + + fullOuterJoin( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.fullOuterJoin( + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ), + ); + } + + crossJoin(table: string, alias: string = ''): this { + return this.push(Query.crossJoin(table, alias)); + } + + vectorDot(attribute: string, vector: number[]): this { + return this.push(Query.vectorDot(attribute, vector)); + } + + vectorCosine(attribute: string, vector: number[]): this { + return this.push(Query.vectorCosine(attribute, vector)); + } + + vectorEuclidean(attribute: string, vector: number[]): this { + return this.push(Query.vectorEuclidean(attribute, vector)); + } + + covers(attribute: string, values: any[]): this { + return this.push(Query.covers(attribute, values)); + } + + notCovers(attribute: string, values: any[]): this { + return this.push(Query.notCovers(attribute, values)); + } + + spatialEquals(attribute: string, values: any[]): this { + return this.push(Query.spatialEquals(attribute, values)); + } + + notSpatialEquals(attribute: string, values: any[]): this { + return this.push(Query.notSpatialEquals(attribute, values)); + } + + page(page: number, perPage: number = 25): this { + return this.push(Query.page(page, perPage)); + } + + private push(query: string | string[]): this { + if (Array.isArray(query)) { + this.queries.push(...query); + } else { + this.queries.push(query); + } + return this; + } } - return this; - } -} } export function normalizeQueries(queries?: QueryInput | null): string[] { - if (queries == null) { - return []; - } - if (queries instanceof Query.Builder) { - return queries.build(); - } - if (!Array.isArray(queries)) { - if (queries instanceof Query) { - return [queries.toString()]; + if (queries == null) { + return []; } - if (typeof queries === "string") { - return [queries]; + if (queries instanceof Query.Builder) { + return queries.build(); } - return []; - } - - const out: string[] = []; - for (const item of queries) { - out.push(...normalizeQueries(item)); - } - return out; + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === 'string') { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...normalizeQueries(item)); + } + return out; } function isQueryList(value: unknown[]): boolean { - return value.every( - (item) => - item instanceof Query.Builder || - item instanceof Query || - typeof item === "string" || - (Array.isArray(item) && isQueryList(item)) - ); + return value.every( + (item) => + item instanceof Query.Builder || + item instanceof Query || + typeof item === 'string' || + (Array.isArray(item) && isQueryList(item)), + ); } export function flattenParam(value: unknown): unknown { - if (value instanceof Query.Builder || value instanceof Query) { - return normalizeQueries(value as QueryInput); - } - if (Array.isArray(value) && isQueryList(value)) { - return normalizeQueries(value as QueryInput); - } - return value; + if (value instanceof Query.Builder || value instanceof Query) { + return normalizeQueries(value as QueryInput); + } + if (Array.isArray(value) && isQueryList(value)) { + return normalizeQueries(value as QueryInput); + } + return value; } export const QueryBuilder = Query.Builder; diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index ba36c90e1e..4f74703ddc 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -9,7 +9,7 @@ import { Platform } from 'react-native'; export type RealtimeSubscriptionUpdate = { channels?: (string | Channel | ActionableChannel | ResolvedChannel)[]; queries?: QueryInput; -} +}; export type RealtimeSubscription = { /** @@ -28,18 +28,18 @@ export type RealtimeSubscription = { * Prefer `unsubscribe()` for per-subscription teardown and `Realtime.disconnect()` for full shutdown. */ close: () => Promise; -} +}; export type RealtimeCallback = { channels: Set; queries: string[]; // Array of query strings callback: (event: RealtimeResponseEvent) => void; -} +}; export type RealtimeResponse = { type: string; data?: any; -} +}; export type RealtimeResponseEvent = { events: string[]; @@ -47,17 +47,17 @@ export type RealtimeResponseEvent = { timestamp: string; payload: T; subscriptions: string[]; // Backend-provided subscription IDs -} +}; export type RealtimeResponseConnected = { channels: string[]; user?: object; -} +}; export type RealtimeRequest = { type: 'authentication' | 'subscribe' | 'unsubscribe' | 'presence'; data: any; -} +}; export type RealtimePresence = { $id: string; @@ -70,25 +70,25 @@ export type RealtimePresence = { status?: string; source: string; metadata?: Record; -} +}; export type RealtimePresenceCreate = { status: string; presenceId: string; permissions?: string[]; metadata?: Record; -} +}; type RealtimeRequestSubscribeRow = { subscriptionId?: string; channels: string[]; queries: string[]; -} +}; export enum RealtimeCode { NORMAL_CLOSURE = 1000, POLICY_VIOLATION = 1008, - UNKNOWN_ERROR = -1 + UNKNOWN_ERROR = -1, } export class Realtime { @@ -116,7 +116,9 @@ export class Realtime { private connectionId = 0; private reconnect = true; - private onErrorCallbacks: Array<(error?: Error, statusCode?: number) => void> = []; + private onErrorCallbacks: Array< + (error?: Error, statusCode?: number) => void + > = []; private onCloseCallbacks: Array<() => void> = []; private onOpenCallbacks: Array<() => void> = []; @@ -130,7 +132,9 @@ export class Realtime { * @param {Function} callback - Callback function to handle errors * @returns {void} */ - public onError(callback: (error?: Error, statusCode?: number) => void): void { + public onError( + callback: (error?: Error, statusCode?: number) => void, + ): void { this.onErrorCallbacks.push(callback); } @@ -249,14 +253,21 @@ export class Realtime { const connectionId = ++this.connectionId; {% if language.name == 'ReactNative' %} const WebSocketCtor: any = WebSocket; - const socket = (this.socket = Platform.OS === 'web' - ? new WebSocketCtor(jwt ? `${url}&jwt=${encodeURIComponent(jwt)}` : url) - : new WebSocketCtor(url, undefined, { - headers: { - Origin: `{{ spec.info.title | caseLower }}-${Platform.OS}://${this.client.config.platform}`, - ...(jwt ? { 'x-{{ spec.info.title | caseLower }}-jwt': jwt } : {}) - } - })); + const socket = (this.socket = + Platform.OS === 'web' + ? new WebSocketCtor( + jwt + ? `${url}&jwt=${encodeURIComponent(jwt)}` + : url, + ) + : new WebSocketCtor(url, undefined, { + headers: { + Origin: `{{ spec.info.title | caseLower }}-${Platform.OS}://${this.client.config.platform}`, + ...(jwt + ? { 'x-{{ spec.info.title | caseLower }}-jwt': jwt } + : {}), + }, + })); {% else %} const socket = (this.socket = new WebSocket(url)); {% endif %} @@ -266,7 +277,7 @@ export class Realtime { return; } this.reconnectAttempts = 0; - this.onOpenCallbacks.forEach(callback => callback()); + this.onOpenCallbacks.forEach((callback) => callback()); this.startHeartbeat(); resolve(); }); @@ -276,7 +287,9 @@ export class Realtime { return; } try { - const message = JSONbig.parse(event.data) as RealtimeResponse; + const message = JSONbig.parse( + event.data, + ) as RealtimeResponse; this.handleMessage(message); } catch (error) { console.error('Failed to parse message:', error); @@ -284,20 +297,28 @@ export class Realtime { }); socket.addEventListener('close', async (event: CloseEvent) => { - if (connectionId !== this.connectionId || socket !== this.socket) { + if ( + connectionId !== this.connectionId || + socket !== this.socket + ) { return; } this.appConnected = false; this.stopHeartbeat(); - this.onCloseCallbacks.forEach(callback => callback()); + this.onCloseCallbacks.forEach((callback) => callback()); - if (!this.reconnect || event.code === RealtimeCode.POLICY_VIOLATION) { + if ( + !this.reconnect || + event.code === RealtimeCode.POLICY_VIOLATION + ) { this.reconnect = true; return; } const timeout = this.getTimeout(); - console.log(`Realtime disconnected. Re-connecting in ${timeout / 1000} seconds.`); + console.log( + `Realtime disconnected. Re-connecting in ${timeout / 1000} seconds.`, + ); await this.sleep(timeout); this.reconnectAttempts++; @@ -310,13 +331,18 @@ export class Realtime { }); socket.addEventListener('error', (event: Event) => { - if (connectionId !== this.connectionId || socket !== this.socket) { + if ( + connectionId !== this.connectionId || + socket !== this.socket + ) { return; } this.stopHeartbeat(); const error = new Error('WebSocket error'); console.error('WebSocket error:', error.message); - this.onErrorCallbacks.forEach(callback => callback(error)); + this.onErrorCallbacks.forEach((callback) => + callback(error), + ); reject(error); }); } catch (error) { @@ -335,11 +361,17 @@ export class Realtime { return; } - if (this.socket.readyState === WebSocket.OPEN || - this.socket.readyState === WebSocket.CONNECTING) { - this.socket.addEventListener('close', () => { - resolve(); - }, { once: true }); + if ( + this.socket.readyState === WebSocket.OPEN || + this.socket.readyState === WebSocket.CONNECTING + ) { + this.socket.addEventListener( + 'close', + () => { + resolve(); + }, + { once: true }, + ); this.socket.close(RealtimeCode.NORMAL_CLOSURE); } else { resolve(); @@ -361,21 +393,25 @@ export class Realtime { } private sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } private sendUnsubscribeMessage(subscriptionIds: string[]): void { - const ids = subscriptionIds.filter(id => typeof id === 'string' && id.length > 0); + const ids = subscriptionIds.filter( + (id) => typeof id === 'string' && id.length > 0, + ); if (ids.length === 0) { return; } if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { return; } - this.socket.send(JSONbig.stringify({ - type: 'unsubscribe', - data: ids.map(subscriptionId => ({ subscriptionId })) - })); + this.socket.send( + JSONbig.stringify({ + type: 'unsubscribe', + data: ids.map((subscriptionId) => ({ subscriptionId })), + }), + ); } private generateUniqueSubscriptionId(): string { @@ -386,7 +422,9 @@ export class Realtime { return id; } } - throw new {{spec.info.title | caseUcfirst}}Exception('Failed to generate unique subscription id'); + throw new {{spec.info.title | caseUcfirst}}Exception( + 'Failed to generate unique subscription id', + ); } private enqueuePendingSubscribe(subscriptionId: string): void { @@ -397,7 +435,7 @@ export class Realtime { this.pendingSubscribes.set(subscriptionId, { subscriptionId, channels: Array.from(subscription.channels), - queries: subscription.queries ?? [] + queries: subscription.queries ?? [], }); } @@ -448,10 +486,12 @@ export class Realtime { const rows = Array.from(this.pendingSubscribes.values()); this.pendingSubscribes.clear(); - this.socket.send(JSONbig.stringify({ - type: 'subscribe', - data: rows - })); + this.socket.send( + JSONbig.stringify({ + type: 'subscribe', + data: rows, + }), + ); } /** @@ -461,12 +501,17 @@ export class Realtime { * @param {string | Channel | ActionableChannel | ResolvedChannel} channel - Channel value (string or Channel builder instance) * @returns {string} Channel string representation */ - private channelToString(channel: string | Channel | ActionableChannel | ResolvedChannel): string { + private channelToString( + channel: string | Channel | ActionableChannel | ResolvedChannel, + ): string { if (typeof channel === 'string') { return channel; } // All Channel instances have toString() method - if (channel && typeof (channel as Channel).toString === 'function') { + if ( + channel && + typeof (channel as Channel).toString === 'function' + ) { return (channel as Channel).toString(); } return String(channel); @@ -482,7 +527,7 @@ export class Realtime { public async subscribe( channel: string | Channel | ActionableChannel | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void, - queries?: QueryInput + queries?: QueryInput, ): Promise; /** @@ -493,9 +538,14 @@ export class Realtime { * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: (string | Channel | ActionableChannel | ResolvedChannel)[], + channels: ( + | string + | Channel + | ActionableChannel + | ResolvedChannel + )[], callback: (event: RealtimeResponseEvent) => void, - queries?: QueryInput + queries?: QueryInput, ): Promise; /** @@ -508,7 +558,7 @@ export class Realtime { public async subscribe( channel: string | Channel | ActionableChannel | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void, - queries?: QueryInput + queries?: QueryInput, ): Promise; /** @@ -519,22 +569,34 @@ export class Realtime { * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: (string | Channel | ActionableChannel | ResolvedChannel)[], + channels: ( + | string + | Channel + | ActionableChannel + | ResolvedChannel + )[], callback: (event: RealtimeResponseEvent) => void, - queries?: QueryInput + queries?: QueryInput, ): Promise; public async subscribe( - channelsOrChannel: string | Channel | ActionableChannel | ResolvedChannel | (string | Channel | ActionableChannel | ResolvedChannel)[], + channelsOrChannel: + | string + | Channel + | ActionableChannel + | ResolvedChannel + | (string | Channel | ActionableChannel | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void, - queries: QueryInput = [] + queries: QueryInput = [], ): Promise { const channelArray = Array.isArray(channelsOrChannel) ? channelsOrChannel : [channelsOrChannel]; - + // Convert all channels to strings - const channelStrings = channelArray.map(ch => this.channelToString(ch)); + const channelStrings = channelArray.map((ch) => + this.channelToString(ch), + ); const channels = new Set(channelStrings); const queryStrings = normalizeQueries(queries); @@ -544,7 +606,7 @@ export class Realtime { this.activeSubscriptions.set(subscriptionId, { channels, queries: queryStrings, - callback + callback, }); this.enqueuePendingSubscribe(subscriptionId); @@ -572,14 +634,18 @@ export class Realtime { this.sendUnsubscribeMessage([subscriptionId]); }; - const update = async (changes: RealtimeSubscriptionUpdate): Promise => { + const update = async ( + changes: RealtimeSubscriptionUpdate, + ): Promise => { const subscription = this.activeSubscriptions.get(subscriptionId); if (!subscription) { return; } if (changes.channels !== undefined) { - const nextChannelStrings = changes.channels.map(ch => this.channelToString(ch)); + const nextChannelStrings = changes.channels.map((ch) => + this.channelToString(ch), + ); subscription.channels = new Set(nextChannelStrings); } @@ -594,7 +660,10 @@ export class Realtime { await this.sleep(this.DEBOUNCE_MS); if (this.subCallDepth === 1) { - if (!this.socket || this.socket.readyState > WebSocket.OPEN) { + if ( + !this.socket || + this.socket.readyState > WebSocket.OPEN + ) { await this.createSocket(); } else if (this.socket.readyState === WebSocket.OPEN) { this.sendPendingSubscribes(); @@ -650,7 +719,10 @@ export class Realtime { // payload is stored. if (!this.socket || this.socket.readyState >= WebSocket.CLOSING) { this.createSocket().catch((error) => { - console.error('Failed to open realtime socket for presence:', error); + console.error( + 'Failed to open realtime socket for presence:', + error, + ); }); } @@ -670,10 +742,12 @@ export class Realtime { if (!this.appConnected) { return; } - this.socket.send(JSONbig.stringify({ - type: 'presence', - data: this.pendingPresence - })); + this.socket.send( + JSONbig.stringify({ + type: 'presence', + data: this.pendingPresence, + }), + ); } private handleMessage(message: RealtimeResponse): void { @@ -707,7 +781,9 @@ export class Realtime { let session = this.client.config.session; if (!session) { try { - const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); + const cookie = JSONbig.parse( + window.localStorage.getItem('cookieFallback') ?? '{}', + ); session = cookie?.[`a_session_${this.client.config.project}`]; } catch (error) { console.error('Failed to parse cookie fallback:', error); @@ -715,12 +791,14 @@ export class Realtime { } if (session && !messageData.user) { - this.socket?.send(JSONbig.stringify({ - type: 'authentication', - data: { - session - } - })); + this.socket?.send( + JSONbig.stringify({ + type: 'authentication', + data: { + session, + }, + }), + ); } for (const subscriptionId of this.activeSubscriptions.keys()) { @@ -733,10 +811,12 @@ export class Realtime { private handleResponseError(message: RealtimeResponse): void { const error = new {{spec.info.title | caseUcfirst}}Exception( - message.data?.message || 'Unknown error' + message.data?.message || 'Unknown error', ); const statusCode = message.data?.code; - this.onErrorCallbacks.forEach(callback => callback(error, statusCode)); + this.onErrorCallbacks.forEach((callback) => + callback(error, statusCode), + ); } private handleResponseEvent(message: RealtimeResponse): void { @@ -751,7 +831,13 @@ export class Realtime { const timestamp = data.timestamp as string; const subscriptions = data.subscriptions as string[] | undefined; - if (!channels || !events || !payload || !subscriptions || subscriptions.length === 0) { + if ( + !channels || + !events || + !payload || + !subscriptions || + subscriptions.length === 0 + ) { return; } @@ -765,7 +851,7 @@ export class Realtime { channels, timestamp, payload, - subscriptions + subscriptions, }); } } From 0a68bd7c3b2e9686d3bdf7bd6c465dce74895ce7 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 26 Aug 2026 19:00:45 +1200 Subject: [PATCH 29/42] style(templates): satisfy the formatter and lint gates main added main landed swift-format, rustfmt and the eslint/prettier chain as required checks after this branch last ran green, so the query-helper code it introduces was never measured against them. Seven jobs went red on the merge: apple, swift, rust, node, react-native and web on both client and console. Every hunk is the formatter's own output, taken by running the gate locally and porting the result back through the template. Where a twig tag sits on the line, the wrap is driven by the generated line rather than the template line, because interpolation changes the width. The lint fixes are real, not cosmetic: a lexical declaration in an unbraced case block, a dead initialiser, two generics constrained to unknown, three let bindings that are never reassigned and an unused listener argument. queryParams and the two localStorage guards differ per platform, so their twig conditionals now carry the whole statement instead of a fragment. Header keys in the generated client are quoted only when prettier would quote them, so a hyphenated header can no longer emit invalid JS. Verified locally against each gate: swift and apple lint clean and build; rust is fmt-clean with 786 tests passing; node runs 865 tests across 34 suites; web (client and console) and react-native pass format:check, eslint, tsc and build. djlint and the 1200-character twig check are clean. Co-Authored-By: Claude Opus 5 --- templates/apple/Sources/Client.swift.twig | 3 +- templates/node/src/index.ts.twig | 8 +- templates/node/test/query.test.js.twig | 332 +-- templates/react-native/src/query.ts.twig | 2233 ++++++++++--------- templates/rust/src/query/builder.rs.twig | 177 +- templates/rust/src/query/mod.rs.twig | 90 +- templates/swift/Sources/Client.swift.twig | 3 +- templates/swift/Sources/Query.swift.twig | 3 +- templates/swift/Tests/Tests.swift.twig | 12 +- templates/web/src/client.ts.twig | 60 +- templates/web/src/services/realtime.ts.twig | 20 +- 11 files changed, 1682 insertions(+), 1259 deletions(-) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index 8997d21bb9..209d0a82d8 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -319,7 +319,8 @@ open class Client { sink: ((ByteBuffer) -> Void)? = nil, converter: ((Any) throws -> T)? = nil ) async throws -> T { - let validParams = params + let validParams = + params .mapValues { flatten($0) } .filter { $0.value != nil } diff --git a/templates/node/src/index.ts.twig b/templates/node/src/index.ts.twig index ec69551376..77680b64c8 100644 --- a/templates/node/src/index.ts.twig +++ b/templates/node/src/index.ts.twig @@ -1,4 +1,10 @@ -export { Client, Query, QueryBuilder, flattenParam, {{spec.info.title | caseUcfirst}}Exception } from './client'; +export { + Client, + Query, + QueryBuilder, + flattenParam, + {{spec.info.title | caseUcfirst}}Exception, +} from './client'; {% for service in services %} export { {{service.name | caseUcfirst}} } from './services/{{service.name | caseKebab}}'; {% endfor %} diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index 05329c9c70..07a6b7bb94 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -1,245 +1,247 @@ -const { Query, normalizeQueries, flattenParam } = require("../dist/query"); +const { Query, normalizeQueries, flattenParam } = require('../dist/query'); const tests = [ { description: 'with a string', value: 's', - expectedValues: '["s"]' + expectedValues: '["s"]', }, { description: 'with a integer', value: 1, - expectedValues: '[1]' + expectedValues: '[1]', }, { description: 'with a double', value: 1.2, - expectedValues: '[1.2]' + expectedValues: '[1.2]', }, { description: 'with a whole number double', value: 1.0, - expectedValues: '[1]' + expectedValues: '[1]', }, { description: 'with a bool', value: false, - expectedValues: '[false]' + expectedValues: '[false]', }, { description: 'with a list', value: ['a', 'b', 'c'], - expectedValues: '["a","b","c"]' - } + expectedValues: '["a","b","c"]', + }, ]; describe('Query', () => { describe('basic filter equal', () => { for (const t of tests) { test(t.description, () => - expect(Query.equal("attr", t.value)) - .toEqual(`{"method":"equal","attribute":"attr","values":${t.expectedValues}}`) - ) + expect(Query.equal('attr', t.value)).toEqual( + `{"method":"equal","attribute":"attr","values":${t.expectedValues}}`, + ), + ); } - }) + }); describe('basic filter notEqual', () => { for (const t of tests) { test(t.description, () => - expect(Query.notEqual("attr", t.value)) - .toEqual(`{"method":"notEqual","attribute":"attr","values":${t.expectedValues}}`) - ) + expect(Query.notEqual('attr', t.value)).toEqual( + `{"method":"notEqual","attribute":"attr","values":${t.expectedValues}}`, + ), + ); } }); describe('basic filter lessThan', () => { for (const t of tests) { test(t.description, () => - expect(Query.lessThan("attr", t.value)) - .toEqual(`{"method":"lessThan","attribute":"attr","values":${t.expectedValues}}`) - ) + expect(Query.lessThan('attr', t.value)).toEqual( + `{"method":"lessThan","attribute":"attr","values":${t.expectedValues}}`, + ), + ); } }); describe('basic filter lessThanEqual', () => { for (const t of tests) { test(t.description, () => - expect(Query.lessThanEqual("attr", t.value)) - .toEqual(`{"method":"lessThanEqual","attribute":"attr","values":${t.expectedValues}}`) - ) + expect(Query.lessThanEqual('attr', t.value)).toEqual( + `{"method":"lessThanEqual","attribute":"attr","values":${t.expectedValues}}`, + ), + ); } }); describe('basic filter greaterThan', () => { for (const t of tests) { test(t.description, () => - expect(Query.greaterThan("attr", t.value)) - .toEqual(`{"method":"greaterThan","attribute":"attr","values":${t.expectedValues}}`) - ) + expect(Query.greaterThan('attr', t.value)).toEqual( + `{"method":"greaterThan","attribute":"attr","values":${t.expectedValues}}`, + ), + ); } }); describe('basic filter greaterThanEqual', () => { for (const t of tests) { test(t.description, () => - expect(Query.greaterThanEqual("attr", t.value)) - .toEqual(`{"method":"greaterThanEqual","attribute":"attr","values":${t.expectedValues}}`) - ) + expect(Query.greaterThanEqual('attr', t.value)).toEqual( + `{"method":"greaterThanEqual","attribute":"attr","values":${t.expectedValues}}`, + ), + ); } }); test('search', () => - expect(Query.search('attr', 'keyword1 keyword2')) - .toEqual(`{"method":"search","attribute":"attr","values":["keyword1 keyword2"]}`) - ); + expect(Query.search('attr', 'keyword1 keyword2')).toEqual( + `{"method":"search","attribute":"attr","values":["keyword1 keyword2"]}`, + )); test('isNull', () => - expect(Query.isNull('attr')) - .toEqual(`{"method":"isNull","attribute":"attr"}`) - ); + expect(Query.isNull('attr')).toEqual( + `{"method":"isNull","attribute":"attr"}`, + )); test('isNotNull', () => - expect(Query.isNotNull('attr')) - .toEqual(`{"method":"isNotNull","attribute":"attr"}`) - ); + expect(Query.isNotNull('attr')).toEqual( + `{"method":"isNotNull","attribute":"attr"}`, + )); describe('between', () => { test('with integers', () => - expect(Query.between('attr', 1, 2)) - .toEqual(`{"method":"between","attribute":"attr","values":[1,2]}`) - ); + expect(Query.between('attr', 1, 2)).toEqual( + `{"method":"between","attribute":"attr","values":[1,2]}`, + )); test('with doubles', () => - expect(Query.between('attr', 1.2, 2.2)) - .toEqual(`{"method":"between","attribute":"attr","values":[1.2,2.2]}`) - ); + expect(Query.between('attr', 1.2, 2.2)).toEqual( + `{"method":"between","attribute":"attr","values":[1.2,2.2]}`, + )); test('with strings', () => - expect(Query.between('attr',"a","z")) - .toEqual(`{"method":"between","attribute":"attr","values":["a","z"]}`) - ); + expect(Query.between('attr', 'a', 'z')).toEqual( + `{"method":"between","attribute":"attr","values":["a","z"]}`, + )); }); test('select', () => - expect(Query.select(['attr1', 'attr2'])) - .toEqual(`{"method":"select","values":["attr1","attr2"]}`) - ); + expect(Query.select(['attr1', 'attr2'])).toEqual( + `{"method":"select","values":["attr1","attr2"]}`, + )); test('orderAsc', () => - expect(Query.orderAsc('attr')) - .toEqual(`{"method":"orderAsc","attribute":"attr"}`) - ); + expect(Query.orderAsc('attr')).toEqual( + `{"method":"orderAsc","attribute":"attr"}`, + )); test('orderDesc', () => - expect(Query.orderDesc('attr')) - .toEqual(`{"method":"orderDesc","attribute":"attr"}`) - ); + expect(Query.orderDesc('attr')).toEqual( + `{"method":"orderDesc","attribute":"attr"}`, + )); test('cursorBefore', () => - expect(Query.cursorBefore('attr')) - .toEqual('{"method":"cursorBefore","values":["attr"]}') - ); + expect(Query.cursorBefore('attr')).toEqual( + '{"method":"cursorBefore","values":["attr"]}', + )); test('cursorAfter', () => - expect(Query.cursorAfter('attr')) - .toEqual('{"method":"cursorAfter","values":["attr"]}') - ); + expect(Query.cursorAfter('attr')).toEqual( + '{"method":"cursorAfter","values":["attr"]}', + )); test('limit', () => - expect(Query.limit(1)) - .toEqual('{"method":"limit","values":[1]}') - ); + expect(Query.limit(1)).toEqual('{"method":"limit","values":[1]}')); test('offset', () => - expect(Query.offset(1)) - .toEqual('{"method":"offset","values":[1]}') - ); + expect(Query.offset(1)).toEqual('{"method":"offset","values":[1]}')); test('count with alias', () => - expect(Query.count('*', 'total')) - .toEqual('{"method":"count","attribute":"*","values":["total"]}') - ); + expect(Query.count('*', 'total')).toEqual( + '{"method":"count","attribute":"*","values":["total"]}', + )); test('count without alias', () => - expect(Query.count()) - .toEqual('{"method":"count","attribute":"*"}') - ); + expect(Query.count()).toEqual('{"method":"count","attribute":"*"}')); test('countDistinct', () => - expect(Query.countDistinct('year', 'uniqueYears')) - .toEqual('{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}') - ); + expect(Query.countDistinct('year', 'uniqueYears')).toEqual( + '{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}', + )); test('sum', () => - expect(Query.sum('price', 'total')) - .toEqual('{"method":"sum","attribute":"price","values":["total"]}') - ); + expect(Query.sum('price', 'total')).toEqual( + '{"method":"sum","attribute":"price","values":["total"]}', + )); test('avg', () => - expect(Query.avg('price', 'avgPrice')) - .toEqual('{"method":"avg","attribute":"price","values":["avgPrice"]}') - ); + expect(Query.avg('price', 'avgPrice')).toEqual( + '{"method":"avg","attribute":"price","values":["avgPrice"]}', + )); test('min', () => - expect(Query.min('price', 'lowest')) - .toEqual('{"method":"min","attribute":"price","values":["lowest"]}') - ); + expect(Query.min('price', 'lowest')).toEqual( + '{"method":"min","attribute":"price","values":["lowest"]}', + )); test('max', () => - expect(Query.max('price', 'highest')) - .toEqual('{"method":"max","attribute":"price","values":["highest"]}') - ); + expect(Query.max('price', 'highest')).toEqual( + '{"method":"max","attribute":"price","values":["highest"]}', + )); test('stddev', () => - expect(Query.stddev('price', 'sd')) - .toEqual('{"method":"stddev","attribute":"price","values":["sd"]}') - ); + expect(Query.stddev('price', 'sd')).toEqual( + '{"method":"stddev","attribute":"price","values":["sd"]}', + )); test('stddevPop', () => - expect(Query.stddevPop('price', 'sdp')) - .toEqual('{"method":"stddevPop","attribute":"price","values":["sdp"]}') - ); + expect(Query.stddevPop('price', 'sdp')).toEqual( + '{"method":"stddevPop","attribute":"price","values":["sdp"]}', + )); test('stddevSamp', () => - expect(Query.stddevSamp('price', 'sds')) - .toEqual('{"method":"stddevSamp","attribute":"price","values":["sds"]}') - ); + expect(Query.stddevSamp('price', 'sds')).toEqual( + '{"method":"stddevSamp","attribute":"price","values":["sds"]}', + )); test('variance', () => - expect(Query.variance('price', 'var')) - .toEqual('{"method":"variance","attribute":"price","values":["var"]}') - ); + expect(Query.variance('price', 'var')).toEqual( + '{"method":"variance","attribute":"price","values":["var"]}', + )); test('varPop', () => - expect(Query.varPop('price', 'vp')) - .toEqual('{"method":"varPop","attribute":"price","values":["vp"]}') - ); + expect(Query.varPop('price', 'vp')).toEqual( + '{"method":"varPop","attribute":"price","values":["vp"]}', + )); test('varSamp', () => - expect(Query.varSamp('price', 'vs')) - .toEqual('{"method":"varSamp","attribute":"price","values":["vs"]}') - ); + expect(Query.varSamp('price', 'vs')).toEqual( + '{"method":"varSamp","attribute":"price","values":["vs"]}', + )); test('bitAnd', () => - expect(Query.bitAnd('flags', 'band')) - .toEqual('{"method":"bitAnd","attribute":"flags","values":["band"]}') - ); + expect(Query.bitAnd('flags', 'band')).toEqual( + '{"method":"bitAnd","attribute":"flags","values":["band"]}', + )); test('bitOr', () => - expect(Query.bitOr('flags', 'bor')) - .toEqual('{"method":"bitOr","attribute":"flags","values":["bor"]}') - ); + expect(Query.bitOr('flags', 'bor')).toEqual( + '{"method":"bitOr","attribute":"flags","values":["bor"]}', + )); test('bitXor', () => - expect(Query.bitXor('flags', 'bxor')) - .toEqual('{"method":"bitXor","attribute":"flags","values":["bxor"]}') - ); + expect(Query.bitXor('flags', 'bxor')).toEqual( + '{"method":"bitXor","attribute":"flags","values":["bxor"]}', + )); test('groupBy', () => - expect(Query.groupBy(['status', 'year'])) - .toEqual('{"method":"groupBy","values":["status","year"]}') - ); + expect(Query.groupBy(['status', 'year'])).toEqual( + '{"method":"groupBy","values":["status","year"]}', + )); test('having nests parsed queries', () => { - const having = JSON.parse(Query.having([Query.greaterThan('total', 1)])); + const having = JSON.parse( + Query.having([Query.greaterThan('total', 1)]), + ); expect(having.method).toEqual('having'); expect(having.values[0].method).toEqual('greaterThan'); expect(having.values[0].attribute).toEqual('total'); @@ -247,40 +249,42 @@ describe('Query', () => { }); test('distinct', () => - expect(Query.distinct()) - .toEqual('{"method":"distinct"}') - ); + expect(Query.distinct()).toEqual('{"method":"distinct"}')); test('join', () => - expect(Query.join('orders', '$id', 'customerId')) - .toEqual('{"method":"join","attribute":"orders","values":["$id","=","customerId"]}') - ); + expect(Query.join('orders', '$id', 'customerId')).toEqual( + '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}', + )); test('leftJoin with alias', () => - expect(Query.leftJoin('orders', '$id', 'customerId', '=', 'ord')) - .toEqual('{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}') - ); + expect( + Query.leftJoin('orders', '$id', 'customerId', '=', 'ord'), + ).toEqual( + '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', + )); test('rightJoin', () => - expect(Query.rightJoin('orders', '$id', 'customerId')) - .toEqual('{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}') - ); + expect(Query.rightJoin('orders', '$id', 'customerId')).toEqual( + '{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}', + )); test('fullOuterJoin', () => - expect(Query.fullOuterJoin('orders', '$id', 'customerId')) - .toEqual('{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}') - ); + expect(Query.fullOuterJoin('orders', '$id', 'customerId')).toEqual( + '{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}', + )); test('on', () => - expect(Query.on('$id', 'customerId')) - .toEqual('{"method":"on","values":["$id","=","customerId"]}') - ); + expect(Query.on('$id', 'customerId')).toEqual( + '{"method":"on","values":["$id","=","customerId"]}', + )); test('nested leftJoin', () => { - const query = JSON.parse(Query.leftJoin('orders', 'ord', [ - Query.on('$id', 'customerId'), - Query.equal('ord.status', 'paid'), - ])); + const query = JSON.parse( + Query.leftJoin('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ]), + ); expect(query.method).toEqual('leftJoin'); expect(query.attribute).toEqual('orders'); expect(query.values[0]).toEqual('ord'); @@ -289,29 +293,29 @@ describe('Query', () => { }); test('crossJoin', () => - expect(Query.crossJoin('orders', 'ord')) - .toEqual('{"method":"crossJoin","attribute":"orders","values":["ord"]}') - ); + expect(Query.crossJoin('orders', 'ord')).toEqual( + '{"method":"crossJoin","attribute":"orders","values":["ord"]}', + )); test('vectorDot', () => - expect(Query.vectorDot('embedding', [0.1, 0.2])) - .toEqual('{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}') - ); + expect(Query.vectorDot('embedding', [0.1, 0.2])).toEqual( + '{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}', + )); test('notCovers', () => - expect(Query.notCovers('location', [1, 2])) - .toEqual('{"method":"notCovers","attribute":"location","values":[[1,2]]}') - ); + expect(Query.notCovers('location', [1, 2])).toEqual( + '{"method":"notCovers","attribute":"location","values":[[1,2]]}', + )); test('spatialEquals', () => - expect(Query.spatialEquals('location', [1, 2])) - .toEqual('{"method":"spatialEquals","attribute":"location","values":[[1,2]]}') - ); + expect(Query.spatialEquals('location', [1, 2])).toEqual( + '{"method":"spatialEquals","attribute":"location","values":[[1,2]]}', + )); test('notSpatialEquals', () => - expect(Query.notSpatialEquals('location', [1, 2])) - .toEqual('{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}') - ); + expect(Query.notSpatialEquals('location', [1, 2])).toEqual( + '{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}', + )); test('page', () => { const queries = Query.page(2, 10); @@ -347,7 +351,12 @@ describe('Query', () => { }); test('normalize page wrapped in array', () => { - expect(normalizeQueries([Query.equal('status', 'active'), Query.page(2, 10)])).toEqual([ + expect( + normalizeQueries([ + Query.equal('status', 'active'), + Query.page(2, 10), + ]), + ).toEqual([ Query.equal('status', 'active'), Query.limit(10), Query.offset(10), @@ -355,15 +364,20 @@ describe('Query', () => { }); test('flattenParam spreads wrapped builder', () => { - expect(flattenParam([Query.builder().limit(1)])).toEqual([Query.limit(1)]); + expect(flattenParam([Query.builder().limit(1)])).toEqual([ + Query.limit(1), + ]); }); test('flattenParam keeps nested numeric arrays', () => { - const geometry = [[1, 2], [3, 4]]; + const geometry = [ + [1, 2], + [3, 4], + ]; expect(flattenParam(geometry)).toBe(geometry); }); test('flattenParam keeps query string list', () => { expect(flattenParam([Query.limit(1)])).toEqual([Query.limit(1)]); }); -}) +}); diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index c5fa21f715..ecd79ae473 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -2,1042 +2,1267 @@ import JSONbigModule from 'json-bigint'; const JSONbig = JSONbigModule({ useNativeBigInt: true }); type QueryTypesSingle = string | number | bigint | boolean; -export type QueryTypesList = string[] | number[] | bigint[] | boolean[] | Query[] | any[]; +export type QueryTypesList = + string[] | number[] | bigint[] | boolean[] | Query[] | any[]; export type QueryTypes = QueryTypesSingle | QueryTypesList; type AttributesTypes = string | string[]; export class Query { - method: string; - attribute: AttributesTypes | undefined; - values: QueryTypesList | undefined; - - constructor( - method: string, - attribute?: AttributesTypes, - values?: QueryTypes - ) { - this.method = method; - this.attribute = attribute; - - if (values !== undefined) { - if (Array.isArray(values)) { - this.values = values; - } else { - this.values = [values] as QueryTypesList; - } + method: string; + attribute: AttributesTypes | undefined; + values: QueryTypesList | undefined; + + constructor( + method: string, + attribute?: AttributesTypes, + values?: QueryTypes, + ) { + this.method = method; + this.attribute = attribute; + + if (values !== undefined) { + if (Array.isArray(values)) { + this.values = values; + } else { + this.values = [values] as QueryTypesList; + } + } } - } - toString(): string { - return JSONbig.stringify({ - method: this.method, - attribute: this.attribute, - values: this.values, - }); - } - - static equal = (attribute: string, value: QueryTypes): string => - new Query("equal", attribute, value).toString(); - - static notEqual = (attribute: string, value: QueryTypes): string => - new Query("notEqual", attribute, value).toString(); - - /** - * Filter resources where attribute matches a regular expression pattern. - * - * @param {string} attribute The attribute to filter on. - * @param {string} pattern The regular expression pattern to match. - * @returns {string} - */ - static regex = (attribute: string, pattern: string): string => - new Query("regex", attribute, pattern).toString(); - - static lessThan = (attribute: string, value: QueryTypes): string => - new Query("lessThan", attribute, value).toString(); - - static lessThanEqual = (attribute: string, value: QueryTypes): string => - new Query("lessThanEqual", attribute, value).toString(); - - static greaterThan = (attribute: string, value: QueryTypes): string => - new Query("greaterThan", attribute, value).toString(); - - static greaterThanEqual = (attribute: string, value: QueryTypes): string => - new Query("greaterThanEqual", attribute, value).toString(); - - static isNull = (attribute: string): string => - new Query("isNull", attribute).toString(); - - static isNotNull = (attribute: string): string => - new Query("isNotNull", attribute).toString(); - - /** - * Filter resources where the specified attributes exist. - * - * @param {string[]} attributes The list of attributes that must exist. - * @returns {string} - */ - static exists = (attributes: string[]): string => - new Query("exists", undefined, attributes).toString(); - - /** - * Filter resources where the specified attributes do not exist. - * - * @param {string[]} attributes The list of attributes that must not exist. - * @returns {string} - */ - static notExists = (attributes: string[]): string => - new Query("notExists", undefined, attributes).toString(); - - static between = (attribute: string, start: string | number | bigint, end: string | number | bigint) => - new Query("between", attribute, [start, end] as QueryTypesList).toString(); - - static startsWith = (attribute: string, value: string): string => - new Query("startsWith", attribute, value).toString(); - - static endsWith = (attribute: string, value: string): string => - new Query("endsWith", attribute, value).toString(); - - static select = (attributes: string[]): string => - new Query("select", undefined, attributes).toString(); - - static search = (attribute: string, value: string): string => - new Query("search", attribute, value).toString(); - - static orderDesc = (attribute: string): string => - new Query("orderDesc", attribute).toString(); - - static orderAsc = (attribute: string): string => - new Query("orderAsc", attribute).toString(); - - static orderRandom = (): string => - new Query("orderRandom").toString(); - - static cursorAfter = (documentId: string): string => - new Query("cursorAfter", undefined, documentId).toString(); - - static cursorBefore = (documentId: string): string => - new Query("cursorBefore", undefined, documentId).toString(); - - static limit = (limit: number): string => - new Query("limit", undefined, limit).toString(); - - static offset = (offset: number): string => - new Query("offset", undefined, offset).toString(); - - /** - * Filter resources where attribute contains the specified value. - * For string attributes, checks if the string contains the substring. - * - * Note: For array attributes, use {@link containsAny} or {@link containsAll} instead. - * @param {string} attribute - * @param {string | string[]} value - * @returns {string} - */ - static contains = (attribute: string, value: string | any[]): string => - new Query("contains", attribute, value).toString(); - - /** - * Filter resources where attribute contains ANY of the specified values. - * For array and relationship attributes, matches documents where the attribute - * contains at least one of the given values. - * - * @param {string} attribute - * @param {any[]} value - * @returns {string} - */ - static containsAny = (attribute: string, value: any[]): string => - new Query("containsAny", attribute, value).toString(); - - /** - * Filter resources where attribute contains ALL of the specified values. - * For array and relationship attributes, matches documents where the attribute - * contains every one of the given values. - * - * @param {string} attribute - * @param {any[]} value - * @returns {string} - */ - static containsAll = (attribute: string, value: any[]): string => - new Query("containsAll", attribute, value).toString(); - - /** - * Filter resources where attribute does not contain the specified value. - * - * @param {string} attribute - * @param {string | string[]} value - * @returns {string} - */ - static notContains = (attribute: string, value: string | any[]): string => - new Query("notContains", attribute, value).toString(); - - /** - * Filter resources by searching attribute for value (inverse of search). - * A fulltext index on attribute is required for this query to work. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static notSearch = (attribute: string, value: string): string => - new Query("notSearch", attribute, value).toString(); - - /** - * Filter resources where attribute is not between start and end (exclusive). - * - * @param {string} attribute - * @param {string | number | bigint} start - * @param {string | number | bigint} end - * @returns {string} - */ - static notBetween = (attribute: string, start: string | number | bigint, end: string | number | bigint): string => - new Query("notBetween", attribute, [start, end] as QueryTypesList).toString(); - - /** - * Filter resources where attribute does not start with value. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static notStartsWith = (attribute: string, value: string): string => - new Query("notStartsWith", attribute, value).toString(); - - /** - * Filter resources where attribute does not end with value. - * - * @param {string} attribute - * @param {string} value - * @returns {string} - */ - static notEndsWith = (attribute: string, value: string): string => - new Query("notEndsWith", attribute, value).toString(); - - /** - * Filter resources where document was created before date. - * - * @param {string} value - * @returns {string} - */ - static createdBefore = (value: string): string => - Query.lessThan("$createdAt", value); - - /** - * Filter resources where document was created after date. - * - * @param {string} value - * @returns {string} - */ - static createdAfter = (value: string): string => - Query.greaterThan("$createdAt", value); - - /** - * Filter resources where document was created between dates. - * - * @param {string} start - * @param {string} end - * @returns {string} - */ - static createdBetween = (start: string, end: string): string => - Query.between("$createdAt", start, end); - - /** - * Filter resources where document was updated before date. - * - * @param {string} value - * @returns {string} - */ - static updatedBefore = (value: string): string => - Query.lessThan("$updatedAt", value); - - /** - * Filter resources where document was updated after date. - * - * @param {string} value - * @returns {string} - */ - static updatedAfter = (value: string): string => - Query.greaterThan("$updatedAt", value); - - /** - * Filter resources where document was updated between dates. - * - * @param {string} start - * @param {string} end - * @returns {string} - */ - static updatedBetween = (start: string, end: string): string => - Query.between("$updatedAt", start, end); - - static or = (queries: string[]) => - new Query("or", undefined, queries.map((query) => JSONbig.parse(query))).toString(); - - static and = (queries: string[]) => - new Query("and", undefined, queries.map((query) => JSONbig.parse(query))).toString(); - - /** - * Filter array elements where at least one element matches all the specified queries. - * - * @param {string} attribute The attribute containing the array to filter on. - * @param {string[]} queries The list of query strings to match against array elements. - * @returns {string} - */ - static elemMatch = (attribute: string, queries: string[]): string => - new Query( - "elemMatch", - attribute, - queries.map((query) => JSONbig.parse(query)) - ).toString(); - - /** - * Filter resources where attribute is at a specific distance from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceEqual = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceEqual", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources where attribute is not at a specific distance from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceNotEqual = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceNotEqual", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources where attribute is at a distance greater than the specified value from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceGreaterThan = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceGreaterThan", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources where attribute is at a distance less than the specified value from the given coordinates. - * - * @param {string} attribute - * @param {any[]} values - * @param {number} distance - * @param {boolean} meters - * @returns {string} - */ - static distanceLessThan = (attribute: string, values: any[], distance: number, meters: boolean = true): string => - new Query("distanceLessThan", attribute, [[values, distance, meters]] as QueryTypesList).toString(); - - /** - * Filter resources using vector dot product similarity. - * - * @param {string} attribute - * @param {number[]} vector - * @returns {string} - */ - static vectorDot = (attribute: string, vector: number[]): string => - new Query("vectorDot", attribute, [vector] as QueryTypesList).toString(); - - /** - * Filter resources using vector cosine similarity. - * - * @param {string} attribute - * @param {number[]} vector - * @returns {string} - */ - static vectorCosine = (attribute: string, vector: number[]): string => - new Query("vectorCosine", attribute, [vector] as QueryTypesList).toString(); - - /** - * Filter resources using vector Euclidean distance. - * - * @param {string} attribute - * @param {number[]} vector - * @returns {string} - */ - static vectorEuclidean = (attribute: string, vector: number[]): string => - new Query("vectorEuclidean", attribute, [vector] as QueryTypesList).toString(); - - /** - * Filter resources where attribute intersects with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static intersects = (attribute: string, values: any[]): string => - new Query("intersects", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not intersect with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notIntersects = (attribute: string, values: any[]): string => - new Query("notIntersects", attribute, [values]).toString(); - - /** - * Filter resources where attribute crosses the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static crosses = (attribute: string, values: any[]): string => - new Query("crosses", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not cross the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notCrosses = (attribute: string, values: any[]): string => - new Query("notCrosses", attribute, [values]).toString(); - - /** - * Filter resources where attribute overlaps with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static overlaps = (attribute: string, values: any[]): string => - new Query("overlaps", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not overlap with the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notOverlaps = (attribute: string, values: any[]): string => - new Query("notOverlaps", attribute, [values]).toString(); - - /** - * Filter resources where attribute touches the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static touches = (attribute: string, values: any[]): string => - new Query("touches", attribute, [values]).toString(); - - /** - * Filter resources where attribute does not touch the given geometry. - * - * @param {string} attribute - * @param {any[]} values - * @returns {string} - */ - static notTouches = (attribute: string, values: any[]): string => - new Query("notTouches", attribute, [values]).toString(); - - /** - * Count documents, optionally aliased. - */ - static count = (attribute: string = "*", alias: string = ""): string => - new Query("count", attribute, alias !== "" ? alias : undefined).toString(); - - /** - * Count distinct values of attribute, optionally aliased. - */ - static countDistinct = (attribute: string, alias: string = ""): string => - new Query("countDistinct", attribute, alias !== "" ? alias : undefined).toString(); - - static sum = (attribute: string, alias: string = ""): string => - new Query("sum", attribute, alias !== "" ? alias : undefined).toString(); - - static avg = (attribute: string, alias: string = ""): string => - new Query("avg", attribute, alias !== "" ? alias : undefined).toString(); - - static min = (attribute: string, alias: string = ""): string => - new Query("min", attribute, alias !== "" ? alias : undefined).toString(); - - static max = (attribute: string, alias: string = ""): string => - new Query("max", attribute, alias !== "" ? alias : undefined).toString(); - - static stddev = (attribute: string, alias: string = ""): string => - new Query("stddev", attribute, alias !== "" ? alias : undefined).toString(); - - static stddevPop = (attribute: string, alias: string = ""): string => - new Query("stddevPop", attribute, alias !== "" ? alias : undefined).toString(); - - static stddevSamp = (attribute: string, alias: string = ""): string => - new Query("stddevSamp", attribute, alias !== "" ? alias : undefined).toString(); - - static variance = (attribute: string, alias: string = ""): string => - new Query("variance", attribute, alias !== "" ? alias : undefined).toString(); - - static varPop = (attribute: string, alias: string = ""): string => - new Query("varPop", attribute, alias !== "" ? alias : undefined).toString(); - - static varSamp = (attribute: string, alias: string = ""): string => - new Query("varSamp", attribute, alias !== "" ? alias : undefined).toString(); - - static bitAnd = (attribute: string, alias: string = ""): string => - new Query("bitAnd", attribute, alias !== "" ? alias : undefined).toString(); - - static bitOr = (attribute: string, alias: string = ""): string => - new Query("bitOr", attribute, alias !== "" ? alias : undefined).toString(); - - static bitXor = (attribute: string, alias: string = ""): string => - new Query("bitXor", attribute, alias !== "" ? alias : undefined).toString(); - - /** - * Group results by attributes. - */ - static groupBy = (attributes: string[]): string => - new Query("groupBy", undefined, attributes).toString(); - - /** - * Filter grouped results. `queries` are nested like `and`/`or`. - */ - static having = (queries: string[]): string => - new Query( - "having", - undefined, - queries.map((query) => JSONbig.parse(query)) - ).toString(); - - static distinct = (): string => - new Query("distinct").toString(); - - /** - * Column-to-column join ON condition. - */ - static on = ( - left: string, - right: string, - operator: string = "=" - ): string => - new Query("on", undefined, [left, operator, right]).toString(); - - /** - * Inner join. `table` is the joined collection/table ID. - */ - static join = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("join", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - static leftJoin = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("leftJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - static rightJoin = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("rightJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - static fullOuterJoin = ( - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string => - Query.createJoin("fullOuterJoin", table, leftOrAliasOrOn, rightOrOn, operator, alias); - - static crossJoin = (table: string, alias: string = ""): string => - new Query("crossJoin", table, alias !== "" ? alias : undefined).toString(); - - static covers = (attribute: string, values: any[]): string => - new Query("covers", attribute, [values]).toString(); - - static notCovers = (attribute: string, values: any[]): string => - new Query("notCovers", attribute, [values]).toString(); - - static spatialEquals = (attribute: string, values: any[]): string => - new Query("spatialEquals", attribute, [values]).toString(); - - static notSpatialEquals = (attribute: string, values: any[]): string => - new Query("notSpatialEquals", attribute, [values]).toString(); - - /** - * Return limit and offset queries for 1-based page pagination. - */ - static page = (page: number, perPage: number = 25): string[] => { - if (page < 1) { - throw new Error("Page must be >= 1, got " + page); + toString(): string { + return JSONbig.stringify({ + method: this.method, + attribute: this.attribute, + values: this.values, + }); } - if (perPage < 1) { - throw new Error("Per page must be >= 1, got " + perPage); - } - return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; - }; - - static builder = (): Query.Builder => new Query.Builder(); - - private static createJoin( - method: string, - table: string, - leftOrAliasOrOn: string | string[], - rightOrOn: string | string[] = "", - operator: string = "=", - alias: string = "" - ): string { - if (Array.isArray(leftOrAliasOrOn)) { - return Query.nestedJoin(method, table, "", leftOrAliasOrOn); - } - if (Array.isArray(rightOrOn)) { - return Query.nestedJoin(method, table, leftOrAliasOrOn, rightOrOn); - } - return new Query( - method, - table, - alias !== "" - ? [leftOrAliasOrOn, operator, rightOrOn, alias] - : [leftOrAliasOrOn, operator, rightOrOn] - ).toString(); - } - - private static nestedJoin( - method: string, - table: string, - alias: string, - on: string[] - ): string { - const values: any[] = []; - if (alias !== "") { - values.push(alias); + + static equal = (attribute: string, value: QueryTypes): string => + new Query('equal', attribute, value).toString(); + + static notEqual = (attribute: string, value: QueryTypes): string => + new Query('notEqual', attribute, value).toString(); + + /** + * Filter resources where attribute matches a regular expression pattern. + * + * @param {string} attribute The attribute to filter on. + * @param {string} pattern The regular expression pattern to match. + * @returns {string} + */ + static regex = (attribute: string, pattern: string): string => + new Query('regex', attribute, pattern).toString(); + + static lessThan = (attribute: string, value: QueryTypes): string => + new Query('lessThan', attribute, value).toString(); + + static lessThanEqual = (attribute: string, value: QueryTypes): string => + new Query('lessThanEqual', attribute, value).toString(); + + static greaterThan = (attribute: string, value: QueryTypes): string => + new Query('greaterThan', attribute, value).toString(); + + static greaterThanEqual = (attribute: string, value: QueryTypes): string => + new Query('greaterThanEqual', attribute, value).toString(); + + static isNull = (attribute: string): string => + new Query('isNull', attribute).toString(); + + static isNotNull = (attribute: string): string => + new Query('isNotNull', attribute).toString(); + + /** + * Filter resources where the specified attributes exist. + * + * @param {string[]} attributes The list of attributes that must exist. + * @returns {string} + */ + static exists = (attributes: string[]): string => + new Query('exists', undefined, attributes).toString(); + + /** + * Filter resources where the specified attributes do not exist. + * + * @param {string[]} attributes The list of attributes that must not exist. + * @returns {string} + */ + static notExists = (attributes: string[]): string => + new Query('notExists', undefined, attributes).toString(); + + static between = ( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ) => + new Query('between', attribute, [ + start, + end, + ] as QueryTypesList).toString(); + + static startsWith = (attribute: string, value: string): string => + new Query('startsWith', attribute, value).toString(); + + static endsWith = (attribute: string, value: string): string => + new Query('endsWith', attribute, value).toString(); + + static select = (attributes: string[]): string => + new Query('select', undefined, attributes).toString(); + + static search = (attribute: string, value: string): string => + new Query('search', attribute, value).toString(); + + static orderDesc = (attribute: string): string => + new Query('orderDesc', attribute).toString(); + + static orderAsc = (attribute: string): string => + new Query('orderAsc', attribute).toString(); + + static orderRandom = (): string => new Query('orderRandom').toString(); + + static cursorAfter = (documentId: string): string => + new Query('cursorAfter', undefined, documentId).toString(); + + static cursorBefore = (documentId: string): string => + new Query('cursorBefore', undefined, documentId).toString(); + + static limit = (limit: number): string => + new Query('limit', undefined, limit).toString(); + + static offset = (offset: number): string => + new Query('offset', undefined, offset).toString(); + + /** + * Filter resources where attribute contains the specified value. + * For string attributes, checks if the string contains the substring. + * + * Note: For array attributes, use {@link containsAny} or {@link containsAll} instead. + * @param {string} attribute + * @param {string | string[]} value + * @returns {string} + */ + static contains = (attribute: string, value: string | any[]): string => + new Query('contains', attribute, value).toString(); + + /** + * Filter resources where attribute contains ANY of the specified values. + * For array and relationship attributes, matches documents where the attribute + * contains at least one of the given values. + * + * @param {string} attribute + * @param {any[]} value + * @returns {string} + */ + static containsAny = (attribute: string, value: any[]): string => + new Query('containsAny', attribute, value).toString(); + + /** + * Filter resources where attribute contains ALL of the specified values. + * For array and relationship attributes, matches documents where the attribute + * contains every one of the given values. + * + * @param {string} attribute + * @param {any[]} value + * @returns {string} + */ + static containsAll = (attribute: string, value: any[]): string => + new Query('containsAll', attribute, value).toString(); + + /** + * Filter resources where attribute does not contain the specified value. + * + * @param {string} attribute + * @param {string | string[]} value + * @returns {string} + */ + static notContains = (attribute: string, value: string | any[]): string => + new Query('notContains', attribute, value).toString(); + + /** + * Filter resources by searching attribute for value (inverse of search). + * A fulltext index on attribute is required for this query to work. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static notSearch = (attribute: string, value: string): string => + new Query('notSearch', attribute, value).toString(); + + /** + * Filter resources where attribute is not between start and end (exclusive). + * + * @param {string} attribute + * @param {string | number | bigint} start + * @param {string | number | bigint} end + * @returns {string} + */ + static notBetween = ( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ): string => + new Query('notBetween', attribute, [ + start, + end, + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute does not start with value. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static notStartsWith = (attribute: string, value: string): string => + new Query('notStartsWith', attribute, value).toString(); + + /** + * Filter resources where attribute does not end with value. + * + * @param {string} attribute + * @param {string} value + * @returns {string} + */ + static notEndsWith = (attribute: string, value: string): string => + new Query('notEndsWith', attribute, value).toString(); + + /** + * Filter resources where document was created before date. + * + * @param {string} value + * @returns {string} + */ + static createdBefore = (value: string): string => + Query.lessThan('$createdAt', value); + + /** + * Filter resources where document was created after date. + * + * @param {string} value + * @returns {string} + */ + static createdAfter = (value: string): string => + Query.greaterThan('$createdAt', value); + + /** + * Filter resources where document was created between dates. + * + * @param {string} start + * @param {string} end + * @returns {string} + */ + static createdBetween = (start: string, end: string): string => + Query.between('$createdAt', start, end); + + /** + * Filter resources where document was updated before date. + * + * @param {string} value + * @returns {string} + */ + static updatedBefore = (value: string): string => + Query.lessThan('$updatedAt', value); + + /** + * Filter resources where document was updated after date. + * + * @param {string} value + * @returns {string} + */ + static updatedAfter = (value: string): string => + Query.greaterThan('$updatedAt', value); + + /** + * Filter resources where document was updated between dates. + * + * @param {string} start + * @param {string} end + * @returns {string} + */ + static updatedBetween = (start: string, end: string): string => + Query.between('$updatedAt', start, end); + + static or = (queries: string[]) => + new Query( + 'or', + undefined, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + static and = (queries: string[]) => + new Query( + 'and', + undefined, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + /** + * Filter array elements where at least one element matches all the specified queries. + * + * @param {string} attribute The attribute containing the array to filter on. + * @param {string[]} queries The list of query strings to match against array elements. + * @returns {string} + */ + static elemMatch = (attribute: string, queries: string[]): string => + new Query( + 'elemMatch', + attribute, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + /** + * Filter resources where attribute is at a specific distance from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceEqual = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceEqual', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute is not at a specific distance from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceNotEqual = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceNotEqual', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute is at a distance greater than the specified value from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceGreaterThan = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceGreaterThan', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute is at a distance less than the specified value from the given coordinates. + * + * @param {string} attribute + * @param {any[]} values + * @param {number} distance + * @param {boolean} meters + * @returns {string} + */ + static distanceLessThan = ( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): string => + new Query('distanceLessThan', attribute, [ + [values, distance, meters], + ] as QueryTypesList).toString(); + + /** + * Filter resources using vector dot product similarity. + * + * @param {string} attribute + * @param {number[]} vector + * @returns {string} + */ + static vectorDot = (attribute: string, vector: number[]): string => + new Query('vectorDot', attribute, [ + vector, + ] as QueryTypesList).toString(); + + /** + * Filter resources using vector cosine similarity. + * + * @param {string} attribute + * @param {number[]} vector + * @returns {string} + */ + static vectorCosine = (attribute: string, vector: number[]): string => + new Query('vectorCosine', attribute, [ + vector, + ] as QueryTypesList).toString(); + + /** + * Filter resources using vector Euclidean distance. + * + * @param {string} attribute + * @param {number[]} vector + * @returns {string} + */ + static vectorEuclidean = (attribute: string, vector: number[]): string => + new Query('vectorEuclidean', attribute, [ + vector, + ] as QueryTypesList).toString(); + + /** + * Filter resources where attribute intersects with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static intersects = (attribute: string, values: any[]): string => + new Query('intersects', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not intersect with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notIntersects = (attribute: string, values: any[]): string => + new Query('notIntersects', attribute, [values]).toString(); + + /** + * Filter resources where attribute crosses the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static crosses = (attribute: string, values: any[]): string => + new Query('crosses', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not cross the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notCrosses = (attribute: string, values: any[]): string => + new Query('notCrosses', attribute, [values]).toString(); + + /** + * Filter resources where attribute overlaps with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static overlaps = (attribute: string, values: any[]): string => + new Query('overlaps', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not overlap with the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notOverlaps = (attribute: string, values: any[]): string => + new Query('notOverlaps', attribute, [values]).toString(); + + /** + * Filter resources where attribute touches the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static touches = (attribute: string, values: any[]): string => + new Query('touches', attribute, [values]).toString(); + + /** + * Filter resources where attribute does not touch the given geometry. + * + * @param {string} attribute + * @param {any[]} values + * @returns {string} + */ + static notTouches = (attribute: string, values: any[]): string => + new Query('notTouches', attribute, [values]).toString(); + + /** + * Count documents, optionally aliased. + */ + static count = (attribute: string = '*', alias: string = ''): string => + new Query( + 'count', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** + * Count distinct values of attribute, optionally aliased. + */ + static countDistinct = (attribute: string, alias: string = ''): string => + new Query( + 'countDistinct', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static sum = (attribute: string, alias: string = ''): string => + new Query( + 'sum', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static avg = (attribute: string, alias: string = ''): string => + new Query( + 'avg', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static min = (attribute: string, alias: string = ''): string => + new Query( + 'min', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static max = (attribute: string, alias: string = ''): string => + new Query( + 'max', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static stddev = (attribute: string, alias: string = ''): string => + new Query( + 'stddev', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static stddevPop = (attribute: string, alias: string = ''): string => + new Query( + 'stddevPop', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static stddevSamp = (attribute: string, alias: string = ''): string => + new Query( + 'stddevSamp', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static variance = (attribute: string, alias: string = ''): string => + new Query( + 'variance', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static varPop = (attribute: string, alias: string = ''): string => + new Query( + 'varPop', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static varSamp = (attribute: string, alias: string = ''): string => + new Query( + 'varSamp', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static bitAnd = (attribute: string, alias: string = ''): string => + new Query( + 'bitAnd', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static bitOr = (attribute: string, alias: string = ''): string => + new Query( + 'bitOr', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + static bitXor = (attribute: string, alias: string = ''): string => + new Query( + 'bitXor', + attribute, + alias !== '' ? alias : undefined, + ).toString(); + + /** + * Group results by attributes. + */ + static groupBy = (attributes: string[]): string => + new Query('groupBy', undefined, attributes).toString(); + + /** + * Filter grouped results. `queries` are nested like `and`/`or`. + */ + static having = (queries: string[]): string => + new Query( + 'having', + undefined, + queries.map((query) => JSONbig.parse(query)), + ).toString(); + + static distinct = (): string => new Query('distinct').toString(); + + /** + * Column-to-column join ON condition. + */ + static on = (left: string, right: string, operator: string = '='): string => + new Query('on', undefined, [left, operator, right]).toString(); + + /** + * Inner join. `table` is the joined collection/table ID. + */ + static join = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'join', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + static leftJoin = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'leftJoin', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + static rightJoin = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'rightJoin', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + static fullOuterJoin = ( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string => + Query.createJoin( + 'fullOuterJoin', + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ); + + static crossJoin = (table: string, alias: string = ''): string => + new Query( + 'crossJoin', + table, + alias !== '' ? alias : undefined, + ).toString(); + + static covers = (attribute: string, values: any[]): string => + new Query('covers', attribute, [values]).toString(); + + static notCovers = (attribute: string, values: any[]): string => + new Query('notCovers', attribute, [values]).toString(); + + static spatialEquals = (attribute: string, values: any[]): string => + new Query('spatialEquals', attribute, [values]).toString(); + + static notSpatialEquals = (attribute: string, values: any[]): string => + new Query('notSpatialEquals', attribute, [values]).toString(); + + /** + * Return limit and offset queries for 1-based page pagination. + */ + static page = (page: number, perPage: number = 25): string[] => { + if (page < 1) { + throw new Error('Page must be >= 1, got ' + page); + } + if (perPage < 1) { + throw new Error('Per page must be >= 1, got ' + perPage); + } + return [Query.limit(perPage), Query.offset((page - 1) * perPage)]; + }; + + static builder = (): Query.Builder => new Query.Builder(); + + private static createJoin( + method: string, + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): string { + if (Array.isArray(leftOrAliasOrOn)) { + return Query.nestedJoin(method, table, '', leftOrAliasOrOn); + } + if (Array.isArray(rightOrOn)) { + return Query.nestedJoin(method, table, leftOrAliasOrOn, rightOrOn); + } + return new Query( + method, + table, + alias !== '' + ? [leftOrAliasOrOn, operator, rightOrOn, alias] + : [leftOrAliasOrOn, operator, rightOrOn], + ).toString(); } - for (const query of on) { - values.push(JSONbig.parse(query)); + + private static nestedJoin( + method: string, + table: string, + alias: string, + on: string[], + ): string { + const values: any[] = []; + if (alias !== '') { + values.push(alias); + } + for (const query of on) { + values.push(JSONbig.parse(query)); + } + return new Query(method, table, values).toString(); } - return new Query(method, table, values).toString(); - } } -export type QueryInput = - | string - | Query - | Query.Builder - | QueryInput[]; +export type QueryInput = string | Query | Query.Builder | QueryInput[]; /** * Fluent collector of Query fragments. Call {@link Query.Builder.build} to * produce the `string[]` expected by list/createQuery methods. */ export namespace Query { - export class Builder { - private readonly queries: string[] = []; - - build(): string[] { - return [...this.queries]; - } - - equal(attribute: string, value: QueryTypes): this { - return this.push(Query.equal(attribute, value)); - } - - notEqual(attribute: string, value: QueryTypes): this { - return this.push(Query.notEqual(attribute, value)); - } - - lessThan(attribute: string, value: QueryTypes): this { - return this.push(Query.lessThan(attribute, value)); - } - - lessThanEqual(attribute: string, value: QueryTypes): this { - return this.push(Query.lessThanEqual(attribute, value)); - } - - greaterThan(attribute: string, value: QueryTypes): this { - return this.push(Query.greaterThan(attribute, value)); - } - - greaterThanEqual(attribute: string, value: QueryTypes): this { - return this.push(Query.greaterThanEqual(attribute, value)); - } - - search(attribute: string, value: string): this { - return this.push(Query.search(attribute, value)); - } - - isNull(attribute: string): this { - return this.push(Query.isNull(attribute)); - } - - isNotNull(attribute: string): this { - return this.push(Query.isNotNull(attribute)); - } - - between(attribute: string, start: string | number | bigint, end: string | number | bigint): this { - return this.push(Query.between(attribute, start, end)); - } - - startsWith(attribute: string, value: string): this { - return this.push(Query.startsWith(attribute, value)); - } - - endsWith(attribute: string, value: string): this { - return this.push(Query.endsWith(attribute, value)); - } - - select(attributes: string[]): this { - return this.push(Query.select(attributes)); - } - - cursorAfter(documentId: string): this { - return this.push(Query.cursorAfter(documentId)); - } - - cursorBefore(documentId: string): this { - return this.push(Query.cursorBefore(documentId)); - } - - orderAsc(attribute: string): this { - return this.push(Query.orderAsc(attribute)); - } - - orderDesc(attribute: string): this { - return this.push(Query.orderDesc(attribute)); - } - - orderRandom(): this { - return this.push(Query.orderRandom()); - } - - limit(limit: number): this { - return this.push(Query.limit(limit)); - } - - offset(offset: number): this { - return this.push(Query.offset(offset)); - } - - contains(attribute: string, value: string | any[]): this { - return this.push(Query.contains(attribute, value)); - } - - containsAny(attribute: string, value: any[]): this { - return this.push(Query.containsAny(attribute, value)); - } - - containsAll(attribute: string, value: any[]): this { - return this.push(Query.containsAll(attribute, value)); - } - - notContains(attribute: string, value: string | any[]): this { - return this.push(Query.notContains(attribute, value)); - } - - notSearch(attribute: string, value: string): this { - return this.push(Query.notSearch(attribute, value)); - } - - notBetween(attribute: string, start: string | number | bigint, end: string | number | bigint): this { - return this.push(Query.notBetween(attribute, start, end)); - } - - notStartsWith(attribute: string, value: string): this { - return this.push(Query.notStartsWith(attribute, value)); - } - - notEndsWith(attribute: string, value: string): this { - return this.push(Query.notEndsWith(attribute, value)); - } - - regex(attribute: string, pattern: string): this { - return this.push(Query.regex(attribute, pattern)); - } - - exists(attributes: string[]): this { - return this.push(Query.exists(attributes)); - } - - notExists(attributes: string[]): this { - return this.push(Query.notExists(attributes)); - } - - createdBefore(value: string): this { - return this.push(Query.createdBefore(value)); - } - - createdAfter(value: string): this { - return this.push(Query.createdAfter(value)); - } - - createdBetween(start: string, end: string): this { - return this.push(Query.createdBetween(start, end)); - } - - updatedBefore(value: string): this { - return this.push(Query.updatedBefore(value)); - } - - updatedAfter(value: string): this { - return this.push(Query.updatedAfter(value)); - } - - updatedBetween(start: string, end: string): this { - return this.push(Query.updatedBetween(start, end)); - } - - or(queries: string[]): this { - return this.push(Query.or(queries)); - } - - and(queries: string[]): this { - return this.push(Query.and(queries)); - } - - elemMatch(attribute: string, queries: string[]): this { - return this.push(Query.elemMatch(attribute, queries)); - } - - distanceEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceEqual(attribute, values, distance, meters)); - } + export class Builder { + private readonly queries: string[] = []; - distanceNotEqual(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceNotEqual(attribute, values, distance, meters)); - } + build(): string[] { + return [...this.queries]; + } - distanceGreaterThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceGreaterThan(attribute, values, distance, meters)); - } + equal(attribute: string, value: QueryTypes): this { + return this.push(Query.equal(attribute, value)); + } - distanceLessThan(attribute: string, values: any[], distance: number, meters: boolean = true): this { - return this.push(Query.distanceLessThan(attribute, values, distance, meters)); - } - - intersects(attribute: string, values: any[]): this { - return this.push(Query.intersects(attribute, values)); - } - - notIntersects(attribute: string, values: any[]): this { - return this.push(Query.notIntersects(attribute, values)); - } - - crosses(attribute: string, values: any[]): this { - return this.push(Query.crosses(attribute, values)); - } - - notCrosses(attribute: string, values: any[]): this { - return this.push(Query.notCrosses(attribute, values)); - } - - overlaps(attribute: string, values: any[]): this { - return this.push(Query.overlaps(attribute, values)); - } - - notOverlaps(attribute: string, values: any[]): this { - return this.push(Query.notOverlaps(attribute, values)); - } - - touches(attribute: string, values: any[]): this { - return this.push(Query.touches(attribute, values)); - } - - notTouches(attribute: string, values: any[]): this { - return this.push(Query.notTouches(attribute, values)); - } - - count(attribute: string = "*", alias: string = ""): this { - return this.push(Query.count(attribute, alias)); - } - - countDistinct(attribute: string, alias: string = ""): this { - return this.push(Query.countDistinct(attribute, alias)); - } - - sum(attribute: string, alias: string = ""): this { - return this.push(Query.sum(attribute, alias)); - } - - avg(attribute: string, alias: string = ""): this { - return this.push(Query.avg(attribute, alias)); - } - - min(attribute: string, alias: string = ""): this { - return this.push(Query.min(attribute, alias)); - } - - max(attribute: string, alias: string = ""): this { - return this.push(Query.max(attribute, alias)); - } - - stddev(attribute: string, alias: string = ""): this { - return this.push(Query.stddev(attribute, alias)); - } - - stddevPop(attribute: string, alias: string = ""): this { - return this.push(Query.stddevPop(attribute, alias)); - } - - stddevSamp(attribute: string, alias: string = ""): this { - return this.push(Query.stddevSamp(attribute, alias)); - } - - variance(attribute: string, alias: string = ""): this { - return this.push(Query.variance(attribute, alias)); - } - - varPop(attribute: string, alias: string = ""): this { - return this.push(Query.varPop(attribute, alias)); - } - - varSamp(attribute: string, alias: string = ""): this { - return this.push(Query.varSamp(attribute, alias)); - } - - bitAnd(attribute: string, alias: string = ""): this { - return this.push(Query.bitAnd(attribute, alias)); - } - - bitOr(attribute: string, alias: string = ""): this { - return this.push(Query.bitOr(attribute, alias)); - } - - bitXor(attribute: string, alias: string = ""): this { - return this.push(Query.bitXor(attribute, alias)); - } - - groupBy(attributes: string[]): this { - return this.push(Query.groupBy(attributes)); - } - - having(queries: string[]): this { - return this.push(Query.having(queries)); - } - - distinct(): this { - return this.push(Query.distinct()); - } - - join(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - leftJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - rightJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - fullOuterJoin(table: string, leftOrAliasOrOn: string | string[], rightOrOn: string | string[] = "", operator: string = "=", alias: string = ""): this { - return this.push(Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias)); - } - - crossJoin(table: string, alias: string = ""): this { - return this.push(Query.crossJoin(table, alias)); - } - - vectorDot(attribute: string, vector: number[]): this { - return this.push(Query.vectorDot(attribute, vector)); - } - - vectorCosine(attribute: string, vector: number[]): this { - return this.push(Query.vectorCosine(attribute, vector)); - } - - vectorEuclidean(attribute: string, vector: number[]): this { - return this.push(Query.vectorEuclidean(attribute, vector)); - } - - covers(attribute: string, values: any[]): this { - return this.push(Query.covers(attribute, values)); - } - - notCovers(attribute: string, values: any[]): this { - return this.push(Query.notCovers(attribute, values)); - } - - spatialEquals(attribute: string, values: any[]): this { - return this.push(Query.spatialEquals(attribute, values)); - } - - notSpatialEquals(attribute: string, values: any[]): this { - return this.push(Query.notSpatialEquals(attribute, values)); - } - - page(page: number, perPage: number = 25): this { - return this.push(Query.page(page, perPage)); - } - - private push(query: string | string[]): this { - if (Array.isArray(query)) { - this.queries.push(...query); - } else { - this.queries.push(query); + notEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.notEqual(attribute, value)); + } + + lessThan(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThan(attribute, value)); + } + + lessThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.lessThanEqual(attribute, value)); + } + + greaterThan(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThan(attribute, value)); + } + + greaterThanEqual(attribute: string, value: QueryTypes): this { + return this.push(Query.greaterThanEqual(attribute, value)); + } + + search(attribute: string, value: string): this { + return this.push(Query.search(attribute, value)); + } + + isNull(attribute: string): this { + return this.push(Query.isNull(attribute)); + } + + isNotNull(attribute: string): this { + return this.push(Query.isNotNull(attribute)); + } + + between( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ): this { + return this.push(Query.between(attribute, start, end)); + } + + startsWith(attribute: string, value: string): this { + return this.push(Query.startsWith(attribute, value)); + } + + endsWith(attribute: string, value: string): this { + return this.push(Query.endsWith(attribute, value)); + } + + select(attributes: string[]): this { + return this.push(Query.select(attributes)); + } + + cursorAfter(documentId: string): this { + return this.push(Query.cursorAfter(documentId)); + } + + cursorBefore(documentId: string): this { + return this.push(Query.cursorBefore(documentId)); + } + + orderAsc(attribute: string): this { + return this.push(Query.orderAsc(attribute)); + } + + orderDesc(attribute: string): this { + return this.push(Query.orderDesc(attribute)); + } + + orderRandom(): this { + return this.push(Query.orderRandom()); + } + + limit(limit: number): this { + return this.push(Query.limit(limit)); + } + + offset(offset: number): this { + return this.push(Query.offset(offset)); + } + + contains(attribute: string, value: string | any[]): this { + return this.push(Query.contains(attribute, value)); + } + + containsAny(attribute: string, value: any[]): this { + return this.push(Query.containsAny(attribute, value)); + } + + containsAll(attribute: string, value: any[]): this { + return this.push(Query.containsAll(attribute, value)); + } + + notContains(attribute: string, value: string | any[]): this { + return this.push(Query.notContains(attribute, value)); + } + + notSearch(attribute: string, value: string): this { + return this.push(Query.notSearch(attribute, value)); + } + + notBetween( + attribute: string, + start: string | number | bigint, + end: string | number | bigint, + ): this { + return this.push(Query.notBetween(attribute, start, end)); + } + + notStartsWith(attribute: string, value: string): this { + return this.push(Query.notStartsWith(attribute, value)); + } + + notEndsWith(attribute: string, value: string): this { + return this.push(Query.notEndsWith(attribute, value)); + } + + regex(attribute: string, pattern: string): this { + return this.push(Query.regex(attribute, pattern)); + } + + exists(attributes: string[]): this { + return this.push(Query.exists(attributes)); + } + + notExists(attributes: string[]): this { + return this.push(Query.notExists(attributes)); + } + + createdBefore(value: string): this { + return this.push(Query.createdBefore(value)); + } + + createdAfter(value: string): this { + return this.push(Query.createdAfter(value)); + } + + createdBetween(start: string, end: string): this { + return this.push(Query.createdBetween(start, end)); + } + + updatedBefore(value: string): this { + return this.push(Query.updatedBefore(value)); + } + + updatedAfter(value: string): this { + return this.push(Query.updatedAfter(value)); + } + + updatedBetween(start: string, end: string): this { + return this.push(Query.updatedBetween(start, end)); + } + + or(queries: string[]): this { + return this.push(Query.or(queries)); + } + + and(queries: string[]): this { + return this.push(Query.and(queries)); + } + + elemMatch(attribute: string, queries: string[]): this { + return this.push(Query.elemMatch(attribute, queries)); + } + + distanceEqual( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceEqual(attribute, values, distance, meters), + ); + } + + distanceNotEqual( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceNotEqual(attribute, values, distance, meters), + ); + } + + distanceGreaterThan( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceGreaterThan(attribute, values, distance, meters), + ); + } + + distanceLessThan( + attribute: string, + values: any[], + distance: number, + meters: boolean = true, + ): this { + return this.push( + Query.distanceLessThan(attribute, values, distance, meters), + ); + } + + intersects(attribute: string, values: any[]): this { + return this.push(Query.intersects(attribute, values)); + } + + notIntersects(attribute: string, values: any[]): this { + return this.push(Query.notIntersects(attribute, values)); + } + + crosses(attribute: string, values: any[]): this { + return this.push(Query.crosses(attribute, values)); + } + + notCrosses(attribute: string, values: any[]): this { + return this.push(Query.notCrosses(attribute, values)); + } + + overlaps(attribute: string, values: any[]): this { + return this.push(Query.overlaps(attribute, values)); + } + + notOverlaps(attribute: string, values: any[]): this { + return this.push(Query.notOverlaps(attribute, values)); + } + + touches(attribute: string, values: any[]): this { + return this.push(Query.touches(attribute, values)); + } + + notTouches(attribute: string, values: any[]): this { + return this.push(Query.notTouches(attribute, values)); + } + + count(attribute: string = '*', alias: string = ''): this { + return this.push(Query.count(attribute, alias)); + } + + countDistinct(attribute: string, alias: string = ''): this { + return this.push(Query.countDistinct(attribute, alias)); + } + + sum(attribute: string, alias: string = ''): this { + return this.push(Query.sum(attribute, alias)); + } + + avg(attribute: string, alias: string = ''): this { + return this.push(Query.avg(attribute, alias)); + } + + min(attribute: string, alias: string = ''): this { + return this.push(Query.min(attribute, alias)); + } + + max(attribute: string, alias: string = ''): this { + return this.push(Query.max(attribute, alias)); + } + + stddev(attribute: string, alias: string = ''): this { + return this.push(Query.stddev(attribute, alias)); + } + + stddevPop(attribute: string, alias: string = ''): this { + return this.push(Query.stddevPop(attribute, alias)); + } + + stddevSamp(attribute: string, alias: string = ''): this { + return this.push(Query.stddevSamp(attribute, alias)); + } + + variance(attribute: string, alias: string = ''): this { + return this.push(Query.variance(attribute, alias)); + } + + varPop(attribute: string, alias: string = ''): this { + return this.push(Query.varPop(attribute, alias)); + } + + varSamp(attribute: string, alias: string = ''): this { + return this.push(Query.varSamp(attribute, alias)); + } + + bitAnd(attribute: string, alias: string = ''): this { + return this.push(Query.bitAnd(attribute, alias)); + } + + bitOr(attribute: string, alias: string = ''): this { + return this.push(Query.bitOr(attribute, alias)); + } + + bitXor(attribute: string, alias: string = ''): this { + return this.push(Query.bitXor(attribute, alias)); + } + + groupBy(attributes: string[]): this { + return this.push(Query.groupBy(attributes)); + } + + having(queries: string[]): this { + return this.push(Query.having(queries)); + } + + distinct(): this { + return this.push(Query.distinct()); + } + + join( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias), + ); + } + + leftJoin( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.leftJoin( + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ), + ); + } + + rightJoin( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.rightJoin( + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ), + ); + } + + fullOuterJoin( + table: string, + leftOrAliasOrOn: string | string[], + rightOrOn: string | string[] = '', + operator: string = '=', + alias: string = '', + ): this { + return this.push( + Query.fullOuterJoin( + table, + leftOrAliasOrOn, + rightOrOn, + operator, + alias, + ), + ); + } + + crossJoin(table: string, alias: string = ''): this { + return this.push(Query.crossJoin(table, alias)); + } + + vectorDot(attribute: string, vector: number[]): this { + return this.push(Query.vectorDot(attribute, vector)); + } + + vectorCosine(attribute: string, vector: number[]): this { + return this.push(Query.vectorCosine(attribute, vector)); + } + + vectorEuclidean(attribute: string, vector: number[]): this { + return this.push(Query.vectorEuclidean(attribute, vector)); + } + + covers(attribute: string, values: any[]): this { + return this.push(Query.covers(attribute, values)); + } + + notCovers(attribute: string, values: any[]): this { + return this.push(Query.notCovers(attribute, values)); + } + + spatialEquals(attribute: string, values: any[]): this { + return this.push(Query.spatialEquals(attribute, values)); + } + + notSpatialEquals(attribute: string, values: any[]): this { + return this.push(Query.notSpatialEquals(attribute, values)); + } + + page(page: number, perPage: number = 25): this { + return this.push(Query.page(page, perPage)); + } + + private push(query: string | string[]): this { + if (Array.isArray(query)) { + this.queries.push(...query); + } else { + this.queries.push(query); + } + return this; + } } - return this; - } -} } export function normalizeQueries(queries?: QueryInput | null): string[] { - if (queries == null) { - return []; - } - if (queries instanceof Query.Builder) { - return queries.build(); - } - if (!Array.isArray(queries)) { - if (queries instanceof Query) { - return [queries.toString()]; + if (queries == null) { + return []; } - if (typeof queries === "string") { - return [queries]; + if (queries instanceof Query.Builder) { + return queries.build(); } - return []; - } - - const out: string[] = []; - for (const item of queries) { - out.push(...normalizeQueries(item)); - } - return out; + if (!Array.isArray(queries)) { + if (queries instanceof Query) { + return [queries.toString()]; + } + if (typeof queries === 'string') { + return [queries]; + } + return []; + } + + const out: string[] = []; + for (const item of queries) { + out.push(...normalizeQueries(item)); + } + return out; } function isQueryList(value: unknown[]): boolean { - return value.every( - (item) => - item instanceof Query.Builder || - item instanceof Query || - typeof item === "string" || - (Array.isArray(item) && isQueryList(item)) - ); + return value.every( + (item) => + item instanceof Query.Builder || + item instanceof Query || + typeof item === 'string' || + (Array.isArray(item) && isQueryList(item)), + ); } export function flattenParam(value: unknown): unknown { - if (value instanceof Query.Builder || value instanceof Query) { - return normalizeQueries(value as QueryInput); - } - if (Array.isArray(value) && isQueryList(value)) { - return normalizeQueries(value as QueryInput); - } - return value; + if (value instanceof Query.Builder || value instanceof Query) { + return normalizeQueries(value as QueryInput); + } + if (Array.isArray(value) && isQueryList(value)) { + return normalizeQueries(value as QueryInput); + } + return value; } export const QueryBuilder = Query.Builder; diff --git a/templates/rust/src/query/builder.rs.twig b/templates/rust/src/query/builder.rs.twig index 71562caafd..f0e780be64 100644 --- a/templates/rust/src/query/builder.rs.twig +++ b/templates/rust/src/query/builder.rs.twig @@ -10,7 +10,9 @@ pub struct Builder { impl Builder { pub fn new() -> Self { - Self { queries: Vec::new() } + Self { + queries: Vec::new(), + } } pub fn build(&self) -> Vec { @@ -32,7 +34,11 @@ impl Builder { self } - pub fn less_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { + pub fn less_than_equal, V: Into>( + mut self, + attribute: S, + value: V, + ) -> Self { self.queries.push(Query::less_than_equal(attribute, value)); self } @@ -42,8 +48,13 @@ impl Builder { self } - pub fn greater_than_equal, V: Into>(mut self, attribute: S, value: V) -> Self { - self.queries.push(Query::greater_than_equal(attribute, value)); + pub fn greater_than_equal, V: Into>( + mut self, + attribute: S, + value: V, + ) -> Self { + self.queries + .push(Query::greater_than_equal(attribute, value)); self } @@ -57,7 +68,12 @@ impl Builder { self } - pub fn between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { + pub fn between, V: Into>( + mut self, + attribute: S, + start: V, + end: V, + ) -> Self { self.queries.push(Query::between(attribute, start, end)); self } @@ -127,12 +143,20 @@ impl Builder { self } - pub fn contains_any, V: Into>(mut self, attribute: S, values: Vec) -> Self { + pub fn contains_any, V: Into>( + mut self, + attribute: S, + values: Vec, + ) -> Self { self.queries.push(Query::contains_any(attribute, values)); self } - pub fn contains_all, V: Into>(mut self, attribute: S, values: Vec) -> Self { + pub fn contains_all, V: Into>( + mut self, + attribute: S, + values: Vec, + ) -> Self { self.queries.push(Query::contains_all(attribute, values)); self } @@ -142,17 +166,30 @@ impl Builder { self } - pub fn not_between, V: Into>(mut self, attribute: S, start: V, end: V) -> Self { + pub fn not_between, V: Into>( + mut self, + attribute: S, + start: V, + end: V, + ) -> Self { self.queries.push(Query::not_between(attribute, start, end)); self } - pub fn not_starts_with, V: Into>(mut self, attribute: S, value: V) -> Self { + pub fn not_starts_with, V: Into>( + mut self, + attribute: S, + value: V, + ) -> Self { self.queries.push(Query::not_starts_with(attribute, value)); self } - pub fn not_ends_with, V: Into>(mut self, attribute: S, value: V) -> Self { + pub fn not_ends_with, V: Into>( + mut self, + attribute: S, + value: V, + ) -> Self { self.queries.push(Query::not_ends_with(attribute, value)); self } @@ -187,23 +224,54 @@ impl Builder { self } - pub fn distance_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_equal(attribute, values, distance, meters)); + pub fn distance_equal, V: Into>( + mut self, + attribute: S, + values: V, + distance: impl Into, + meters: bool, + ) -> Self { + self.queries + .push(Query::distance_equal(attribute, values, distance, meters)); self } - pub fn distance_not_equal, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_not_equal(attribute, values, distance, meters)); + pub fn distance_not_equal, V: Into>( + mut self, + attribute: S, + values: V, + distance: impl Into, + meters: bool, + ) -> Self { + self.queries.push(Query::distance_not_equal( + attribute, values, distance, meters, + )); self } - pub fn distance_greater_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_greater_than(attribute, values, distance, meters)); + pub fn distance_greater_than, V: Into>( + mut self, + attribute: S, + values: V, + distance: impl Into, + meters: bool, + ) -> Self { + self.queries.push(Query::distance_greater_than( + attribute, values, distance, meters, + )); self } - pub fn distance_less_than, V: Into>(mut self, attribute: S, values: V, distance: impl Into, meters: bool) -> Self { - self.queries.push(Query::distance_less_than(attribute, values, distance, meters)); + pub fn distance_less_than, V: Into>( + mut self, + attribute: S, + values: V, + distance: impl Into, + meters: bool, + ) -> Self { + self.queries.push(Query::distance_less_than( + attribute, values, distance, meters, + )); self } @@ -212,7 +280,11 @@ impl Builder { self } - pub fn not_intersects, V: Into>(mut self, attribute: S, values: V) -> Self { + pub fn not_intersects, V: Into>( + mut self, + attribute: S, + values: V, + ) -> Self { self.queries.push(Query::not_intersects(attribute, values)); self } @@ -232,7 +304,11 @@ impl Builder { self } - pub fn not_overlaps, V: Into>(mut self, attribute: S, values: V) -> Self { + pub fn not_overlaps, V: Into>( + mut self, + attribute: S, + values: V, + ) -> Self { self.queries.push(Query::not_overlaps(attribute, values)); self } @@ -295,7 +371,11 @@ impl Builder { self } - pub fn count_distinct(mut self, attribute: impl Into, alias: impl Into) -> Self { + pub fn count_distinct( + mut self, + attribute: impl Into, + alias: impl Into, + ) -> Self { self.queries.push(Query::count_distinct(attribute, alias)); self } @@ -396,7 +476,8 @@ impl Builder { operator: impl Into, alias: impl Into, ) -> Self { - self.queries.push(Query::join(table, left, right, operator, alias)); + self.queries + .push(Query::join(table, left, right, operator, alias)); self } @@ -408,11 +489,17 @@ impl Builder { operator: impl Into, alias: impl Into, ) -> Self { - self.queries.push(Query::left_join(table, left, right, operator, alias)); + self.queries + .push(Query::left_join(table, left, right, operator, alias)); self } - pub fn left_join_on(mut self, table: impl Into, alias: impl Into, on: I) -> Self + pub fn left_join_on( + mut self, + table: impl Into, + alias: impl Into, + on: I, + ) -> Self where I: IntoIterator, S: AsRef, @@ -429,7 +516,8 @@ impl Builder { operator: impl Into, alias: impl Into, ) -> Self { - self.queries.push(Query::right_join(table, left, right, operator, alias)); + self.queries + .push(Query::right_join(table, left, right, operator, alias)); self } @@ -441,7 +529,8 @@ impl Builder { operator: impl Into, alias: impl Into, ) -> Self { - self.queries.push(Query::full_outer_join(table, left, right, operator, alias)); + self.queries + .push(Query::full_outer_join(table, left, right, operator, alias)); self } @@ -450,21 +539,34 @@ impl Builder { self } - pub fn vector_dot, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + pub fn vector_dot, N: Into>( + mut self, + attribute: S, + vector: Vec, + ) -> Self { let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); self.queries.push(Query::vector_dot(attribute, values)); self } - pub fn vector_cosine, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + pub fn vector_cosine, N: Into>( + mut self, + attribute: S, + vector: Vec, + ) -> Self { let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); self.queries.push(Query::vector_cosine(attribute, values)); self } - pub fn vector_euclidean, N: Into>(mut self, attribute: S, vector: Vec) -> Self { + pub fn vector_euclidean, N: Into>( + mut self, + attribute: S, + vector: Vec, + ) -> Self { let values = Value::Array(vector.into_iter().map(|n| n.into()).collect()); - self.queries.push(Query::vector_euclidean(attribute, values)); + self.queries + .push(Query::vector_euclidean(attribute, values)); self } @@ -478,13 +580,22 @@ impl Builder { self } - pub fn spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { + pub fn spatial_equals, V: Into>( + mut self, + attribute: S, + values: V, + ) -> Self { self.queries.push(Query::spatial_equals(attribute, values)); self } - pub fn not_spatial_equals, V: Into>(mut self, attribute: S, values: V) -> Self { - self.queries.push(Query::not_spatial_equals(attribute, values)); + pub fn not_spatial_equals, V: Into>( + mut self, + attribute: S, + values: V, + ) -> Self { + self.queries + .push(Query::not_spatial_equals(attribute, values)); self } diff --git a/templates/rust/src/query/mod.rs.twig b/templates/rust/src/query/mod.rs.twig index b4a199073f..0a3eea6057 100644 --- a/templates/rust/src/query/mod.rs.twig +++ b/templates/rust/src/query/mod.rs.twig @@ -526,7 +526,10 @@ impl Query { I: IntoIterator, S: Into, { - let values: Vec = attributes.into_iter().map(|s| Value::String(s.into())).collect(); + let values: Vec = attributes + .into_iter() + .map(|s| Value::String(s.into())) + .collect(); Self::new("groupBy".to_string(), None, values) } @@ -630,7 +633,11 @@ impl Query { Self::nested_join("rightJoin", table, alias, on) } - pub fn full_outer_join_on(table: impl Into, alias: impl Into, on: I) -> Self + pub fn full_outer_join_on( + table: impl Into, + alias: impl Into, + on: I, + ) -> Self where I: IntoIterator, S: AsRef, @@ -639,19 +646,35 @@ impl Query { } pub fn covers, V: Into>(attribute: S, values: V) -> Self { - Self::new("covers".to_string(), Some(attribute.into()), vec![values.into()]) + Self::new( + "covers".to_string(), + Some(attribute.into()), + vec![values.into()], + ) } pub fn not_covers, V: Into>(attribute: S, values: V) -> Self { - Self::new("notCovers".to_string(), Some(attribute.into()), vec![values.into()]) + Self::new( + "notCovers".to_string(), + Some(attribute.into()), + vec![values.into()], + ) } pub fn spatial_equals, V: Into>(attribute: S, values: V) -> Self { - Self::new("spatialEquals".to_string(), Some(attribute.into()), vec![values.into()]) + Self::new( + "spatialEquals".to_string(), + Some(attribute.into()), + vec![values.into()], + ) } pub fn not_spatial_equals, V: Into>(attribute: S, values: V) -> Self { - Self::new("notSpatialEquals".to_string(), Some(attribute.into()), vec![values.into()]) + Self::new( + "notSpatialEquals".to_string(), + Some(attribute.into()), + vec![values.into()], + ) } pub fn page(page: u32, per_page: u32) -> Vec { @@ -715,7 +738,12 @@ impl Query { Self::new(method.to_string(), Some(table.into()), values) } - fn nested_join(method: &str, table: impl Into, alias: impl Into, on: I) -> Self + fn nested_join( + method: &str, + table: impl Into, + alias: impl Into, + on: I, + ) -> Self where I: IntoIterator, S: AsRef, @@ -1422,17 +1450,26 @@ mod tests { #[test] fn test_stddev_pop() { - assert_eq!(Query::stddev_pop("price", "sdp").to_value()["method"], "stddevPop"); + assert_eq!( + Query::stddev_pop("price", "sdp").to_value()["method"], + "stddevPop" + ); } #[test] fn test_stddev_samp() { - assert_eq!(Query::stddev_samp("price", "sds").to_value()["method"], "stddevSamp"); + assert_eq!( + Query::stddev_samp("price", "sds").to_value()["method"], + "stddevSamp" + ); } #[test] fn test_variance() { - assert_eq!(Query::variance("price", "var").to_value()["method"], "variance"); + assert_eq!( + Query::variance("price", "var").to_value()["method"], + "variance" + ); } #[test] @@ -1442,12 +1479,18 @@ mod tests { #[test] fn test_var_samp() { - assert_eq!(Query::var_samp("price", "vs").to_value()["method"], "varSamp"); + assert_eq!( + Query::var_samp("price", "vs").to_value()["method"], + "varSamp" + ); } #[test] fn test_bit_and() { - assert_eq!(Query::bit_and("flags", "band").to_value()["method"], "bitAnd"); + assert_eq!( + Query::bit_and("flags", "band").to_value()["method"], + "bitAnd" + ); } #[test] @@ -1457,7 +1500,10 @@ mod tests { #[test] fn test_bit_xor() { - assert_eq!(Query::bit_xor("flags", "bxor").to_value()["method"], "bitXor"); + assert_eq!( + Query::bit_xor("flags", "bxor").to_value()["method"], + "bitXor" + ); } #[test] @@ -1561,7 +1607,11 @@ mod tests { #[test] fn test_not_covers() { - let value = Query::not_covers("location", Value::Array(vec![Value::from(1), Value::from(2)])).to_value(); + let value = Query::not_covers( + "location", + Value::Array(vec![Value::from(1), Value::from(2)]), + ) + .to_value(); assert_eq!(value["method"], "notCovers"); assert_eq!(value["values"][0][0], 1); assert_eq!(value["values"][0][1], 2); @@ -1569,13 +1619,21 @@ mod tests { #[test] fn test_spatial_equals() { - let value = Query::spatial_equals("location", Value::Array(vec![Value::from(1), Value::from(2)])).to_value(); + let value = Query::spatial_equals( + "location", + Value::Array(vec![Value::from(1), Value::from(2)]), + ) + .to_value(); assert_eq!(value["method"], "spatialEquals"); } #[test] fn test_not_spatial_equals() { - let value = Query::not_spatial_equals("location", Value::Array(vec![Value::from(1), Value::from(2)])).to_value(); + let value = Query::not_spatial_equals( + "location", + Value::Array(vec![Value::from(1), Value::from(2)]), + ) + .to_value(); assert_eq!(value["method"], "notSpatialEquals"); } diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 006d5a5a97..dac788b8a7 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -329,7 +329,8 @@ open class Client { headers: [String: String] = [:], params: [String: Any?] = [:] ) throws -> HTTPClientRequest { - let validParams = params + let validParams = + params .mapValues { flatten($0) } .filter { $0.value != nil } diff --git a/templates/swift/Sources/Query.swift.twig b/templates/swift/Sources/Query.swift.twig index 1022011c2b..9a6d1f2fde 100644 --- a/templates/swift/Sources/Query.swift.twig +++ b/templates/swift/Sources/Query.swift.twig @@ -912,7 +912,8 @@ public struct Query: Codable, CustomStringConvertible { } for query in on { if let data = query.data(using: .utf8), - let parsed = try? JSONDecoder().decode(Query.self, from: data) { + let parsed = try? JSONDecoder().decode(Query.self, from: data) + { values.append(parsed) } } diff --git a/templates/swift/Tests/Tests.swift.twig b/templates/swift/Tests/Tests.swift.twig index d8d26f81fe..521568bc09 100644 --- a/templates/swift/Tests/Tests.swift.twig +++ b/templates/swift/Tests/Tests.swift.twig @@ -1,6 +1,6 @@ +import {{ spec.info.title | caseUcfirst }} import Foundation import XCTest -import {{ spec.info.title | caseUcfirst }} class Tests: XCTestCase { func method(_ raw: String) -> String { @@ -89,10 +89,12 @@ class Tests: XCTestCase { } func testNestedLeftJoin() { - let raw = Query.leftJoin("orders", alias: "ord", on: [ - Query.on("$id", "customerId"), - Query.equal("ord.status", value: "paid"), - ]) + let raw = Query.leftJoin( + "orders", alias: "ord", + on: [ + Query.on("$id", "customerId"), + Query.equal("ord.status", value: "paid"), + ]) let object = try! JSONSerialization.jsonObject(with: Data(raw.utf8)) as! [String: Any] XCTAssertEqual(object["method"] as? String, "leftJoin") let values = object["values"] as! [Any] diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index dc72d7e43f..d9049eaaa4 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -103,7 +103,7 @@ type RealtimeRequestSubscribe = { /** * Realtime event response structure with generic payload type. */ -type RealtimeResponseEvent = { +type RealtimeResponseEvent = { /** * List of event names associated with the response. */ @@ -422,18 +422,14 @@ class Client { */ setEndpoint(endpoint: string): this { if (!endpoint || typeof endpoint !== 'string') { - throw new {{spec.info.title | caseUcfirst}}Exception( - 'Endpoint must be a valid string', - ); + throw new {{spec.info.title | caseUcfirst}}Exception('Endpoint must be a valid string'); } if ( !endpoint.startsWith('http://') && !endpoint.startsWith('https://') ) { - throw new {{spec.info.title | caseUcfirst}}Exception( - 'Invalid endpoint URL: ' + endpoint, - ); + throw new {{spec.info.title | caseUcfirst}}Exception('Invalid endpoint URL: ' + endpoint); } this.config.endpoint = endpoint; @@ -453,9 +449,7 @@ class Client { */ setEndpointRealtime(endpointRealtime: string): this { if (!endpointRealtime || typeof endpointRealtime !== 'string') { - throw new {{spec.info.title | caseUcfirst}}Exception( - 'Endpoint must be a valid string', - ); + throw new {{spec.info.title | caseUcfirst}}Exception('Endpoint must be a valid string'); } if ( @@ -856,7 +850,7 @@ class Client { * @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update. * @returns {() => void} Unsubscribes from events. */ - subscribe( + subscribe( channels: | string | string[] @@ -943,7 +937,15 @@ class Client { headers = Object.assign({}, this.headers, headers); - if (typeof window !== 'undefined' && window.localStorage{% if sdk.platform == 'console' %} && this.shouldUseFallbackCredentials(url){% endif %}) { +{%~ if sdk.platform == 'console' %} + if ( + typeof window !== 'undefined' && + window.localStorage && + this.shouldUseFallbackCredentials(url) + ) { +{%~ else %} + if (typeof window !== 'undefined' && window.localStorage) { +{%~ endif %} const cookieFallback = window.localStorage.getItem('cookieFallback'); if (cookieFallback) { @@ -951,7 +953,7 @@ class Client { } } - let options: RequestInit = { + const options: RequestInit = { method, headers, }; @@ -974,7 +976,7 @@ class Client { options.body = JSONbig.stringify(params); break; - case 'multipart/form-data': + case 'multipart/form-data': { const formData = new FormData(); for (const [key, value] of Object.entries(params)) { @@ -992,6 +994,7 @@ class Client { options.body = formData; delete headers['content-type']; break; + } } } @@ -1030,7 +1033,7 @@ class Client { const firstPayload = { ...originalPayload }; firstPayload[fileParam] = new File([firstChunk], file.name); - let response = await this.call( + const response = await this.call( method, url, firstChunkHeaders, @@ -1163,18 +1166,14 @@ class Client { {%~ for clientMethodName, clientMethods in clientMethods %} {%~ for clientMethod in clientMethods %} async {{ clientMethodName | caseCamel }}(): Promise { - return this.call( - '{{ clientMethod.method.value | caseUpper }}', - new URL(this.config.endpoint + '{{ clientMethod.path }}'), - { + return this.call('{{ clientMethod.method.value | caseUpper }}', new URL(this.config.endpoint + '{{ clientMethod.path }}'), { {%~ for key, security in (clientMethod | securityHeaders) %} - '{{ security.name }}': this.config.{{ key | caseLower }}, + '{{ security.name }}': this.config.{{ key | caseLower }}, {%~ endfor %} {%~ for key, header in (clientMethod | methodHeaders) %} - '{{ key }}': '{{ header }}', + {% if key matches '/^[A-Za-z_$][A-Za-z0-9_$]*$/' %}{{ key }}{% else %}'{{ key }}'{% endif %}: '{{ header }}', {%~ endfor %} - }, - ); + }); } {%~ endfor %} {%~ endfor %} @@ -1229,7 +1228,7 @@ class Client { } if (400 <= response.status) { - let responseText = ''; + let responseText; if ( response.headers .get('content-type') @@ -1250,7 +1249,16 @@ class Client { const cookieFallback = response.headers.get('X-Fallback-Cookies'); - if (typeof window !== 'undefined' && window.localStorage && cookieFallback{% if sdk.platform == 'console' %} && this.shouldUseFallbackCredentials(url){% endif %}) { + if ( + typeof window !== 'undefined' && + window.localStorage && +{%~ if sdk.platform == 'console' %} + cookieFallback && + this.shouldUseFallbackCredentials(url) +{%~ else %} + cookieFallback +{%~ endif %} + ) { window.console.warn( '{{spec.info.title | caseUcfirst}} is using localStorage for session management. Increase your security by adding a custom domain as your API endpoint.', ); @@ -1273,7 +1281,7 @@ class Client { let output: Payload = {}; for (const [key, value] of Object.entries(data)) { - let finalKey = prefix ? prefix + '[' + key + ']' : key; + const finalKey = prefix ? prefix + '[' + key + ']' : key; if (Array.isArray(value)) { output = { ...output, ...Client.flatten(value, finalKey) }; } else { diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index 4f74703ddc..c983b6272e 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -212,7 +212,11 @@ export class Realtime { } // URL carries only the project; channels/queries are sent via the subscribe message. +{%~ if language.name == 'ReactNative' %} + const queryParams = `project=${projectId}`; +{%~ else %} let queryParams = `project=${projectId}`; +{%~ endif %} const jwt = this.client.config.jwt; {% if language.name != 'ReactNative' %} @@ -263,9 +267,7 @@ export class Realtime { : new WebSocketCtor(url, undefined, { headers: { Origin: `{{ spec.info.title | caseLower }}-${Platform.OS}://${this.client.config.platform}`, - ...(jwt - ? { 'x-{{ spec.info.title | caseLower }}-jwt': jwt } - : {}), + ...(jwt ? { 'x-{{ spec.info.title | caseLower }}-jwt': jwt } : {}), }, })); {% else %} @@ -330,7 +332,7 @@ export class Realtime { } }); - socket.addEventListener('error', (event: Event) => { + socket.addEventListener('error', (_event: Event) => { if ( connectionId !== this.connectionId || socket !== this.socket @@ -539,10 +541,7 @@ export class Realtime { */ public async subscribe( channels: ( - | string - | Channel - | ActionableChannel - | ResolvedChannel + string | Channel | ActionableChannel | ResolvedChannel )[], callback: (event: RealtimeResponseEvent) => void, queries?: QueryInput, @@ -570,10 +569,7 @@ export class Realtime { */ public async subscribe( channels: ( - | string - | Channel - | ActionableChannel - | ResolvedChannel + string | Channel | ActionableChannel | ResolvedChannel )[], callback: (event: RealtimeResponseEvent) => void, queries?: QueryInput, From 3be98b70d411973a19184aba3e98d4cfc42d774e Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 7 Sep 2026 12:55:32 +1200 Subject: [PATCH 30/42] fix: drop wrapping parens on generated PHP Query constructors Rector NewMethodCallWithoutParenthesesRector fails Validation php (server) on (new Query(...))->__toString() in the generated SDK. --- templates/php/src/Query.php.twig | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/templates/php/src/Query.php.twig b/templates/php/src/Query.php.twig index c10f7e1454..6ecd9d618f 100644 --- a/templates/php/src/Query.php.twig +++ b/templates/php/src/Query.php.twig @@ -503,7 +503,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function count(string $attribute = '*', string $alias = ''): string { - return (new Query('count', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('count', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -511,7 +511,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function countDistinct(string $attribute, string $alias = ''): string { - return (new Query('countDistinct', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('countDistinct', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -519,7 +519,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function sum(string $attribute, string $alias = ''): string { - return (new Query('sum', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('sum', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -527,7 +527,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function avg(string $attribute, string $alias = ''): string { - return (new Query('avg', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('avg', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -535,7 +535,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function min(string $attribute, string $alias = ''): string { - return (new Query('min', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('min', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -543,7 +543,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function max(string $attribute, string $alias = ''): string { - return (new Query('max', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('max', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -551,7 +551,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function stddev(string $attribute, string $alias = ''): string { - return (new Query('stddev', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('stddev', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -559,7 +559,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function stddevPop(string $attribute, string $alias = ''): string { - return (new Query('stddevPop', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('stddevPop', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -567,7 +567,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function stddevSamp(string $attribute, string $alias = ''): string { - return (new Query('stddevSamp', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('stddevSamp', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -575,7 +575,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function variance(string $attribute, string $alias = ''): string { - return (new Query('variance', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('variance', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -583,7 +583,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function varPop(string $attribute, string $alias = ''): string { - return (new Query('varPop', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('varPop', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -591,7 +591,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function varSamp(string $attribute, string $alias = ''): string { - return (new Query('varSamp', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('varSamp', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -599,7 +599,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function bitAnd(string $attribute, string $alias = ''): string { - return (new Query('bitAnd', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('bitAnd', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -607,7 +607,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function bitOr(string $attribute, string $alias = ''): string { - return (new Query('bitOr', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('bitOr', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -615,7 +615,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function bitXor(string $attribute, string $alias = ''): string { - return (new Query('bitXor', $attribute, $alias !== '' ? $alias : null))->__toString(); + return new Query('bitXor', $attribute, $alias !== '' ? $alias : null)->__toString(); } /** @@ -623,7 +623,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function groupBy(array $attributes): string { - return (new Query('groupBy', null, $attributes))->__toString(); + return new Query('groupBy', null, $attributes)->__toString(); } /** @@ -641,7 +641,7 @@ class Query implements \JsonSerializable, \Stringable } unset($query); - return (new Query('having', null, $queries))->__toString(); + return new Query('having', null, $queries)->__toString(); } /** @@ -649,7 +649,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function distinct(): string { - return (new Query('distinct'))->__toString(); + return new Query('distinct')->__toString(); } /** @@ -657,7 +657,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function on(string $left, string $right, string $operator = '='): string { - return (new Query('on', null, [$left, $operator, $right]))->__toString(); + return new Query('on', null, [$left, $operator, $right])->__toString(); } /** @@ -697,7 +697,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function crossJoin(string $table, string $alias = ''): string { - return (new Query('crossJoin', $table, $alias !== '' ? $alias : null))->__toString(); + return new Query('crossJoin', $table, $alias !== '' ? $alias : null)->__toString(); } /** @@ -705,7 +705,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function covers(string $attribute, array $values): string { - return (new Query('covers', $attribute, [$values]))->__toString(); + return new Query('covers', $attribute, [$values])->__toString(); } /** @@ -713,7 +713,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function notCovers(string $attribute, array $values): string { - return (new Query('notCovers', $attribute, [$values]))->__toString(); + return new Query('notCovers', $attribute, [$values])->__toString(); } /** @@ -721,7 +721,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function spatialEquals(string $attribute, array $values): string { - return (new Query('spatialEquals', $attribute, [$values]))->__toString(); + return new Query('spatialEquals', $attribute, [$values])->__toString(); } /** @@ -729,7 +729,7 @@ class Query implements \JsonSerializable, \Stringable */ public static function notSpatialEquals(string $attribute, array $values): string { - return (new Query('notSpatialEquals', $attribute, [$values]))->__toString(); + return new Query('notSpatialEquals', $attribute, [$values])->__toString(); } /** @@ -784,7 +784,7 @@ class Query implements \JsonSerializable, \Stringable $values[] = \json_decode($query, true); } - return (new Query($method, $table, $values))->__toString(); + return new Query($method, $table, $values)->__toString(); } private static function joinQuery(string $method, string $table, string $left, string $right, string $operator, string $alias): string @@ -794,6 +794,6 @@ class Query implements \JsonSerializable, \Stringable $values[] = $alias; } - return (new Query($method, $table, $values))->__toString(); + return new Query($method, $table, $values)->__toString(); } } From 55de80fd01d6d437e6180b73c844edfc6d42871b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 7 Sep 2026 13:02:14 +1200 Subject: [PATCH 31/42] fix: use array_all for generated PHP string-list check Rector ForeachToArrayAllRector fails Validation php (server) on isStringList() in the generated Client. --- templates/php/src/Client.php.twig | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index e00dcbc7d9..be379eff55 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -434,13 +434,8 @@ class Client if (!\is_array($value) || !\array_is_list($value)) { return false; } - foreach ($value as $item) { - if (!\is_string($item)) { - return false; - } - } - return true; + return array_all($value, fn($item): bool => \is_string($item)); } /** From be56eed11eacdcd26a9037a45d41fa0041a0d4d0 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Mon, 7 Sep 2026 13:05:32 +1200 Subject: [PATCH 32/42] fix: space the generated array_all arrow function for Pint Validation php (server) runs Pint before Rector; fn($item) fails function_declaration. --- templates/php/src/Client.php.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index be379eff55..278cab6383 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -435,7 +435,7 @@ class Client return false; } - return array_all($value, fn($item): bool => \is_string($item)); + return array_all($value, fn ($item): bool => \is_string($item)); } /** From 3c05c5bb56d04692e03cedc49164a0a4cb6b9503 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 9 Sep 2026 22:06:12 +1200 Subject: [PATCH 33/42] test(query): assert query helpers through the client boundary The new query contract printed each helper's rendering in process and compared it against a string, and reported flatten behaviour as a pass/fail label the harness computed for itself. Nothing crossed the client, so a harmless change of representation broke the test while a real regression on the transport path (a dropped array index, a mangled parameter, a builder that never got spread) still passed it. The helpers now travel as the queries parameter of a real listRows call, and the assertion reads them back from the echo the mock server returns, so what is pinned is the request the server received. Query.page and Query.builder feed that list instead of being indexed, and the .NET harness no longer requires Query.Flatten to hand back the same collection instance. Flatten keeps its coverage on the path a caller can reach: every request now carries its queries through the client's flatten before transport, and the languages whose service signature accepts a builder pass an unbuilt one so the server proves it was spread. The generated SDKs' own unit tests still assert the helper directly. Deno's nested-join helper referenced an undefined JSONbig, so the nested ON overload threw at runtime. The Deno suites are absent from the CI matrix, which is why only a run that reaches the server could see it. Co-Authored-By: Claude Opus 5 --- templates/deno/src/query.ts.twig | 2 +- templates/rust/tests/tests.rs | 97 +++++++++----------- tests/e2e/Android16Java17Test.php | 2 +- tests/e2e/Android5Java17Test.php | 2 +- tests/e2e/AppleSwift61Test.php | 1 + tests/e2e/Base.php | 43 +++++++-- tests/e2e/DartBetaTest.php | 2 +- tests/e2e/DartStableTest.php | 2 +- tests/e2e/Deno1193Test.php | 2 +- tests/e2e/Deno1303Test.php | 2 +- tests/e2e/DotNet60Test.php | 2 +- tests/e2e/DotNet80Test.php | 2 +- tests/e2e/DotNet90Test.php | 2 +- tests/e2e/FlutterBetaTest.php | 2 +- tests/e2e/FlutterStableTest.php | 2 +- tests/e2e/Go113Test.php | 2 +- tests/e2e/Go118Test.php | 2 +- tests/e2e/KotlinJava17Test.php | 2 +- tests/e2e/Node18Test.php | 2 +- tests/e2e/Node20Test.php | 2 +- tests/e2e/Node26Test.php | 2 +- tests/e2e/PHP85Test.php | 2 +- tests/e2e/Python310Test.php | 2 +- tests/e2e/Python311Test.php | 2 +- tests/e2e/Python312Test.php | 2 +- tests/e2e/Python313Test.php | 2 +- tests/e2e/Python39Test.php | 2 +- tests/e2e/ReactNativeTest.php | 2 +- tests/e2e/Ruby27Test.php | 2 +- tests/e2e/Ruby30Test.php | 2 +- tests/e2e/Ruby31Test.php | 2 +- tests/e2e/Rust183Test.php | 2 +- tests/e2e/Swift61Test.php | 1 + tests/e2e/Unity2021Test.php | 2 +- tests/e2e/WebChromiumTest.php | 2 +- tests/e2e/WebNodeTest.php | 2 +- tests/e2e/languages/android/Tests.kt | 79 ++++++++--------- tests/e2e/languages/apple/Tests.swift | 72 +++++++-------- tests/e2e/languages/dart/tests.dart | 80 ++++++++--------- tests/e2e/languages/deno/tests.ts | 75 ++++++++-------- tests/e2e/languages/dotnet/Tests.cs | 82 ++++++++--------- tests/e2e/languages/flutter/tests.dart | 80 ++++++++--------- tests/e2e/languages/go-v2/tests.go | 98 +++++++++------------ tests/e2e/languages/go/tests.go | 95 +++++++++----------- tests/e2e/languages/kotlin/Tests.kt | 79 ++++++++--------- tests/e2e/languages/node/test.js | 77 ++++++++-------- tests/e2e/languages/php/test.php | 77 ++++++++-------- tests/e2e/languages/python/tests.py | 75 ++++++++-------- tests/e2e/languages/react-native/browser.js | 75 ++++++++-------- tests/e2e/languages/ruby/tests.rb | 73 ++++++++------- tests/e2e/languages/swift/Tests.swift | 72 +++++++-------- tests/e2e/languages/unity/Tests.cs | 81 ++++++++--------- tests/e2e/languages/web/index.html | 76 ++++++++-------- tests/e2e/languages/web/node.js | 76 ++++++++-------- 54 files changed, 780 insertions(+), 848 deletions(-) diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index 3cade2a6d6..2757508793 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -629,7 +629,7 @@ export class Query { values.push(alias); } for (const query of on) { - values.push(JSONbig.parse(query)); + values.push(JSON.parse(query)); } return new Query(method, table, values).toString(); } diff --git a/templates/rust/tests/tests.rs b/templates/rust/tests/tests.rs index c148f81920..9d46744345 100644 --- a/templates/rust/tests/tests.rs +++ b/templates/rust/tests/tests.rs @@ -156,7 +156,7 @@ async fn test_general_service(client: &Client, string_in_array: &[String]) -> Re let (): () = general.empty().await?; // Test Queries - test_queries(); + test_queries(client).await?; // Test Permission Helpers test_permission_helpers(); @@ -226,7 +226,7 @@ async fn test_large_upload(client: &Client, string_in_array: &[String]) -> Resul Ok(()) } -fn test_queries() { +async fn test_queries(client: &Client) -> Result<(), Box> { println!("{}", Query::equal("released", true)); println!("{}", Query::equal("title", vec!["Spiderman", "Dr. Strange"])); println!("{}", Query::not_equal("title", "Spiderman")); @@ -301,61 +301,46 @@ fn test_queries() { Query::equal("name", "Alice").to_string(), Query::greater_than("age", 18).to_string(), ])); - println!("{}", Query::count("*", "total")); - println!("{}", Query::join("orders", "$id", "customerId", "=", "")); - println!("{}", Query::group_by(vec!["status"])); - println!("{}", Query::distinct()); - println!("{}", Query::covers("location", json!([1, 2]))); - println!("{}", Query::count_distinct("year", "uniqueYears")); - println!("{}", Query::sum("price", "total")); - println!("{}", Query::avg("price", "avgPrice")); - println!("{}", Query::min("price", "lowest")); - println!("{}", Query::max("price", "highest")); - println!("{}", Query::stddev("price", "sd")); - println!("{}", Query::stddev_pop("price", "sdp")); - println!("{}", Query::stddev_samp("price", "sds")); - println!("{}", Query::variance("price", "var")); - println!("{}", Query::var_pop("price", "vp")); - println!("{}", Query::var_samp("price", "vs")); - println!("{}", Query::bit_and("flags", "band")); - println!("{}", Query::bit_or("flags", "bor")); - println!("{}", Query::bit_xor("flags", "bxor")); - println!("{}", Query::having(vec![Query::greater_than("total", 1).to_string()])); - println!("{}", Query::left_join("orders", "$id", "customerId", "=", "ord")); - println!("{}", Query::right_join("orders", "$id", "customerId", "=", "")); - println!("{}", Query::full_outer_join("orders", "$id", "customerId", "=", "")); - println!("{}", Query::cross_join("orders", "ord")); - println!("{}", Query::on("$id", "customerId", "=")); - println!("{}", Query::left_join_on("orders", "ord", vec![ + let general = General::new(client); + let mut queries: Vec = vec![ + Query::count("*", "total").to_string(), + Query::join("orders", "$id", "customerId", "=", "").to_string(), + Query::group_by(vec!["status"]).to_string(), + Query::distinct().to_string(), + Query::covers("location", json!([1, 2])).to_string(), + Query::count_distinct("year", "uniqueYears").to_string(), + Query::sum("price", "total").to_string(), + Query::avg("price", "avgPrice").to_string(), + Query::min("price", "lowest").to_string(), + Query::max("price", "highest").to_string(), + Query::stddev("price", "sd").to_string(), + Query::stddev_pop("price", "sdp").to_string(), + Query::stddev_samp("price", "sds").to_string(), + Query::variance("price", "var").to_string(), + Query::var_pop("price", "vp").to_string(), + Query::var_samp("price", "vs").to_string(), + Query::bit_and("flags", "band").to_string(), + Query::bit_or("flags", "bor").to_string(), + Query::bit_xor("flags", "bxor").to_string(), + Query::having(vec![Query::greater_than("total", 1).to_string()]).to_string(), + Query::left_join("orders", "$id", "customerId", "=", "ord").to_string(), + Query::right_join("orders", "$id", "customerId", "=", "").to_string(), + Query::full_outer_join("orders", "$id", "customerId", "=", "").to_string(), + Query::cross_join("orders", "ord").to_string(), Query::on("$id", "customerId", "=").to_string(), - Query::equal("ord.status", "paid").to_string(), - ])); - println!("{}", Query::not_covers("location", json!([1, 2]))); - println!("{}", Query::spatial_equals("location", json!([1, 2]))); - println!("{}", Query::not_spatial_equals("location", json!([1, 2]))); - let page_queries = Query::page(2, 10); - println!("{}", page_queries[0]); - println!("{}", page_queries[1]); - println!("{}", Query::builder().limit(1).build()[0]); - let built = Query::builder().limit(1).build(); - let nested = json!([built.clone()]); - if Client::flatten_value(nested) == json!(built) { - println!("flatten-builder:ok"); - } else { - println!("flatten-builder:fail"); - } - let geometry = json!([[1, 2], [3, 4]]); - if Client::flatten_value(geometry.clone()) == geometry { - println!("flatten-geometry:ok"); - } else { - println!("flatten-geometry:fail"); - } - let query_list = json!([Query::limit(1).to_string()]); - if Client::flatten_value(query_list.clone()) == query_list { - println!("flatten-list:ok"); - } else { - println!("flatten-list:fail"); - } + Query::left_join_on("orders", "ord", vec![ + Query::on("$id", "customerId", "=").to_string(), + Query::equal("ord.status", "paid").to_string(), + ]).to_string(), + Query::not_covers("location", json!([1, 2])).to_string(), + Query::spatial_equals("location", json!([1, 2])).to_string(), + Query::not_spatial_equals("location", json!([1, 2])).to_string(), + ]; + queries.extend(Query::page(2, 10).iter().map(|query| query.to_string())); + queries.extend(Query::builder().limit(1).build()); + println!("{}", general.list_rows(Some(queries)).await?.result); + + Ok(()) } fn test_permission_helpers() { diff --git a/tests/e2e/Android16Java17Test.php b/tests/e2e/Android16Java17Test.php index e43e5ee25d..8f606fa1ba 100644 --- a/tests/e2e/Android16Java17Test.php +++ b/tests/e2e/Android16Java17Test.php @@ -48,7 +48,7 @@ final class Android16Java17Test extends Base ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Android5Java17Test.php b/tests/e2e/Android5Java17Test.php index 7dd2e6ddf0..5bfa69611b 100644 --- a/tests/e2e/Android5Java17Test.php +++ b/tests/e2e/Android5Java17Test.php @@ -48,7 +48,7 @@ final class Android5Java17Test extends Base ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/AppleSwift61Test.php b/tests/e2e/AppleSwift61Test.php index 9824bf25e4..183da9c0a6 100644 --- a/tests/e2e/AppleSwift61Test.php +++ b/tests/e2e/AppleSwift61Test.php @@ -48,6 +48,7 @@ final class AppleSwift61Test extends Base ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Base.php b/tests/e2e/Base.php index cf2de67d8f..4cd845b3a0 100644 --- a/tests/e2e/Base.php +++ b/tests/e2e/Base.php @@ -206,6 +206,14 @@ 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]}]}', + ]; + + // Each language harness sends these through a real listRows request and prints + // the echo the mock server returns, so the assertion reads the request the + // server received rather than what a helper rendered in process. + protected const QUERY_TRANSPORT_RESPONSE = '__QUERY_TRANSPORT_RESPONSE__'; + + protected const QUERY_TRANSPORT_QUERIES = [ '{"method":"count","attribute":"*","values":["total"]}', '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}', '{"method":"groupBy","values":["status"]}', @@ -240,12 +248,6 @@ abstract class Base extends TestCase '{"method":"limit","values":[1]}', ]; - protected const QUERY_FLATTEN_RESPONSES = [ - 'flatten-builder:ok', - 'flatten-geometry:ok', - 'flatten-list:ok', - ]; - protected const PERMISSION_HELPER_RESPONSES = [ 'read("any")', 'write("user:userid")', @@ -600,6 +602,11 @@ public function testHTTPSuccess(): void $expected = $this->getExpectedSdkHeaders() . '; accept: application/json, text/plain'; } + if ($expected === self::QUERY_TRANSPORT_RESPONSE) { + $this->assertQueriesReachedTheServer($output[$index] ?? null); + continue; + } + // HACK: Swift does not guarantee the order of the JSON parameters if (\str_starts_with((string) $expected, '{')) { $this->assertEquals( @@ -617,6 +624,30 @@ public function testHTTPSuccess(): void } } + private function assertQueriesReachedTheServer(?string $echo): void + { + $received = \json_decode((string) $echo, true); + + $this->assertIsArray( + $received, + "The SDK did not print the query list the mock server echoes back, it printed: {$echo}" + ); + $this->assertCount( + \count(self::QUERY_TRANSPORT_QUERIES), + $received, + 'The server received a different number of queries than the SDK was given' + ); + + foreach (self::QUERY_TRANSPORT_QUERIES as $position => $expected) { + // HACK: Swift does not guarantee the order of the JSON parameters + $this->assertEquals( + \json_decode($expected, true), + \json_decode((string) $received[$position], true), + "Query {$position} did not reach the server as the helper described it" + ); + } + } + private function rmdirRecursive(string $dir): void { if (!\is_dir($dir)) { diff --git a/tests/e2e/DartBetaTest.php b/tests/e2e/DartBetaTest.php index 6efdaa91d9..a09ae90d37 100644 --- a/tests/e2e/DartBetaTest.php +++ b/tests/e2e/DartBetaTest.php @@ -46,7 +46,7 @@ final class DartBetaTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DartStableTest.php b/tests/e2e/DartStableTest.php index 3d78086e88..c0c630f253 100644 --- a/tests/e2e/DartStableTest.php +++ b/tests/e2e/DartStableTest.php @@ -46,7 +46,7 @@ final class DartStableTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Deno1193Test.php b/tests/e2e/Deno1193Test.php index eea02719d8..40706b9fd3 100644 --- a/tests/e2e/Deno1193Test.php +++ b/tests/e2e/Deno1193Test.php @@ -41,7 +41,7 @@ final class Deno1193Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Deno1303Test.php b/tests/e2e/Deno1303Test.php index 61780e8715..fa3ab52dd3 100644 --- a/tests/e2e/Deno1303Test.php +++ b/tests/e2e/Deno1303Test.php @@ -41,7 +41,7 @@ final class Deno1303Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DotNet60Test.php b/tests/e2e/DotNet60Test.php index 597a094c94..3778c699f8 100644 --- a/tests/e2e/DotNet60Test.php +++ b/tests/e2e/DotNet60Test.php @@ -45,7 +45,7 @@ final class DotNet60Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DotNet80Test.php b/tests/e2e/DotNet80Test.php index a8d37c2c31..b7019ade13 100644 --- a/tests/e2e/DotNet80Test.php +++ b/tests/e2e/DotNet80Test.php @@ -45,7 +45,7 @@ final class DotNet80Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/DotNet90Test.php b/tests/e2e/DotNet90Test.php index a030f5b414..e5708f4086 100644 --- a/tests/e2e/DotNet90Test.php +++ b/tests/e2e/DotNet90Test.php @@ -45,7 +45,7 @@ final class DotNet90Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/FlutterBetaTest.php b/tests/e2e/FlutterBetaTest.php index ab87b33e26..58d5a13cda 100644 --- a/tests/e2e/FlutterBetaTest.php +++ b/tests/e2e/FlutterBetaTest.php @@ -47,7 +47,7 @@ final class FlutterBetaTest extends Base ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/FlutterStableTest.php b/tests/e2e/FlutterStableTest.php index e75e162f6b..98002694f2 100644 --- a/tests/e2e/FlutterStableTest.php +++ b/tests/e2e/FlutterStableTest.php @@ -47,7 +47,7 @@ final class FlutterStableTest extends Base ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Go113Test.php b/tests/e2e/Go113Test.php index 3a0b8d4649..95f19bdbf3 100644 --- a/tests/e2e/Go113Test.php +++ b/tests/e2e/Go113Test.php @@ -41,7 +41,7 @@ final class Go113Test extends Base ...Base::DOWNLOAD_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES, diff --git a/tests/e2e/Go118Test.php b/tests/e2e/Go118Test.php index 73cd46b4df..bca00281ec 100644 --- a/tests/e2e/Go118Test.php +++ b/tests/e2e/Go118Test.php @@ -41,7 +41,7 @@ final class Go118Test extends Base ...Base::DOWNLOAD_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES, diff --git a/tests/e2e/KotlinJava17Test.php b/tests/e2e/KotlinJava17Test.php index 1b43766244..85273d12ac 100644 --- a/tests/e2e/KotlinJava17Test.php +++ b/tests/e2e/KotlinJava17Test.php @@ -47,7 +47,7 @@ final class KotlinJava17Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Node18Test.php b/tests/e2e/Node18Test.php index f3427a9924..eb836c2fb5 100644 --- a/tests/e2e/Node18Test.php +++ b/tests/e2e/Node18Test.php @@ -54,7 +54,7 @@ final class Node18Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Node20Test.php b/tests/e2e/Node20Test.php index b65cdf3404..e351c7ac0b 100644 --- a/tests/e2e/Node20Test.php +++ b/tests/e2e/Node20Test.php @@ -54,7 +54,7 @@ final class Node20Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Node26Test.php b/tests/e2e/Node26Test.php index b696365674..bf103446f6 100644 --- a/tests/e2e/Node26Test.php +++ b/tests/e2e/Node26Test.php @@ -54,7 +54,7 @@ final class Node26Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/PHP85Test.php b/tests/e2e/PHP85Test.php index fd5b0746f7..21620042a1 100644 --- a/tests/e2e/PHP85Test.php +++ b/tests/e2e/PHP85Test.php @@ -44,7 +44,7 @@ final class PHP85Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::ADDITIONAL_PROPERTIES_RESPONSES, diff --git a/tests/e2e/Python310Test.php b/tests/e2e/Python310Test.php index 8cfad820d5..649ca4be3d 100644 --- a/tests/e2e/Python310Test.php +++ b/tests/e2e/Python310Test.php @@ -45,7 +45,7 @@ final class Python310Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python311Test.php b/tests/e2e/Python311Test.php index 9e10e062ad..866d289735 100644 --- a/tests/e2e/Python311Test.php +++ b/tests/e2e/Python311Test.php @@ -45,7 +45,7 @@ final class Python311Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python312Test.php b/tests/e2e/Python312Test.php index dfbe363168..b060ee9875 100644 --- a/tests/e2e/Python312Test.php +++ b/tests/e2e/Python312Test.php @@ -45,7 +45,7 @@ final class Python312Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python313Test.php b/tests/e2e/Python313Test.php index d037b349d2..a7b3e07ac7 100644 --- a/tests/e2e/Python313Test.php +++ b/tests/e2e/Python313Test.php @@ -45,7 +45,7 @@ final class Python313Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Python39Test.php b/tests/e2e/Python39Test.php index b152f792b9..06acafa49a 100644 --- a/tests/e2e/Python39Test.php +++ b/tests/e2e/Python39Test.php @@ -45,7 +45,7 @@ final class Python39Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/ReactNativeTest.php b/tests/e2e/ReactNativeTest.php index 7f0c924ddc..95aed558f2 100644 --- a/tests/e2e/ReactNativeTest.php +++ b/tests/e2e/ReactNativeTest.php @@ -54,7 +54,7 @@ final class ReactNativeTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/Ruby27Test.php b/tests/e2e/Ruby27Test.php index 4290bbb137..8f0d4971b8 100644 --- a/tests/e2e/Ruby27Test.php +++ b/tests/e2e/Ruby27Test.php @@ -43,7 +43,7 @@ final class Ruby27Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Ruby30Test.php b/tests/e2e/Ruby30Test.php index e323f3aef1..177bab45f4 100644 --- a/tests/e2e/Ruby30Test.php +++ b/tests/e2e/Ruby30Test.php @@ -43,7 +43,7 @@ final class Ruby30Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Ruby31Test.php b/tests/e2e/Ruby31Test.php index f8bc9ed8ae..ed3f61990c 100644 --- a/tests/e2e/Ruby31Test.php +++ b/tests/e2e/Ruby31Test.php @@ -43,7 +43,7 @@ final class Ruby31Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Rust183Test.php b/tests/e2e/Rust183Test.php index 18bbd2e329..b0c233df94 100644 --- a/tests/e2e/Rust183Test.php +++ b/tests/e2e/Rust183Test.php @@ -38,7 +38,7 @@ final class Rust183Test extends Base ...Base::DOWNLOAD_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES, diff --git a/tests/e2e/Swift61Test.php b/tests/e2e/Swift61Test.php index a93b72b224..85f950bc12 100644 --- a/tests/e2e/Swift61Test.php +++ b/tests/e2e/Swift61Test.php @@ -47,6 +47,7 @@ final class Swift61Test extends Base ...Base::EXCEPTION_RESPONSES, ...Base::OAUTH_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES diff --git a/tests/e2e/Unity2021Test.php b/tests/e2e/Unity2021Test.php index 19e4188311..39dc3f3b2a 100644 --- a/tests/e2e/Unity2021Test.php +++ b/tests/e2e/Unity2021Test.php @@ -102,7 +102,7 @@ public function tearDown(): void ...Base::REALTIME_RESPONSES, ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/WebChromiumTest.php b/tests/e2e/WebChromiumTest.php index 20a7e7950d..dc6c7bd899 100644 --- a/tests/e2e/WebChromiumTest.php +++ b/tests/e2e/WebChromiumTest.php @@ -54,7 +54,7 @@ final class WebChromiumTest extends Base ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/WebNodeTest.php b/tests/e2e/WebNodeTest.php index 446b18f65f..2dcbd042e7 100644 --- a/tests/e2e/WebNodeTest.php +++ b/tests/e2e/WebNodeTest.php @@ -50,7 +50,7 @@ final class WebNodeTest extends Base ...Base::UNION_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, - ...Base::QUERY_FLATTEN_RESPONSES, + Base::QUERY_TRANSPORT_RESPONSE, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, ...Base::CHANNEL_HELPER_RESPONSES, diff --git a/tests/e2e/languages/android/Tests.kt b/tests/e2e/languages/android/Tests.kt index 343925ae92..09dc36189d 100644 --- a/tests/e2e/languages/android/Tests.kt +++ b/tests/e2e/languages/android/Tests.kt @@ -409,48 +409,43 @@ class ServiceTest { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ))) - writeToFile(Query.count("*", "total")) - writeToFile(Query.join("orders", "\$id", "customerId")) - writeToFile(Query.groupBy(listOf("status"))) - writeToFile(Query.distinct()) - writeToFile(Query.covers("location", listOf(1, 2))) - writeToFile(Query.countDistinct("year", "uniqueYears")) - writeToFile(Query.sum("price", "total")) - writeToFile(Query.avg("price", "avgPrice")) - writeToFile(Query.min("price", "lowest")) - writeToFile(Query.max("price", "highest")) - writeToFile(Query.stddev("price", "sd")) - writeToFile(Query.stddevPop("price", "sdp")) - writeToFile(Query.stddevSamp("price", "sds")) - writeToFile(Query.variance("price", "var")) - writeToFile(Query.varPop("price", "vp")) - writeToFile(Query.varSamp("price", "vs")) - writeToFile(Query.bitAnd("flags", "band")) - writeToFile(Query.bitOr("flags", "bor")) - writeToFile(Query.bitXor("flags", "bxor")) - writeToFile(Query.having(listOf(Query.greaterThan("total", 1)))) - writeToFile(Query.leftJoin("orders", "\$id", "customerId", "=", "ord")) - writeToFile(Query.rightJoin("orders", "\$id", "customerId")) - writeToFile(Query.fullOuterJoin("orders", "\$id", "customerId")) - writeToFile(Query.crossJoin("orders", "ord")) - writeToFile(Query.on("\$id", "customerId")) - writeToFile(Query.leftJoin("orders", "ord", listOf( - Query.on("\$id", "customerId"), - Query.equal("ord.status", "paid"), - ))) - writeToFile(Query.notCovers("location", listOf(1, 2))) - writeToFile(Query.spatialEquals("location", listOf(1, 2))) - writeToFile(Query.notSpatialEquals("location", listOf(1, 2))) - val pageQueries = Query.page(2, 10) - writeToFile(pageQueries[0]) - writeToFile(pageQueries[1]) - writeToFile(Query.builder().limit(1).build()[0]) - val flatBuilder = client.flatten(listOf(Query.builder().limit(1))) - writeToFile(if (flatBuilder == listOf(Query.limit(1))) "flatten-builder:ok" else "flatten-builder:fail") - val geometry = listOf(listOf(1, 2), listOf(3, 4)) - writeToFile(if (client.flatten(geometry) == geometry) "flatten-geometry:ok" else "flatten-geometry:fail") - val flatList = client.flatten(listOf(Query.limit(1))) - writeToFile(if (flatList == listOf(Query.limit(1))) "flatten-list:ok" else "flatten-list:fail") + val queryTransport = general.listRows( + listOf( + Query.count("*", "total"), + Query.join("orders", "\$id", "customerId"), + Query.groupBy(listOf("status")), + Query.distinct(), + Query.covers("location", listOf(1, 2)), + Query.countDistinct("year", "uniqueYears"), + Query.sum("price", "total"), + Query.avg("price", "avgPrice"), + Query.min("price", "lowest"), + Query.max("price", "highest"), + Query.stddev("price", "sd"), + Query.stddevPop("price", "sdp"), + Query.stddevSamp("price", "sds"), + Query.variance("price", "var"), + Query.varPop("price", "vp"), + Query.varSamp("price", "vs"), + Query.bitAnd("flags", "band"), + Query.bitOr("flags", "bor"), + Query.bitXor("flags", "bxor"), + Query.having(listOf(Query.greaterThan("total", 1))), + Query.leftJoin("orders", "\$id", "customerId", "=", "ord"), + Query.rightJoin("orders", "\$id", "customerId"), + Query.fullOuterJoin("orders", "\$id", "customerId"), + Query.crossJoin("orders", "ord"), + Query.on("\$id", "customerId"), + Query.leftJoin("orders", "ord", listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), + )), + Query.notCovers("location", listOf(1, 2)), + Query.spatialEquals("location", listOf(1, 2)), + Query.notSpatialEquals("location", listOf(1, 2)), + ) + Query.page(2, 10) + Query.builder().limit(1).build() + ) + writeToFile(queryTransport.result) // 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 292b4ae03f..f18dc65952 100644 --- a/tests/e2e/languages/apple/Tests.swift +++ b/tests/e2e/languages/apple/Tests.swift @@ -386,42 +386,44 @@ class Tests: XCTestCase { Query.equal("name", value: "Alice"), Query.greaterThan("age", value: 18) ])) - print(Query.count("*", alias: "total")) - print(Query.join("orders", left: "$id", right: "customerId")) - print(Query.groupBy(["status"])) - print(Query.distinct()) - print(Query.covers("location", values: [1, 2])) - print(Query.countDistinct("year", alias: "uniqueYears")) - print(Query.sum("price", alias: "total")) - print(Query.avg("price", alias: "avgPrice")) - print(Query.min("price", alias: "lowest")) - print(Query.max("price", alias: "highest")) - print(Query.stddev("price", alias: "sd")) - print(Query.stddevPop("price", alias: "sdp")) - print(Query.stddevSamp("price", alias: "sds")) - print(Query.variance("price", alias: "var")) - print(Query.varPop("price", alias: "vp")) - print(Query.varSamp("price", alias: "vs")) - print(Query.bitAnd("flags", alias: "band")) - print(Query.bitOr("flags", alias: "bor")) - print(Query.bitXor("flags", alias: "bxor")) - print(Query.having([Query.greaterThan("total", value: 1)])) - print(Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")) - print(Query.rightJoin("orders", left: "$id", right: "customerId")) - print(Query.fullOuterJoin("orders", left: "$id", right: "customerId")) - print(Query.crossJoin("orders", alias: "ord")) - print(Query.on("$id", "customerId")) - print(Query.leftJoin("orders", alias: "ord", on: [ + let transportQueries: [String] = [ + Query.count("*", alias: "total"), + Query.join("orders", left: "$id", right: "customerId"), + Query.groupBy(["status"]), + Query.distinct(), + Query.covers("location", values: [1, 2]), + Query.countDistinct("year", alias: "uniqueYears"), + Query.sum("price", alias: "total"), + Query.avg("price", alias: "avgPrice"), + Query.min("price", alias: "lowest"), + Query.max("price", alias: "highest"), + Query.stddev("price", alias: "sd"), + Query.stddevPop("price", alias: "sdp"), + Query.stddevSamp("price", alias: "sds"), + Query.variance("price", alias: "var"), + Query.varPop("price", alias: "vp"), + Query.varSamp("price", alias: "vs"), + Query.bitAnd("flags", alias: "band"), + Query.bitOr("flags", alias: "bor"), + Query.bitXor("flags", alias: "bxor"), + Query.having([Query.greaterThan("total", value: 1)]), + Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord"), + Query.rightJoin("orders", left: "$id", right: "customerId"), + Query.fullOuterJoin("orders", left: "$id", right: "customerId"), + Query.crossJoin("orders", alias: "ord"), Query.on("$id", "customerId"), - Query.equal("ord.status", value: "paid"), - ])) - print(Query.notCovers("location", values: [1, 2])) - print(Query.spatialEquals("location", values: [1, 2])) - print(Query.notSpatialEquals("location", values: [1, 2])) - let pageQueries = Query.page(2, perPage: 10) - print(pageQueries[0]) - print(pageQueries[1]) - print(Query.builder().limit(1).build()[0]) + Query.leftJoin("orders", alias: "ord", on: [ + Query.on("$id", "customerId"), + Query.equal("ord.status", value: "paid"), + ]), + Query.notCovers("location", values: [1, 2]), + Query.spatialEquals("location", values: [1, 2]), + Query.notSpatialEquals("location", values: [1, 2]), + ] + let queryTransport = try await general.listRows( + queries: transportQueries + Query.page(2, perPage: 10) + Query.builder().limit(1).build() + ) + print(queryTransport.result) // 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 6c0b5e6d52..32ef8afb9a 100644 --- a/tests/e2e/languages/dart/tests.dart +++ b/tests/e2e/languages/dart/tests.dart @@ -236,51 +236,43 @@ void main() async { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); - print(Query.count('*', 'total')); - print(Query.join('orders', '\$id', 'customerId')); - print(Query.groupBy(['status'])); - print(Query.distinct()); - print(Query.covers('location', [1, 2])); - print(Query.countDistinct('year', 'uniqueYears')); - print(Query.sum('price', 'total')); - print(Query.avg('price', 'avgPrice')); - print(Query.min('price', 'lowest')); - print(Query.max('price', 'highest')); - print(Query.stddev('price', 'sd')); - print(Query.stddevPop('price', 'sdp')); - print(Query.stddevSamp('price', 'sds')); - print(Query.variance('price', 'var')); - print(Query.varPop('price', 'vp')); - print(Query.varSamp('price', 'vs')); - print(Query.bitAnd('flags', 'band')); - print(Query.bitOr('flags', 'bor')); - print(Query.bitXor('flags', 'bxor')); - print(Query.having([Query.greaterThan('total', 1)])); - print(Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord')); - print(Query.rightJoin('orders', '\$id', 'customerId')); - print(Query.fullOuterJoin('orders', '\$id', 'customerId')); - print(Query.crossJoin('orders', 'ord')); - print(Query.on('\$id', 'customerId')); - print(Query.leftJoin('orders', 'ord', [ + final queryTransport = await general.listRows(queries: [ + Query.count('*', 'total'), + Query.join('orders', '\$id', 'customerId'), + Query.groupBy(['status']), + Query.distinct(), + Query.covers('location', [1, 2]), + Query.countDistinct('year', 'uniqueYears'), + Query.sum('price', 'total'), + Query.avg('price', 'avgPrice'), + Query.min('price', 'lowest'), + Query.max('price', 'highest'), + Query.stddev('price', 'sd'), + Query.stddevPop('price', 'sdp'), + Query.stddevSamp('price', 'sds'), + Query.variance('price', 'var'), + Query.varPop('price', 'vp'), + Query.varSamp('price', 'vs'), + Query.bitAnd('flags', 'band'), + Query.bitOr('flags', 'bor'), + Query.bitXor('flags', 'bxor'), + Query.having([Query.greaterThan('total', 1)]), + Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord'), + Query.rightJoin('orders', '\$id', 'customerId'), + Query.fullOuterJoin('orders', '\$id', 'customerId'), + Query.crossJoin('orders', 'ord'), Query.on('\$id', 'customerId'), - Query.equal('ord.status', 'paid'), - ])); - print(Query.notCovers('location', [1, 2])); - print(Query.spatialEquals('location', [1, 2])); - print(Query.notSpatialEquals('location', [1, 2])); - final pageQueries = Query.page(2, 10); - print(pageQueries[0]); - print(pageQueries[1]); - print(Query.builder().limit(1).build()[0]); - final flatBuilder = client.flatten([Query.builder().limit(1)]); - print(flatBuilder is List && flatBuilder.length == 1 && flatBuilder[0] == Query.limit(1) ? 'flatten-builder:ok' : 'flatten-builder:fail'); - final geometry = [ - [1, 2], - [3, 4], - ]; - print(client.flatten(geometry).toString() == geometry.toString() ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); - final flatList = client.flatten([Query.limit(1)]); - print(flatList is List && flatList.length == 1 && flatList[0] == Query.limit(1) ? 'flatten-list:ok' : 'flatten-list:fail'); + Query.leftJoin('orders', 'ord', [ + Query.on('\$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ]), + Query.notCovers('location', [1, 2]), + Query.spatialEquals('location', [1, 2]), + Query.notSpatialEquals('location', [1, 2]), + ...Query.page(2, 10), + ...Query.builder().limit(1).build(), + ]); + print(queryTransport.result); // 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 a06e6bde81..1f48e93edc 100644 --- a/tests/e2e/languages/deno/tests.ts +++ b/tests/e2e/languages/deno/tests.ts @@ -9,7 +9,6 @@ async function start() { let Role = appwrite.Role; let ID = appwrite.ID; let Query = appwrite.Query; - let flattenParam = appwrite.flattenParam; let Operator = appwrite.Operator; let Condition = appwrite.Condition; @@ -269,45 +268,43 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); - console.log(Query.count("*", "total")); - console.log(Query.join("orders", "$id", "customerId")); - console.log(Query.groupBy(["status"])); - console.log(Query.distinct()); - console.log(Query.covers("location", [1, 2])); - console.log(Query.countDistinct("year", "uniqueYears")); - console.log(Query.sum("price", "total")); - console.log(Query.avg("price", "avgPrice")); - console.log(Query.min("price", "lowest")); - console.log(Query.max("price", "highest")); - console.log(Query.stddev("price", "sd")); - console.log(Query.stddevPop("price", "sdp")); - console.log(Query.stddevSamp("price", "sds")); - console.log(Query.variance("price", "var")); - console.log(Query.varPop("price", "vp")); - console.log(Query.varSamp("price", "vs")); - console.log(Query.bitAnd("flags", "band")); - console.log(Query.bitOr("flags", "bor")); - console.log(Query.bitXor("flags", "bxor")); - console.log(Query.having([Query.greaterThan("total", 1)])); - console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); - console.log(Query.rightJoin("orders", "$id", "customerId")); - console.log(Query.fullOuterJoin("orders", "$id", "customerId")); - console.log(Query.crossJoin("orders", "ord")); - console.log(Query.on("$id", "customerId")); - console.log(Query.leftJoin("orders", "ord", [ + const queryTransport = await general.listRows([ + Query.count("*", "total"), + Query.join("orders", "$id", "customerId"), + Query.groupBy(["status"]), + Query.distinct(), + Query.covers("location", [1, 2]), + Query.countDistinct("year", "uniqueYears"), + Query.sum("price", "total"), + Query.avg("price", "avgPrice"), + Query.min("price", "lowest"), + Query.max("price", "highest"), + Query.stddev("price", "sd"), + Query.stddevPop("price", "sdp"), + Query.stddevSamp("price", "sds"), + Query.variance("price", "var"), + Query.varPop("price", "vp"), + Query.varSamp("price", "vs"), + Query.bitAnd("flags", "band"), + Query.bitOr("flags", "bor"), + Query.bitXor("flags", "bxor"), + Query.having([Query.greaterThan("total", 1)]), + Query.leftJoin("orders", "$id", "customerId", "=", "ord"), + Query.rightJoin("orders", "$id", "customerId"), + Query.fullOuterJoin("orders", "$id", "customerId"), + Query.crossJoin("orders", "ord"), Query.on("$id", "customerId"), - Query.equal("ord.status", "paid"), - ])); - console.log(Query.notCovers("location", [1, 2])); - console.log(Query.spatialEquals("location", [1, 2])); - console.log(Query.notSpatialEquals("location", [1, 2])); - const pageQueries = Query.page(2, 10); - console.log(pageQueries[0]); - console.log(pageQueries[1]); - console.log(Query.builder().limit(1).build()[0]); - console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? "flatten-builder:ok" : "flatten-builder:fail"); - console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? "flatten-geometry:ok" : "flatten-geometry:fail"); - console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? "flatten-list:ok" : "flatten-list:fail"); + Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ]), + Query.notCovers("location", [1, 2]), + Query.spatialEquals("location", [1, 2]), + Query.notSpatialEquals("location", [1, 2]), + ...Query.page(2, 10), + Query.builder().limit(1), + ]); + console.log(queryTransport.result); // 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 b0a0b4a0ab..9818e42586 100644 --- a/tests/e2e/languages/dotnet/Tests.cs +++ b/tests/e2e/languages/dotnet/Tests.cs @@ -1,8 +1,6 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using Appwrite; @@ -253,49 +251,45 @@ public async Task Test1() Query.Equal("name", "Alice"), Query.GreaterThan("age", 18) })); - TestContext.WriteLine(Query.Count("*", "total")); - TestContext.WriteLine(Query.Join("orders", "$id", "customerId")); - TestContext.WriteLine(Query.GroupBy(new List { "status" })); - TestContext.WriteLine(Query.Distinct()); - TestContext.WriteLine(Query.Covers("location", new List { 1, 2 })); - TestContext.WriteLine(Query.CountDistinct("year", "uniqueYears")); - TestContext.WriteLine(Query.Sum("price", "total")); - TestContext.WriteLine(Query.Avg("price", "avgPrice")); - TestContext.WriteLine(Query.Min("price", "lowest")); - TestContext.WriteLine(Query.Max("price", "highest")); - TestContext.WriteLine(Query.Stddev("price", "sd")); - TestContext.WriteLine(Query.StddevPop("price", "sdp")); - TestContext.WriteLine(Query.StddevSamp("price", "sds")); - TestContext.WriteLine(Query.Variance("price", "var")); - TestContext.WriteLine(Query.VarPop("price", "vp")); - TestContext.WriteLine(Query.VarSamp("price", "vs")); - TestContext.WriteLine(Query.BitAnd("flags", "band")); - TestContext.WriteLine(Query.BitOr("flags", "bor")); - TestContext.WriteLine(Query.BitXor("flags", "bxor")); - TestContext.WriteLine(Query.Having(new List { Query.GreaterThan("total", 1) })); - TestContext.WriteLine(Query.LeftJoin("orders", "$id", "customerId", "=", "ord")); - TestContext.WriteLine(Query.RightJoin("orders", "$id", "customerId")); - TestContext.WriteLine(Query.FullOuterJoin("orders", "$id", "customerId")); - TestContext.WriteLine(Query.CrossJoin("orders", "ord")); - TestContext.WriteLine(Query.On("$id", "customerId")); - TestContext.WriteLine(Query.LeftJoin("orders", "ord", new List { + var transportQueries = new List + { + Query.Count("*", "total"), + Query.Join("orders", "$id", "customerId"), + Query.GroupBy(new List { "status" }), + Query.Distinct(), + Query.Covers("location", new List { 1, 2 }), + Query.CountDistinct("year", "uniqueYears"), + Query.Sum("price", "total"), + Query.Avg("price", "avgPrice"), + Query.Min("price", "lowest"), + Query.Max("price", "highest"), + Query.Stddev("price", "sd"), + Query.StddevPop("price", "sdp"), + Query.StddevSamp("price", "sds"), + Query.Variance("price", "var"), + Query.VarPop("price", "vp"), + Query.VarSamp("price", "vs"), + Query.BitAnd("flags", "band"), + Query.BitOr("flags", "bor"), + Query.BitXor("flags", "bxor"), + Query.Having(new List { Query.GreaterThan("total", 1) }), + Query.LeftJoin("orders", "$id", "customerId", "=", "ord"), + Query.RightJoin("orders", "$id", "customerId"), + Query.FullOuterJoin("orders", "$id", "customerId"), + Query.CrossJoin("orders", "ord"), Query.On("$id", "customerId"), - Query.Equal("ord.status", "paid"), - })); - TestContext.WriteLine(Query.NotCovers("location", new List { 1, 2 })); - TestContext.WriteLine(Query.SpatialEquals("location", new List { 1, 2 })); - TestContext.WriteLine(Query.NotSpatialEquals("location", new List { 1, 2 })); - var pageQueries = Query.Page(2, 10); - TestContext.WriteLine(pageQueries[0]); - TestContext.WriteLine(pageQueries[1]); - TestContext.WriteLine(new Query.Builder().Limit(1).Build()[0]); - var expectedLimit = new List { Query.Limit(1) }; - var flatBuilder = Query.Flatten(new List { new Query.Builder().Limit(1) }) as IEnumerable; - TestContext.WriteLine(flatBuilder != null && expectedLimit.SequenceEqual(flatBuilder.Cast()) ? "flatten-builder:ok" : "flatten-builder:fail"); - var geometry = new List { new List { 1, 2 }, new List { 3, 4 } }; - TestContext.WriteLine(object.ReferenceEquals(Query.Flatten(geometry), geometry) ? "flatten-geometry:ok" : "flatten-geometry:fail"); - var queryList = new List { Query.Limit(1) }; - TestContext.WriteLine(object.ReferenceEquals(Query.Flatten(queryList), queryList) ? "flatten-list:ok" : "flatten-list:fail"); + Query.LeftJoin("orders", "ord", new List { + Query.On("$id", "customerId"), + Query.Equal("ord.status", "paid"), + }), + Query.NotCovers("location", new List { 1, 2 }), + Query.SpatialEquals("location", new List { 1, 2 }), + Query.NotSpatialEquals("location", new List { 1, 2 }), + }; + transportQueries.AddRange(Query.Page(2, 10)); + transportQueries.AddRange(new Query.Builder().Limit(1).Build()); + var queryTransport = await general.ListRows(transportQueries); + TestContext.WriteLine(queryTransport.Result); // 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 51623119b3..8bb74eb326 100644 --- a/tests/e2e/languages/flutter/tests.dart +++ b/tests/e2e/languages/flutter/tests.dart @@ -382,51 +382,43 @@ void main() async { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); - print(Query.count('*', 'total')); - print(Query.join('orders', '\$id', 'customerId')); - print(Query.groupBy(['status'])); - print(Query.distinct()); - print(Query.covers('location', [1, 2])); - print(Query.countDistinct('year', 'uniqueYears')); - print(Query.sum('price', 'total')); - print(Query.avg('price', 'avgPrice')); - print(Query.min('price', 'lowest')); - print(Query.max('price', 'highest')); - print(Query.stddev('price', 'sd')); - print(Query.stddevPop('price', 'sdp')); - print(Query.stddevSamp('price', 'sds')); - print(Query.variance('price', 'var')); - print(Query.varPop('price', 'vp')); - print(Query.varSamp('price', 'vs')); - print(Query.bitAnd('flags', 'band')); - print(Query.bitOr('flags', 'bor')); - print(Query.bitXor('flags', 'bxor')); - print(Query.having([Query.greaterThan('total', 1)])); - print(Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord')); - print(Query.rightJoin('orders', '\$id', 'customerId')); - print(Query.fullOuterJoin('orders', '\$id', 'customerId')); - print(Query.crossJoin('orders', 'ord')); - print(Query.on('\$id', 'customerId')); - print(Query.leftJoin('orders', 'ord', [ + final queryTransport = await general.listRows(queries: [ + Query.count('*', 'total'), + Query.join('orders', '\$id', 'customerId'), + Query.groupBy(['status']), + Query.distinct(), + Query.covers('location', [1, 2]), + Query.countDistinct('year', 'uniqueYears'), + Query.sum('price', 'total'), + Query.avg('price', 'avgPrice'), + Query.min('price', 'lowest'), + Query.max('price', 'highest'), + Query.stddev('price', 'sd'), + Query.stddevPop('price', 'sdp'), + Query.stddevSamp('price', 'sds'), + Query.variance('price', 'var'), + Query.varPop('price', 'vp'), + Query.varSamp('price', 'vs'), + Query.bitAnd('flags', 'band'), + Query.bitOr('flags', 'bor'), + Query.bitXor('flags', 'bxor'), + Query.having([Query.greaterThan('total', 1)]), + Query.leftJoin('orders', '\$id', 'customerId', '=', 'ord'), + Query.rightJoin('orders', '\$id', 'customerId'), + Query.fullOuterJoin('orders', '\$id', 'customerId'), + Query.crossJoin('orders', 'ord'), Query.on('\$id', 'customerId'), - Query.equal('ord.status', 'paid'), - ])); - print(Query.notCovers('location', [1, 2])); - print(Query.spatialEquals('location', [1, 2])); - print(Query.notSpatialEquals('location', [1, 2])); - final pageQueries = Query.page(2, 10); - print(pageQueries[0]); - print(pageQueries[1]); - print(Query.builder().limit(1).build()[0]); - final flatBuilder = client.flatten([Query.builder().limit(1)]); - print(flatBuilder is List && flatBuilder.length == 1 && flatBuilder[0] == Query.limit(1) ? 'flatten-builder:ok' : 'flatten-builder:fail'); - final geometry = [ - [1, 2], - [3, 4], - ]; - print(client.flatten(geometry).toString() == geometry.toString() ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); - final flatList = client.flatten([Query.limit(1)]); - print(flatList is List && flatList.length == 1 && flatList[0] == Query.limit(1) ? 'flatten-list:ok' : 'flatten-list:fail'); + Query.leftJoin('orders', 'ord', [ + Query.on('\$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ]), + Query.notCovers('location', [1, 2]), + Query.spatialEquals('location', [1, 2]), + Query.notSpatialEquals('location', [1, 2]), + ...Query.page(2, 10), + ...Query.builder().limit(1).build(), + ]); + print(queryTransport.result); // 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 25e7975284..1d579405e0 100644 --- a/tests/e2e/languages/go-v2/tests.go +++ b/tests/e2e/languages/go-v2/tests.go @@ -3,7 +3,6 @@ package main import ( "fmt" "path" - "reflect" "time" "github.com/repoowner/reponame/v2/appwrite" @@ -168,7 +167,7 @@ func testGeneralService(client client.Client, stringInArray []string) { general.Empty() // Test Queries - testQueries() + testQueries(client) // Test Permission Helpers testPermissionHelpers() @@ -220,7 +219,7 @@ func testLargeUpload(client client.Client, stringInArray []string) { fmt.Printf("%s\n", response.Result) } -func testQueries() { +func testQueries(client client.Client) { fmt.Println(query.Equal("released", true)) fmt.Println(query.Equal("title", []interface{}{"Spiderman", "Dr. Strange"})) fmt.Println(query.NotEqual("title", "Spiderman")) @@ -304,63 +303,48 @@ func testQueries() { query.Equal("name", "Alice"), query.GreaterThan("age", 18), })) - fmt.Println(query.Count("*", "total")) - fmt.Println(query.Join("orders", "$id", "customerId", "=", "")) - fmt.Println(query.GroupBy([]interface{}{"status"})) - fmt.Println(query.Distinct()) - fmt.Println(query.Covers("location", []interface{}{1, 2})) - fmt.Println(query.CountDistinct("year", "uniqueYears")) - fmt.Println(query.Sum("price", "total")) - fmt.Println(query.Avg("price", "avgPrice")) - fmt.Println(query.Min("price", "lowest")) - fmt.Println(query.Max("price", "highest")) - fmt.Println(query.Stddev("price", "sd")) - fmt.Println(query.StddevPop("price", "sdp")) - fmt.Println(query.StddevSamp("price", "sds")) - fmt.Println(query.Variance("price", "var")) - fmt.Println(query.VarPop("price", "vp")) - fmt.Println(query.VarSamp("price", "vs")) - fmt.Println(query.BitAnd("flags", "band")) - fmt.Println(query.BitOr("flags", "bor")) - fmt.Println(query.BitXor("flags", "bxor")) - fmt.Println(query.Having([]string{query.GreaterThan("total", 1)})) - fmt.Println(query.LeftJoin("orders", "$id", "customerId", "=", "ord")) - fmt.Println(query.RightJoin("orders", "$id", "customerId", "=", "")) - fmt.Println(query.FullOuterJoin("orders", "$id", "customerId", "=", "")) - fmt.Println(query.CrossJoin("orders", "ord")) - fmt.Println(query.On("$id", "customerId", "")) - fmt.Println(query.LeftJoinOn("orders", "ord", []string{ + general := appwrite.NewGeneral(client) + queries := []string{ + query.Count("*", "total"), + query.Join("orders", "$id", "customerId", "=", ""), + query.GroupBy([]interface{}{"status"}), + query.Distinct(), + query.Covers("location", []interface{}{1, 2}), + query.CountDistinct("year", "uniqueYears"), + query.Sum("price", "total"), + query.Avg("price", "avgPrice"), + query.Min("price", "lowest"), + query.Max("price", "highest"), + query.Stddev("price", "sd"), + query.StddevPop("price", "sdp"), + query.StddevSamp("price", "sds"), + query.Variance("price", "var"), + query.VarPop("price", "vp"), + query.VarSamp("price", "vs"), + query.BitAnd("flags", "band"), + query.BitOr("flags", "bor"), + query.BitXor("flags", "bxor"), + query.Having([]string{query.GreaterThan("total", 1)}), + query.LeftJoin("orders", "$id", "customerId", "=", "ord"), + query.RightJoin("orders", "$id", "customerId", "=", ""), + query.FullOuterJoin("orders", "$id", "customerId", "=", ""), + query.CrossJoin("orders", "ord"), query.On("$id", "customerId", ""), - query.Equal("ord.status", "paid"), - })) - fmt.Println(query.NotCovers("location", []interface{}{1, 2})) - fmt.Println(query.SpatialEquals("location", []interface{}{1, 2})) - fmt.Println(query.NotSpatialEquals("location", []interface{}{1, 2})) - pageQueries := query.Page(2, 10) - fmt.Println(pageQueries[0]) - fmt.Println(pageQueries[1]) - fmt.Println(query.NewBuilder().Limit(1).Build()[0]) - flatBuilder := client.Flatten([]interface{}{query.NewBuilder().Limit(1)}) - if got, ok := flatBuilder.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { - fmt.Println("flatten-builder:ok") - } else { - fmt.Println("flatten-builder:fail") - } - geometry := [][]int{ - {1, 2}, - {3, 4}, + query.LeftJoinOn("orders", "ord", []string{ + query.On("$id", "customerId", ""), + query.Equal("ord.status", "paid"), + }), + query.NotCovers("location", []interface{}{1, 2}), + query.SpatialEquals("location", []interface{}{1, 2}), + query.NotSpatialEquals("location", []interface{}{1, 2}), } - if reflect.DeepEqual(client.Flatten(geometry), geometry) { - fmt.Println("flatten-geometry:ok") - } else { - fmt.Println("flatten-geometry:fail") - } - flatList := client.Flatten([]interface{}{query.Limit(1)}) - if got, ok := flatList.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { - fmt.Println("flatten-list:ok") - } else { - fmt.Println("flatten-list:fail") + queries = append(queries, query.Page(2, 10)...) + queries = append(queries, query.NewBuilder().Limit(1).Build()...) + queryTransport, err := general.ListRows(general.WithListRowsQueries(queries)) + if err != nil { + fmt.Printf("general.ListRows => error %v", err) } + fmt.Println(queryTransport.Result) } func testPermissionHelpers() { diff --git a/tests/e2e/languages/go/tests.go b/tests/e2e/languages/go/tests.go index bae6dc378e..75a4cad8b1 100644 --- a/tests/e2e/languages/go/tests.go +++ b/tests/e2e/languages/go/tests.go @@ -3,7 +3,6 @@ package main import ( "fmt" "path" - "reflect" "time" "github.com/repoowner/reponame/appwrite" @@ -168,7 +167,7 @@ func testGeneralService(client client.Client, stringInArray []string) { general.Empty() // Test Queries - testQueries() + testQueries(client) // Test Permission Helpers testPermissionHelpers() @@ -220,7 +219,7 @@ func testLargeUpload(client client.Client, stringInArray []string) { fmt.Printf("%s\n", response.Result) } -func testQueries() { +func testQueries(client client.Client) { fmt.Println(query.Equal("released", true)) fmt.Println(query.Equal("title", []interface{}{"Spiderman", "Dr. Strange"})) fmt.Println(query.NotEqual("title", "Spiderman")) @@ -304,60 +303,48 @@ func testQueries() { query.Equal("name", "Alice"), query.GreaterThan("age", 18), })) - fmt.Println(query.Count("*", "total")) - fmt.Println(query.Join("orders", "$id", "customerId", "=", "")) - fmt.Println(query.GroupBy([]interface{}{"status"})) - fmt.Println(query.Distinct()) - fmt.Println(query.Covers("location", []interface{}{1, 2})) - fmt.Println(query.CountDistinct("year", "uniqueYears")) - fmt.Println(query.Sum("price", "total")) - fmt.Println(query.Avg("price", "avgPrice")) - fmt.Println(query.Min("price", "lowest")) - fmt.Println(query.Max("price", "highest")) - fmt.Println(query.Stddev("price", "sd")) - fmt.Println(query.StddevPop("price", "sdp")) - fmt.Println(query.StddevSamp("price", "sds")) - fmt.Println(query.Variance("price", "var")) - fmt.Println(query.VarPop("price", "vp")) - fmt.Println(query.VarSamp("price", "vs")) - fmt.Println(query.BitAnd("flags", "band")) - fmt.Println(query.BitOr("flags", "bor")) - fmt.Println(query.BitXor("flags", "bxor")) - fmt.Println(query.Having([]string{query.GreaterThan("total", 1)})) - fmt.Println(query.LeftJoin("orders", "$id", "customerId", "=", "ord")) - fmt.Println(query.RightJoin("orders", "$id", "customerId", "=", "")) - fmt.Println(query.FullOuterJoin("orders", "$id", "customerId", "=", "")) - fmt.Println(query.CrossJoin("orders", "ord")) - fmt.Println(query.On("$id", "customerId", "")) - fmt.Println(query.LeftJoinOn("orders", "ord", []string{ + general := appwrite.NewGeneral(client) + queries := []string{ + query.Count("*", "total"), + query.Join("orders", "$id", "customerId", "=", ""), + query.GroupBy([]interface{}{"status"}), + query.Distinct(), + query.Covers("location", []interface{}{1, 2}), + query.CountDistinct("year", "uniqueYears"), + query.Sum("price", "total"), + query.Avg("price", "avgPrice"), + query.Min("price", "lowest"), + query.Max("price", "highest"), + query.Stddev("price", "sd"), + query.StddevPop("price", "sdp"), + query.StddevSamp("price", "sds"), + query.Variance("price", "var"), + query.VarPop("price", "vp"), + query.VarSamp("price", "vs"), + query.BitAnd("flags", "band"), + query.BitOr("flags", "bor"), + query.BitXor("flags", "bxor"), + query.Having([]string{query.GreaterThan("total", 1)}), + query.LeftJoin("orders", "$id", "customerId", "=", "ord"), + query.RightJoin("orders", "$id", "customerId", "=", ""), + query.FullOuterJoin("orders", "$id", "customerId", "=", ""), + query.CrossJoin("orders", "ord"), query.On("$id", "customerId", ""), - query.Equal("ord.status", "paid"), - })) - fmt.Println(query.NotCovers("location", []interface{}{1, 2})) - fmt.Println(query.SpatialEquals("location", []interface{}{1, 2})) - fmt.Println(query.NotSpatialEquals("location", []interface{}{1, 2})) - pageQueries := query.Page(2, 10) - fmt.Println(pageQueries[0]) - fmt.Println(pageQueries[1]) - fmt.Println(query.NewBuilder().Limit(1).Build()[0]) - flatBuilder := client.Flatten([]interface{}{query.NewBuilder().Limit(1)}) - if got, ok := flatBuilder.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { - fmt.Println("flatten-builder:ok") - } else { - fmt.Println("flatten-builder:fail") + query.LeftJoinOn("orders", "ord", []string{ + query.On("$id", "customerId", ""), + query.Equal("ord.status", "paid"), + }), + query.NotCovers("location", []interface{}{1, 2}), + query.SpatialEquals("location", []interface{}{1, 2}), + query.NotSpatialEquals("location", []interface{}{1, 2}), } - geometry := [][]int{{1, 2}, {3, 4}} - if reflect.DeepEqual(client.Flatten(geometry), geometry) { - fmt.Println("flatten-geometry:ok") - } else { - fmt.Println("flatten-geometry:fail") - } - flatList := client.Flatten([]interface{}{query.Limit(1)}) - if got, ok := flatList.([]interface{}); ok && len(got) == 1 && got[0] == query.Limit(1) { - fmt.Println("flatten-list:ok") - } else { - fmt.Println("flatten-list:fail") + queries = append(queries, query.Page(2, 10)...) + queries = append(queries, query.NewBuilder().Limit(1).Build()...) + queryTransport, err := general.ListRows(general.WithListRowsQueries(queries)) + if err != nil { + fmt.Printf("general.ListRows => error %v", err) } + fmt.Println(queryTransport.Result) } func testPermissionHelpers() { diff --git a/tests/e2e/languages/kotlin/Tests.kt b/tests/e2e/languages/kotlin/Tests.kt index ea1020084c..91840c30a7 100644 --- a/tests/e2e/languages/kotlin/Tests.kt +++ b/tests/e2e/languages/kotlin/Tests.kt @@ -270,48 +270,43 @@ class ServiceTest { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ))) - writeToFile(Query.count("*", "total")) - writeToFile(Query.join("orders", "\$id", "customerId")) - writeToFile(Query.groupBy(listOf("status"))) - writeToFile(Query.distinct()) - writeToFile(Query.covers("location", listOf(1, 2))) - writeToFile(Query.countDistinct("year", "uniqueYears")) - writeToFile(Query.sum("price", "total")) - writeToFile(Query.avg("price", "avgPrice")) - writeToFile(Query.min("price", "lowest")) - writeToFile(Query.max("price", "highest")) - writeToFile(Query.stddev("price", "sd")) - writeToFile(Query.stddevPop("price", "sdp")) - writeToFile(Query.stddevSamp("price", "sds")) - writeToFile(Query.variance("price", "var")) - writeToFile(Query.varPop("price", "vp")) - writeToFile(Query.varSamp("price", "vs")) - writeToFile(Query.bitAnd("flags", "band")) - writeToFile(Query.bitOr("flags", "bor")) - writeToFile(Query.bitXor("flags", "bxor")) - writeToFile(Query.having(listOf(Query.greaterThan("total", 1)))) - writeToFile(Query.leftJoin("orders", "\$id", "customerId", "=", "ord")) - writeToFile(Query.rightJoin("orders", "\$id", "customerId")) - writeToFile(Query.fullOuterJoin("orders", "\$id", "customerId")) - writeToFile(Query.crossJoin("orders", "ord")) - writeToFile(Query.on("\$id", "customerId")) - writeToFile(Query.leftJoin("orders", "ord", listOf( - Query.on("\$id", "customerId"), - Query.equal("ord.status", "paid"), - ))) - writeToFile(Query.notCovers("location", listOf(1, 2))) - writeToFile(Query.spatialEquals("location", listOf(1, 2))) - writeToFile(Query.notSpatialEquals("location", listOf(1, 2))) - val pageQueries = Query.page(2, 10) - writeToFile(pageQueries[0]) - writeToFile(pageQueries[1]) - writeToFile(Query.builder().limit(1).build()[0]) - val flatBuilder = client.flatten(listOf(Query.builder().limit(1))) - writeToFile(if (flatBuilder == listOf(Query.limit(1))) "flatten-builder:ok" else "flatten-builder:fail") - val geometry = listOf(listOf(1, 2), listOf(3, 4)) - writeToFile(if (client.flatten(geometry) == geometry) "flatten-geometry:ok" else "flatten-geometry:fail") - val flatList = client.flatten(listOf(Query.limit(1))) - writeToFile(if (flatList == listOf(Query.limit(1))) "flatten-list:ok" else "flatten-list:fail") + val queryTransport = general.listRows( + listOf( + Query.count("*", "total"), + Query.join("orders", "\$id", "customerId"), + Query.groupBy(listOf("status")), + Query.distinct(), + Query.covers("location", listOf(1, 2)), + Query.countDistinct("year", "uniqueYears"), + Query.sum("price", "total"), + Query.avg("price", "avgPrice"), + Query.min("price", "lowest"), + Query.max("price", "highest"), + Query.stddev("price", "sd"), + Query.stddevPop("price", "sdp"), + Query.stddevSamp("price", "sds"), + Query.variance("price", "var"), + Query.varPop("price", "vp"), + Query.varSamp("price", "vs"), + Query.bitAnd("flags", "band"), + Query.bitOr("flags", "bor"), + Query.bitXor("flags", "bxor"), + Query.having(listOf(Query.greaterThan("total", 1))), + Query.leftJoin("orders", "\$id", "customerId", "=", "ord"), + Query.rightJoin("orders", "\$id", "customerId"), + Query.fullOuterJoin("orders", "\$id", "customerId"), + Query.crossJoin("orders", "ord"), + Query.on("\$id", "customerId"), + Query.leftJoin("orders", "ord", listOf( + Query.on("\$id", "customerId"), + Query.equal("ord.status", "paid"), + )), + Query.notCovers("location", listOf(1, 2)), + Query.spatialEquals("location", listOf(1, 2)), + Query.notSpatialEquals("location", listOf(1, 2)), + ) + Query.page(2, 10) + Query.builder().limit(1).build() + ) + writeToFile(queryTransport.result) // 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 b8223bae98..8b46ee0006 100644 --- a/tests/e2e/languages/node/test.js +++ b/tests/e2e/languages/node/test.js @@ -10,8 +10,7 @@ const { Foo, Bar, General, - AppwriteException, - flattenParam, + AppwriteException } = require('./dist/index.js'); const { InputFile } = require('./dist/inputFile.js'); const { readFile } = require('fs/promises'); @@ -362,45 +361,43 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); - console.log(Query.count("*", "total")); - console.log(Query.join("orders", "$id", "customerId")); - console.log(Query.groupBy(["status"])); - console.log(Query.distinct()); - console.log(Query.covers("location", [1, 2])); - console.log(Query.countDistinct("year", "uniqueYears")); - console.log(Query.sum("price", "total")); - console.log(Query.avg("price", "avgPrice")); - console.log(Query.min("price", "lowest")); - console.log(Query.max("price", "highest")); - console.log(Query.stddev("price", "sd")); - console.log(Query.stddevPop("price", "sdp")); - console.log(Query.stddevSamp("price", "sds")); - console.log(Query.variance("price", "var")); - console.log(Query.varPop("price", "vp")); - console.log(Query.varSamp("price", "vs")); - console.log(Query.bitAnd("flags", "band")); - console.log(Query.bitOr("flags", "bor")); - console.log(Query.bitXor("flags", "bxor")); - console.log(Query.having([Query.greaterThan("total", 1)])); - console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); - console.log(Query.rightJoin("orders", "$id", "customerId")); - console.log(Query.fullOuterJoin("orders", "$id", "customerId")); - console.log(Query.crossJoin("orders", "ord")); - console.log(Query.on("$id", "customerId")); - console.log(Query.leftJoin("orders", "ord", [ + const queryTransport = await general.listRows([ + Query.count("*", "total"), + Query.join("orders", "$id", "customerId"), + Query.groupBy(["status"]), + Query.distinct(), + Query.covers("location", [1, 2]), + Query.countDistinct("year", "uniqueYears"), + Query.sum("price", "total"), + Query.avg("price", "avgPrice"), + Query.min("price", "lowest"), + Query.max("price", "highest"), + Query.stddev("price", "sd"), + Query.stddevPop("price", "sdp"), + Query.stddevSamp("price", "sds"), + Query.variance("price", "var"), + Query.varPop("price", "vp"), + Query.varSamp("price", "vs"), + Query.bitAnd("flags", "band"), + Query.bitOr("flags", "bor"), + Query.bitXor("flags", "bxor"), + Query.having([Query.greaterThan("total", 1)]), + Query.leftJoin("orders", "$id", "customerId", "=", "ord"), + Query.rightJoin("orders", "$id", "customerId"), + Query.fullOuterJoin("orders", "$id", "customerId"), + Query.crossJoin("orders", "ord"), Query.on("$id", "customerId"), - Query.equal("ord.status", "paid"), - ])); - console.log(Query.notCovers("location", [1, 2])); - console.log(Query.spatialEquals("location", [1, 2])); - console.log(Query.notSpatialEquals("location", [1, 2])); - const pageQueries = Query.page(2, 10); - console.log(pageQueries[0]); - console.log(pageQueries[1]); - console.log(Query.builder().limit(1).build()[0]); - console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); - console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); - console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); + Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ]), + Query.notCovers("location", [1, 2]), + Query.spatialEquals("location", [1, 2]), + Query.notSpatialEquals("location", [1, 2]), + ...Query.page(2, 10), + Query.builder().limit(1), + ]); + console.log(queryTransport.result); // 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 39a8ba7043..78b86e1135 100644 --- a/tests/e2e/languages/php/test.php +++ b/tests/e2e/languages/php/test.php @@ -340,48 +340,43 @@ public function toArray(): array Query::equal('name', ['Alice']), Query::greaterThan('age', 18) ]) . "\n"; -echo Query::count('*', 'total') . "\n"; -echo Query::join('orders', '$id', 'customerId') . "\n"; -echo Query::groupBy(['status']) . "\n"; -echo Query::distinct() . "\n"; -echo Query::covers('location', [1, 2]) . "\n"; -echo Query::countDistinct('year', 'uniqueYears') . "\n"; -echo Query::sum('price', 'total') . "\n"; -echo Query::avg('price', 'avgPrice') . "\n"; -echo Query::min('price', 'lowest') . "\n"; -echo Query::max('price', 'highest') . "\n"; -echo Query::stddev('price', 'sd') . "\n"; -echo Query::stddevPop('price', 'sdp') . "\n"; -echo Query::stddevSamp('price', 'sds') . "\n"; -echo Query::variance('price', 'var') . "\n"; -echo Query::varPop('price', 'vp') . "\n"; -echo Query::varSamp('price', 'vs') . "\n"; -echo Query::bitAnd('flags', 'band') . "\n"; -echo Query::bitOr('flags', 'bor') . "\n"; -echo Query::bitXor('flags', 'bxor') . "\n"; -echo Query::having([Query::greaterThan('total', 1)]) . "\n"; -echo Query::leftJoin('orders', '$id', 'customerId', '=', 'ord') . "\n"; -echo Query::rightJoin('orders', '$id', 'customerId') . "\n"; -echo Query::fullOuterJoin('orders', '$id', 'customerId') . "\n"; -echo Query::crossJoin('orders', 'ord') . "\n"; -echo Query::on('$id', 'customerId') . "\n"; -echo Query::leftJoin('orders', 'ord', [ +$queryTransport = $general->listRows([ + Query::count('*', 'total'), + Query::join('orders', '$id', 'customerId'), + Query::groupBy(['status']), + Query::distinct(), + Query::covers('location', [1, 2]), + Query::countDistinct('year', 'uniqueYears'), + Query::sum('price', 'total'), + Query::avg('price', 'avgPrice'), + Query::min('price', 'lowest'), + Query::max('price', 'highest'), + Query::stddev('price', 'sd'), + Query::stddevPop('price', 'sdp'), + Query::stddevSamp('price', 'sds'), + Query::variance('price', 'var'), + Query::varPop('price', 'vp'), + Query::varSamp('price', 'vs'), + Query::bitAnd('flags', 'band'), + Query::bitOr('flags', 'bor'), + Query::bitXor('flags', 'bxor'), + Query::having([Query::greaterThan('total', 1)]), + Query::leftJoin('orders', '$id', 'customerId', '=', 'ord'), + Query::rightJoin('orders', '$id', 'customerId'), + Query::fullOuterJoin('orders', '$id', 'customerId'), + Query::crossJoin('orders', 'ord'), Query::on('$id', 'customerId'), - Query::equal('ord.status', 'paid'), -]) . "\n"; -echo Query::notCovers('location', [1, 2]) . "\n"; -echo Query::spatialEquals('location', [1, 2]) . "\n"; -echo Query::notSpatialEquals('location', [1, 2]) . "\n"; -$page = Query::page(2, 10); -echo $page[0] . "\n"; -echo $page[1] . "\n"; -echo Query::builder()->limit(1)->build()[0] . "\n"; -$flatBuilder = Client::flatten([Query::builder()->limit(1)]); -echo ((is_array($flatBuilder) ? array_values($flatBuilder) : []) === [Query::limit(1)] ? 'flatten-builder:ok' : 'flatten-builder:fail') . "\n"; -$geometry = [[1, 2], [3, 4]]; -echo (Client::flatten($geometry) === $geometry ? 'flatten-geometry:ok' : 'flatten-geometry:fail') . "\n"; -$flatList = Client::flatten([Query::limit(1)]); -echo ((is_array($flatList) ? array_values($flatList) : []) === [Query::limit(1)] ? 'flatten-list:ok' : 'flatten-list:fail') . "\n"; + Query::leftJoin('orders', 'ord', [ + Query::on('$id', 'customerId'), + Query::equal('ord.status', 'paid'), + ]), + Query::notCovers('location', [1, 2]), + Query::spatialEquals('location', [1, 2]), + Query::notSpatialEquals('location', [1, 2]), + ...Query::page(2, 10), + Query::builder()->limit(1), +]); +echo $queryTransport->result . "\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 839d986904..d35f4b5038 100644 --- a/tests/e2e/languages/python/tests.py +++ b/tests/e2e/languages/python/tests.py @@ -219,46 +219,43 @@ Query.equal("name", "Alice"), Query.greater_than("age", 18) ])) -print(Query.count("*", "total")) -print(Query.join("orders", "$id", "customerId")) -print(Query.group_by(["status"])) -print(Query.distinct()) -print(Query.covers("location", [1, 2])) -print(Query.count_distinct("year", "uniqueYears")) -print(Query.sum("price", "total")) -print(Query.avg("price", "avgPrice")) -print(Query.min("price", "lowest")) -print(Query.max("price", "highest")) -print(Query.stddev("price", "sd")) -print(Query.stddev_pop("price", "sdp")) -print(Query.stddev_samp("price", "sds")) -print(Query.variance("price", "var")) -print(Query.var_pop("price", "vp")) -print(Query.var_samp("price", "vs")) -print(Query.bit_and("flags", "band")) -print(Query.bit_or("flags", "bor")) -print(Query.bit_xor("flags", "bxor")) -print(Query.having([Query.greater_than("total", 1)])) -print(Query.left_join("orders", "$id", "customerId", "=", "ord")) -print(Query.right_join("orders", "$id", "customerId")) -print(Query.full_outer_join("orders", "$id", "customerId")) -print(Query.cross_join("orders", "ord")) -print(Query.on("$id", "customerId")) -print(Query.left_join("orders", "ord", [ +query_transport = general.list_rows([ + Query.count("*", "total"), + Query.join("orders", "$id", "customerId"), + Query.group_by(["status"]), + Query.distinct(), + Query.covers("location", [1, 2]), + Query.count_distinct("year", "uniqueYears"), + Query.sum("price", "total"), + Query.avg("price", "avgPrice"), + Query.min("price", "lowest"), + Query.max("price", "highest"), + Query.stddev("price", "sd"), + Query.stddev_pop("price", "sdp"), + Query.stddev_samp("price", "sds"), + Query.variance("price", "var"), + Query.var_pop("price", "vp"), + Query.var_samp("price", "vs"), + Query.bit_and("flags", "band"), + Query.bit_or("flags", "bor"), + Query.bit_xor("flags", "bxor"), + Query.having([Query.greater_than("total", 1)]), + Query.left_join("orders", "$id", "customerId", "=", "ord"), + Query.right_join("orders", "$id", "customerId"), + Query.full_outer_join("orders", "$id", "customerId"), + Query.cross_join("orders", "ord"), Query.on("$id", "customerId"), - Query.equal("ord.status", "paid"), -])) -print(Query.not_covers("location", [1, 2])) -print(Query.spatial_equals("location", [1, 2])) -print(Query.not_spatial_equals("location", [1, 2])) -page_queries = Query.page(2, 10) -print(page_queries[0]) -print(page_queries[1]) -print(Query.builder().limit(1).build()[0]) -print('flatten-builder:ok' if Query._flatten([Query.builder().limit(1)]) == [Query.limit(1)] else 'flatten-builder:fail') -geometry = [[1, 2], [3, 4]] -print('flatten-geometry:ok' if Query._flatten(geometry) == geometry else 'flatten-geometry:fail') -print('flatten-list:ok' if Query._flatten([Query.limit(1)]) == [Query.limit(1)] else 'flatten-list:fail') + Query.left_join("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ]), + Query.not_covers("location", [1, 2]), + Query.spatial_equals("location", [1, 2]), + Query.not_spatial_equals("location", [1, 2]), + *Query.page(2, 10), + Query.builder().limit(1), +]) +print(query_transport.result) # 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 c4c3fdff78..423bb69315 100644 --- a/tests/e2e/languages/react-native/browser.js +++ b/tests/e2e/languages/react-native/browser.js @@ -6,7 +6,6 @@ import { AppwriteException, Realtime, Query, - flattenParam, Permission, Role, ID, @@ -316,45 +315,43 @@ import { Query.equal('name', 'Alice'), Query.greaterThan('age', 18), ])); - console.log(Query.count('*', 'total')); - console.log(Query.join('orders', '$id', 'customerId')); - console.log(Query.groupBy(['status'])); - console.log(Query.distinct()); - console.log(Query.covers('location', [1, 2])); - console.log(Query.countDistinct('year', 'uniqueYears')); - console.log(Query.sum('price', 'total')); - console.log(Query.avg('price', 'avgPrice')); - console.log(Query.min('price', 'lowest')); - console.log(Query.max('price', 'highest')); - console.log(Query.stddev('price', 'sd')); - console.log(Query.stddevPop('price', 'sdp')); - console.log(Query.stddevSamp('price', 'sds')); - console.log(Query.variance('price', 'var')); - console.log(Query.varPop('price', 'vp')); - console.log(Query.varSamp('price', 'vs')); - console.log(Query.bitAnd('flags', 'band')); - console.log(Query.bitOr('flags', 'bor')); - console.log(Query.bitXor('flags', 'bxor')); - console.log(Query.having([Query.greaterThan('total', 1)])); - console.log(Query.leftJoin('orders', '$id', 'customerId', '=', 'ord')); - console.log(Query.rightJoin('orders', '$id', 'customerId')); - console.log(Query.fullOuterJoin('orders', '$id', 'customerId')); - console.log(Query.crossJoin('orders', 'ord')); - console.log(Query.on('$id', 'customerId')); - console.log(Query.leftJoin('orders', 'ord', [ + const queryTransport = await general.listRows([ + Query.count('*', 'total'), + Query.join('orders', '$id', 'customerId'), + Query.groupBy(['status']), + Query.distinct(), + Query.covers('location', [1, 2]), + Query.countDistinct('year', 'uniqueYears'), + Query.sum('price', 'total'), + Query.avg('price', 'avgPrice'), + Query.min('price', 'lowest'), + Query.max('price', 'highest'), + Query.stddev('price', 'sd'), + Query.stddevPop('price', 'sdp'), + Query.stddevSamp('price', 'sds'), + Query.variance('price', 'var'), + Query.varPop('price', 'vp'), + Query.varSamp('price', 'vs'), + Query.bitAnd('flags', 'band'), + Query.bitOr('flags', 'bor'), + Query.bitXor('flags', 'bxor'), + Query.having([Query.greaterThan('total', 1)]), + Query.leftJoin('orders', '$id', 'customerId', '=', 'ord'), + Query.rightJoin('orders', '$id', 'customerId'), + Query.fullOuterJoin('orders', '$id', 'customerId'), + Query.crossJoin('orders', 'ord'), Query.on('$id', 'customerId'), - Query.equal('ord.status', 'paid'), - ])); - console.log(Query.notCovers('location', [1, 2])); - console.log(Query.spatialEquals('location', [1, 2])); - console.log(Query.notSpatialEquals('location', [1, 2])); - const pageQueries = Query.page(2, 10); - console.log(pageQueries[0]); - console.log(pageQueries[1]); - console.log(Query.builder().limit(1).build()[0]); - console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); - console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); - console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); + Query.leftJoin('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ]), + Query.notCovers('location', [1, 2]), + Query.spatialEquals('location', [1, 2]), + Query.notSpatialEquals('location', [1, 2]), + ...Query.page(2, 10), + Query.builder().limit(1), + ]); + console.log(queryTransport.result); // 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 665fdfb3fd..8ec900de27 100644 --- a/tests/e2e/languages/ruby/tests.rb +++ b/tests/e2e/languages/ruby/tests.rb @@ -227,46 +227,43 @@ Query.equal("name", "Alice"), Query.greater_than("age", 18) ]) -puts Query.count('*', 'total') -puts Query.join('orders', '$id', 'customerId') -puts Query.group_by(['status']) -puts Query.distinct() -puts Query.covers('location', [1, 2]) -puts Query.count_distinct('year', 'uniqueYears') -puts Query.sum('price', 'total') -puts Query.avg('price', 'avgPrice') -puts Query.min('price', 'lowest') -puts Query.max('price', 'highest') -puts Query.stddev('price', 'sd') -puts Query.stddev_pop('price', 'sdp') -puts Query.stddev_samp('price', 'sds') -puts Query.variance('price', 'var') -puts Query.var_pop('price', 'vp') -puts Query.var_samp('price', 'vs') -puts Query.bit_and('flags', 'band') -puts Query.bit_or('flags', 'bor') -puts Query.bit_xor('flags', 'bxor') -puts Query.having([Query.greater_than('total', 1)]) -puts Query.left_join('orders', '$id', 'customerId', '=', 'ord') -puts Query.right_join('orders', '$id', 'customerId') -puts Query.full_outer_join('orders', '$id', 'customerId') -puts Query.cross_join('orders', 'ord') -puts Query.on('$id', 'customerId') -puts Query.left_join('orders', 'ord', [ +query_transport = general.list_rows(queries: [ + Query.count('*', 'total'), + Query.join('orders', '$id', 'customerId'), + Query.group_by(['status']), + Query.distinct(), + Query.covers('location', [1, 2]), + Query.count_distinct('year', 'uniqueYears'), + Query.sum('price', 'total'), + Query.avg('price', 'avgPrice'), + Query.min('price', 'lowest'), + Query.max('price', 'highest'), + Query.stddev('price', 'sd'), + Query.stddev_pop('price', 'sdp'), + Query.stddev_samp('price', 'sds'), + Query.variance('price', 'var'), + Query.var_pop('price', 'vp'), + Query.var_samp('price', 'vs'), + Query.bit_and('flags', 'band'), + Query.bit_or('flags', 'bor'), + Query.bit_xor('flags', 'bxor'), + Query.having([Query.greater_than('total', 1)]), + Query.left_join('orders', '$id', 'customerId', '=', 'ord'), + Query.right_join('orders', '$id', 'customerId'), + Query.full_outer_join('orders', '$id', 'customerId'), + Query.cross_join('orders', 'ord'), Query.on('$id', 'customerId'), - Query.equal('ord.status', 'paid'), + Query.left_join('orders', 'ord', [ + Query.on('$id', 'customerId'), + Query.equal('ord.status', 'paid'), + ]), + Query.not_covers('location', [1, 2]), + Query.spatial_equals('location', [1, 2]), + Query.not_spatial_equals('location', [1, 2]), + *Query.page(2, 10), + Query.builder.limit(1), ]) -puts Query.not_covers('location', [1, 2]) -puts Query.spatial_equals('location', [1, 2]) -puts Query.not_spatial_equals('location', [1, 2]) -page_queries = Query.page(2, 10) -puts page_queries[0] -puts page_queries[1] -puts Query.builder.limit(1).build[0] -puts(Query._flatten([Query.builder.limit(1)]) == [Query.limit(1)] ? 'flatten-builder:ok' : 'flatten-builder:fail') -geometry = [[1, 2], [3, 4]] -puts(Query._flatten(geometry) == geometry ? 'flatten-geometry:ok' : 'flatten-geometry:fail') -puts(Query._flatten([Query.limit(1)]) == [Query.limit(1)] ? 'flatten-list:ok' : 'flatten-list:fail') +puts query_transport.result # 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 900b4ebcea..4dc37f6fcc 100644 --- a/tests/e2e/languages/swift/Tests.swift +++ b/tests/e2e/languages/swift/Tests.swift @@ -265,42 +265,44 @@ class Tests: XCTestCase { Query.equal("name", value: "Alice"), Query.greaterThan("age", value: 18) ])) - print(Query.count("*", alias: "total")) - print(Query.join("orders", left: "$id", right: "customerId")) - print(Query.groupBy(["status"])) - print(Query.distinct()) - print(Query.covers("location", values: [1, 2])) - print(Query.countDistinct("year", alias: "uniqueYears")) - print(Query.sum("price", alias: "total")) - print(Query.avg("price", alias: "avgPrice")) - print(Query.min("price", alias: "lowest")) - print(Query.max("price", alias: "highest")) - print(Query.stddev("price", alias: "sd")) - print(Query.stddevPop("price", alias: "sdp")) - print(Query.stddevSamp("price", alias: "sds")) - print(Query.variance("price", alias: "var")) - print(Query.varPop("price", alias: "vp")) - print(Query.varSamp("price", alias: "vs")) - print(Query.bitAnd("flags", alias: "band")) - print(Query.bitOr("flags", alias: "bor")) - print(Query.bitXor("flags", alias: "bxor")) - print(Query.having([Query.greaterThan("total", value: 1)])) - print(Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord")) - print(Query.rightJoin("orders", left: "$id", right: "customerId")) - print(Query.fullOuterJoin("orders", left: "$id", right: "customerId")) - print(Query.crossJoin("orders", alias: "ord")) - print(Query.on("$id", "customerId")) - print(Query.leftJoin("orders", alias: "ord", on: [ + let transportQueries: [String] = [ + Query.count("*", alias: "total"), + Query.join("orders", left: "$id", right: "customerId"), + Query.groupBy(["status"]), + Query.distinct(), + Query.covers("location", values: [1, 2]), + Query.countDistinct("year", alias: "uniqueYears"), + Query.sum("price", alias: "total"), + Query.avg("price", alias: "avgPrice"), + Query.min("price", alias: "lowest"), + Query.max("price", alias: "highest"), + Query.stddev("price", alias: "sd"), + Query.stddevPop("price", alias: "sdp"), + Query.stddevSamp("price", alias: "sds"), + Query.variance("price", alias: "var"), + Query.varPop("price", alias: "vp"), + Query.varSamp("price", alias: "vs"), + Query.bitAnd("flags", alias: "band"), + Query.bitOr("flags", alias: "bor"), + Query.bitXor("flags", alias: "bxor"), + Query.having([Query.greaterThan("total", value: 1)]), + Query.leftJoin("orders", left: "$id", right: "customerId", operator: "=", alias: "ord"), + Query.rightJoin("orders", left: "$id", right: "customerId"), + Query.fullOuterJoin("orders", left: "$id", right: "customerId"), + Query.crossJoin("orders", alias: "ord"), Query.on("$id", "customerId"), - Query.equal("ord.status", value: "paid"), - ])) - print(Query.notCovers("location", values: [1, 2])) - print(Query.spatialEquals("location", values: [1, 2])) - print(Query.notSpatialEquals("location", values: [1, 2])) - let pageQueries = Query.page(2, perPage: 10) - print(pageQueries[0]) - print(pageQueries[1]) - print(Query.builder().limit(1).build()[0]) + Query.leftJoin("orders", alias: "ord", on: [ + Query.on("$id", "customerId"), + Query.equal("ord.status", value: "paid"), + ]), + Query.notCovers("location", values: [1, 2]), + Query.spatialEquals("location", values: [1, 2]), + Query.notSpatialEquals("location", values: [1, 2]), + ] + let queryTransport = try await general.listRows( + queries: transportQueries + Query.page(2, perPage: 10) + Query.builder().limit(1).build() + ) + print(queryTransport.result) // 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 987d340578..9346553d0c 100644 --- a/tests/e2e/languages/unity/Tests.cs +++ b/tests/e2e/languages/unity/Tests.cs @@ -1,7 +1,6 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.TestTools; @@ -388,49 +387,45 @@ private async Task RunAsyncTest() Query.Equal("name", "Alice"), Query.GreaterThan("age", 18) })); - LogResult(Query.Count("*", "total")); - LogResult(Query.Join("orders", "$id", "customerId")); - LogResult(Query.GroupBy(new List { "status" })); - LogResult(Query.Distinct()); - LogResult(Query.Covers("location", new List { 1, 2 })); - LogResult(Query.CountDistinct("year", "uniqueYears")); - LogResult(Query.Sum("price", "total")); - LogResult(Query.Avg("price", "avgPrice")); - LogResult(Query.Min("price", "lowest")); - LogResult(Query.Max("price", "highest")); - LogResult(Query.Stddev("price", "sd")); - LogResult(Query.StddevPop("price", "sdp")); - LogResult(Query.StddevSamp("price", "sds")); - LogResult(Query.Variance("price", "var")); - LogResult(Query.VarPop("price", "vp")); - LogResult(Query.VarSamp("price", "vs")); - LogResult(Query.BitAnd("flags", "band")); - LogResult(Query.BitOr("flags", "bor")); - LogResult(Query.BitXor("flags", "bxor")); - LogResult(Query.Having(new List { Query.GreaterThan("total", 1) })); - LogResult(Query.LeftJoin("orders", "$id", "customerId", "=", "ord")); - LogResult(Query.RightJoin("orders", "$id", "customerId")); - LogResult(Query.FullOuterJoin("orders", "$id", "customerId")); - LogResult(Query.CrossJoin("orders", "ord")); - LogResult(Query.On("$id", "customerId")); - LogResult(Query.LeftJoin("orders", "ord", new List { + var transportQueries = new List + { + Query.Count("*", "total"), + Query.Join("orders", "$id", "customerId"), + Query.GroupBy(new List { "status" }), + Query.Distinct(), + Query.Covers("location", new List { 1, 2 }), + Query.CountDistinct("year", "uniqueYears"), + Query.Sum("price", "total"), + Query.Avg("price", "avgPrice"), + Query.Min("price", "lowest"), + Query.Max("price", "highest"), + Query.Stddev("price", "sd"), + Query.StddevPop("price", "sdp"), + Query.StddevSamp("price", "sds"), + Query.Variance("price", "var"), + Query.VarPop("price", "vp"), + Query.VarSamp("price", "vs"), + Query.BitAnd("flags", "band"), + Query.BitOr("flags", "bor"), + Query.BitXor("flags", "bxor"), + Query.Having(new List { Query.GreaterThan("total", 1) }), + Query.LeftJoin("orders", "$id", "customerId", "=", "ord"), + Query.RightJoin("orders", "$id", "customerId"), + Query.FullOuterJoin("orders", "$id", "customerId"), + Query.CrossJoin("orders", "ord"), Query.On("$id", "customerId"), - Query.Equal("ord.status", "paid"), - })); - LogResult(Query.NotCovers("location", new List { 1, 2 })); - LogResult(Query.SpatialEquals("location", new List { 1, 2 })); - LogResult(Query.NotSpatialEquals("location", new List { 1, 2 })); - var pageQueries = Query.Page(2, 10); - LogResult(pageQueries[0]); - LogResult(pageQueries[1]); - LogResult(new Query.Builder().Limit(1).Build()[0]); - var expectedLimit = new List { Query.Limit(1) }; - var flatBuilder = Query.Flatten(new List { new Query.Builder().Limit(1) }) as IEnumerable; - LogResult(flatBuilder != null && expectedLimit.SequenceEqual(flatBuilder.Cast()) ? "flatten-builder:ok" : "flatten-builder:fail"); - var geometry = new List { new List { 1, 2 }, new List { 3, 4 } }; - LogResult(object.ReferenceEquals(Query.Flatten(geometry), geometry) ? "flatten-geometry:ok" : "flatten-geometry:fail"); - var queryList = new List { Query.Limit(1) }; - LogResult(object.ReferenceEquals(Query.Flatten(queryList), queryList) ? "flatten-list:ok" : "flatten-list:fail"); + Query.LeftJoin("orders", "ord", new List { + Query.On("$id", "customerId"), + Query.Equal("ord.status", "paid"), + }), + Query.NotCovers("location", new List { 1, 2 }), + Query.SpatialEquals("location", new List { 1, 2 }), + Query.NotSpatialEquals("location", new List { 1, 2 }), + }; + transportQueries.AddRange(Query.Page(2, 10)); + transportQueries.AddRange(new Query.Builder().Limit(1).Build()); + var queryTransport = await general.ListRows(transportQueries); + LogResult(queryTransport.Result); // 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 2f62b0a731..df984f9dca 100644 --- a/tests/e2e/languages/web/index.html +++ b/tests/e2e/languages/web/index.html @@ -29,7 +29,7 @@ let rtsubFailureFiredAfterUnsubscribe = false; // Init SDK - const { Client, Foo, Bar, General, Realtime, Query, Permission, Role, ID, Channel, Operator, Condition, MockType, flattenParam } = Appwrite; + const { Client, Foo, Bar, General, Realtime, Query, Permission, Role, ID, Channel, Operator, Condition, MockType } = Appwrite; const client = new Client(); const foo = new Foo(client); @@ -450,45 +450,43 @@ Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); - console.log(Query.count("*", "total")); - console.log(Query.join("orders", "$id", "customerId")); - console.log(Query.groupBy(["status"])); - console.log(Query.distinct()); - console.log(Query.covers("location", [1, 2])); - console.log(Query.countDistinct("year", "uniqueYears")); - console.log(Query.sum("price", "total")); - console.log(Query.avg("price", "avgPrice")); - console.log(Query.min("price", "lowest")); - console.log(Query.max("price", "highest")); - console.log(Query.stddev("price", "sd")); - console.log(Query.stddevPop("price", "sdp")); - console.log(Query.stddevSamp("price", "sds")); - console.log(Query.variance("price", "var")); - console.log(Query.varPop("price", "vp")); - console.log(Query.varSamp("price", "vs")); - console.log(Query.bitAnd("flags", "band")); - console.log(Query.bitOr("flags", "bor")); - console.log(Query.bitXor("flags", "bxor")); - console.log(Query.having([Query.greaterThan("total", 1)])); - console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); - console.log(Query.rightJoin("orders", "$id", "customerId")); - console.log(Query.fullOuterJoin("orders", "$id", "customerId")); - console.log(Query.crossJoin("orders", "ord")); - console.log(Query.on("$id", "customerId")); - console.log(Query.leftJoin("orders", "ord", [ + const queryTransport = await general.listRows([ + Query.count("*", "total"), + Query.join("orders", "$id", "customerId"), + Query.groupBy(["status"]), + Query.distinct(), + Query.covers("location", [1, 2]), + Query.countDistinct("year", "uniqueYears"), + Query.sum("price", "total"), + Query.avg("price", "avgPrice"), + Query.min("price", "lowest"), + Query.max("price", "highest"), + Query.stddev("price", "sd"), + Query.stddevPop("price", "sdp"), + Query.stddevSamp("price", "sds"), + Query.variance("price", "var"), + Query.varPop("price", "vp"), + Query.varSamp("price", "vs"), + Query.bitAnd("flags", "band"), + Query.bitOr("flags", "bor"), + Query.bitXor("flags", "bxor"), + Query.having([Query.greaterThan("total", 1)]), + Query.leftJoin("orders", "$id", "customerId", "=", "ord"), + Query.rightJoin("orders", "$id", "customerId"), + Query.fullOuterJoin("orders", "$id", "customerId"), + Query.crossJoin("orders", "ord"), Query.on("$id", "customerId"), - Query.equal("ord.status", "paid"), - ])); - console.log(Query.notCovers("location", [1, 2])); - console.log(Query.spatialEquals("location", [1, 2])); - console.log(Query.notSpatialEquals("location", [1, 2])); - const pageQueries = Query.page(2, 10); - console.log(pageQueries[0]); - console.log(pageQueries[1]); - console.log(Query.builder().limit(1).build()[0]); - console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); - console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); - console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); + Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ]), + Query.notCovers("location", [1, 2]), + Query.spatialEquals("location", [1, 2]), + Query.notSpatialEquals("location", [1, 2]), + ...Query.page(2, 10), + Query.builder().limit(1), + ]); + console.log(queryTransport.result); // 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 ebc64c16f4..e36d4f0c5b 100644 --- a/tests/e2e/languages/web/node.js +++ b/tests/e2e/languages/web/node.js @@ -1,4 +1,4 @@ -const { AppwriteException, Client, Foo, Bar, General, Query, Permission, Role, ID, Channel, Operator, Condition, MockType, flattenParam } = require('./dist/cjs/sdk.js'); +const { AppwriteException, Client, Foo, Bar, General, Query, Permission, Role, ID, Channel, Operator, Condition, MockType } = require('./dist/cjs/sdk.js'); async function start() { let response; @@ -288,45 +288,43 @@ async function start() { Query.equal("name", "Alice"), Query.greaterThan("age", 18) ])); - console.log(Query.count("*", "total")); - console.log(Query.join("orders", "$id", "customerId")); - console.log(Query.groupBy(["status"])); - console.log(Query.distinct()); - console.log(Query.covers("location", [1, 2])); - console.log(Query.countDistinct("year", "uniqueYears")); - console.log(Query.sum("price", "total")); - console.log(Query.avg("price", "avgPrice")); - console.log(Query.min("price", "lowest")); - console.log(Query.max("price", "highest")); - console.log(Query.stddev("price", "sd")); - console.log(Query.stddevPop("price", "sdp")); - console.log(Query.stddevSamp("price", "sds")); - console.log(Query.variance("price", "var")); - console.log(Query.varPop("price", "vp")); - console.log(Query.varSamp("price", "vs")); - console.log(Query.bitAnd("flags", "band")); - console.log(Query.bitOr("flags", "bor")); - console.log(Query.bitXor("flags", "bxor")); - console.log(Query.having([Query.greaterThan("total", 1)])); - console.log(Query.leftJoin("orders", "$id", "customerId", "=", "ord")); - console.log(Query.rightJoin("orders", "$id", "customerId")); - console.log(Query.fullOuterJoin("orders", "$id", "customerId")); - console.log(Query.crossJoin("orders", "ord")); - console.log(Query.on("$id", "customerId")); - console.log(Query.leftJoin("orders", "ord", [ + const queryTransport = await general.listRows([ + Query.count("*", "total"), + Query.join("orders", "$id", "customerId"), + Query.groupBy(["status"]), + Query.distinct(), + Query.covers("location", [1, 2]), + Query.countDistinct("year", "uniqueYears"), + Query.sum("price", "total"), + Query.avg("price", "avgPrice"), + Query.min("price", "lowest"), + Query.max("price", "highest"), + Query.stddev("price", "sd"), + Query.stddevPop("price", "sdp"), + Query.stddevSamp("price", "sds"), + Query.variance("price", "var"), + Query.varPop("price", "vp"), + Query.varSamp("price", "vs"), + Query.bitAnd("flags", "band"), + Query.bitOr("flags", "bor"), + Query.bitXor("flags", "bxor"), + Query.having([Query.greaterThan("total", 1)]), + Query.leftJoin("orders", "$id", "customerId", "=", "ord"), + Query.rightJoin("orders", "$id", "customerId"), + Query.fullOuterJoin("orders", "$id", "customerId"), + Query.crossJoin("orders", "ord"), Query.on("$id", "customerId"), - Query.equal("ord.status", "paid"), - ])); - console.log(Query.notCovers("location", [1, 2])); - console.log(Query.spatialEquals("location", [1, 2])); - console.log(Query.notSpatialEquals("location", [1, 2])); - const pageQueries = Query.page(2, 10); - console.log(pageQueries[0]); - console.log(pageQueries[1]); - console.log(Query.builder().limit(1).build()[0]); - console.log(JSON.stringify(flattenParam([Query.builder().limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-builder:ok' : 'flatten-builder:fail'); - console.log(JSON.stringify(flattenParam([[1, 2], [3, 4]])) === JSON.stringify([[1, 2], [3, 4]]) ? 'flatten-geometry:ok' : 'flatten-geometry:fail'); - console.log(JSON.stringify(flattenParam([Query.limit(1)])) === JSON.stringify([Query.limit(1)]) ? 'flatten-list:ok' : 'flatten-list:fail'); + Query.leftJoin("orders", "ord", [ + Query.on("$id", "customerId"), + Query.equal("ord.status", "paid"), + ]), + Query.notCovers("location", [1, 2]), + Query.spatialEquals("location", [1, 2]), + Query.notSpatialEquals("location", [1, 2]), + ...Query.page(2, 10), + Query.builder().limit(1), + ]); + console.log(queryTransport.result); // Permission & Role helper tests console.log(Permission.read(Role.any())); From 4b82e660a59686a3e547978a7a1a8f2e7e375b71 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 9 Sep 2026 22:25:05 +1200 Subject: [PATCH 34/42] test(query): compare parsed queries in the generated SDK tests The Node, Deno and Python query tests compared each helper against a literal JSON string, so key order, whitespace and number formatting were pinned even though the server accepts any of them. The PHP, Go, Dart, Kotlin, Swift and Rust tests in this repository already decode the query and assert its fields; these three now do the same. Five Deno expectations were describing a shape the SDK has not emitted for a while: isNull and isNotNull carried an empty values array, select put its attributes under attribute, and both cursor helpers put their id there too. Nothing runs the Deno unit tests, so nothing said so. Co-Authored-By: Claude Opus 5 --- templates/deno/test/query.test.ts.twig | 516 +++++++++++++++++------ templates/node/test/query.test.js.twig | 373 +++++++++------- templates/python/test/test_query.py.twig | 173 +++++--- 3 files changed, 711 insertions(+), 351 deletions(-) diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index 7a81a3cc0d..6bbadc6356 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -5,39 +5,41 @@ import {Query, QueryTypes, flattenParam, normalizeQueries} from "../src/query.ts type BasicFilterQueryTest = { description: string; value: QueryTypes; - expectedValues: string; + expectedValues: unknown[]; } +const parsed = (query: string) => JSON.parse(query); + const tests: BasicFilterQueryTest[] = [ { description: 'with a string', value: 's', - expectedValues: '["s"]' + expectedValues: ['s'] }, { description: 'with a integer', value: 1, - expectedValues: '[1]' + expectedValues: [1] }, { description: 'with a double', value: 1.2, - expectedValues: '[1.2]' + expectedValues: [1.2] }, { description: 'with a whole number double', value: 1.0, - expectedValues: '[1]' + expectedValues: [1] }, { description: 'with a bool', value: false, - expectedValues: '[false]' + expectedValues: [false] }, { description: 'with a list', value: ['a', 'b', 'c'], - expectedValues: '["a","b","c"]' + expectedValues: ['a', 'b', 'c'] } ]; @@ -46,8 +48,12 @@ describe('Query', () => { for (const t of tests) { test(t.description, () => assertEquals( - Query.equal("attr", t.value).toString(), - `{"method":"equal","attribute":"attr","values":${t.expectedValues}}`, + parsed(Query.equal("attr", t.value).toString()), + { + method: 'equal', + attribute: 'attr', + values: t.expectedValues, + }, ) ) } @@ -57,8 +63,12 @@ describe('Query', () => { for (const t of tests) { test(t.description, () => assertEquals( - Query.notEqual("attr", t.value).toString(), - `{"method":"notEqual","attribute":"attr","values":${t.expectedValues}}`, + parsed(Query.notEqual("attr", t.value).toString()), + { + method: 'notEqual', + attribute: 'attr', + values: t.expectedValues, + }, ) ) } @@ -68,8 +78,12 @@ describe('Query', () => { for (const t of tests) { test(t.description, () => assertEquals( - Query.lessThan("attr", t.value).toString(), - `{"method":"lessThan","attribute":"attr","values":${t.expectedValues}}`, + parsed(Query.lessThan("attr", t.value).toString()), + { + method: 'lessThan', + attribute: 'attr', + values: t.expectedValues, + }, ) ) } @@ -79,8 +93,12 @@ describe('Query', () => { for (const t of tests) { test(t.description, () => assertEquals( - Query.lessThanEqual("attr", t.value).toString(), - `{"method":"lessThanEqual","attribute":"attr","values":${t.expectedValues}}`, + parsed(Query.lessThanEqual("attr", t.value).toString()), + { + method: 'lessThanEqual', + attribute: 'attr', + values: t.expectedValues, + }, ) ) } @@ -90,8 +108,12 @@ describe('Query', () => { for (const t of tests) { test(t.description, () => assertEquals( - Query.greaterThan("attr", t.value).toString(), - `{"method":"greaterThan","attribute":"attr","values":${t.expectedValues}}`, + parsed(Query.greaterThan("attr", t.value).toString()), + { + method: 'greaterThan', + attribute: 'attr', + values: t.expectedValues, + }, ) ) } @@ -101,131 +123,218 @@ describe('Query', () => { for (const t of tests) { test(t.description, () => assertEquals( - Query.greaterThanEqual("attr", t.value).toString(), - `{"method":"greaterThanEqual","attribute":"attr","values":${t.expectedValues}}`, + parsed(Query.greaterThanEqual("attr", t.value).toString()), + { + method: 'greaterThanEqual', + attribute: 'attr', + values: t.expectedValues, + }, ) ) } }); test('search', () => assertEquals( - Query.search('attr', 'keyword1 keyword2').toString(), - '{"method":"search","attribute":"attr","values":["keyword1 keyword2"]}', + parsed(Query.search('attr', 'keyword1 keyword2').toString()), + { + method: 'search', + attribute: 'attr', + values: ['keyword1 keyword2'], + }, )); test('isNull', () => assertEquals( - Query.isNull('attr').toString(), - `{"method":"isNull","attribute":"attr","values":[]}`, + parsed(Query.isNull('attr').toString()), + { + method: 'isNull', + attribute: 'attr', + }, )); test('isNotNull', () => assertEquals( - Query.isNotNull('attr').toString(), - `{"method":"isNotNull","attribute":"attr","values":[]}`, + parsed(Query.isNotNull('attr').toString()), + { + method: 'isNotNull', + attribute: 'attr', + }, )); describe('between', () => { test('with integers', () => assertEquals( - Query.between('attr', 1, 2).toString(), - `{"method":"between","attribute":"attr","values":[1,2]}`, + parsed(Query.between('attr', 1, 2).toString()), + { + method: 'between', + attribute: 'attr', + values: [1, 2], + }, )); test('with doubles', () => assertEquals( - Query.between('attr', 1.2, 2.2).toString(), - `{"method":"between","attribute":"attr","values":[1.2,2.2]}`, + parsed(Query.between('attr', 1.2, 2.2).toString()), + { + method: 'between', + attribute: 'attr', + values: [1.2, 2.2], + }, )); test('with strings', () => assertEquals( - Query.between('attr', "a", "z").toString(), - `{"method":"between","attribute":"attr","values":["a","z"]}`, + parsed(Query.between('attr', "a", "z").toString()), + { + method: 'between', + attribute: 'attr', + values: ['a', 'z'], + }, )); }); test('select', () => assertEquals( - Query.select(['attr1', 'attr2']).toString(), - `{"method":"select","attribute":["attr1","attr2"]}`, + parsed(Query.select(['attr1', 'attr2']).toString()), + { + method: 'select', + values: ['attr1', 'attr2'], + }, )); test('orderAsc', () => assertEquals( - Query.orderAsc('attr').toString(), - `{"method":"orderAsc","attribute":"attr"}`, + parsed(Query.orderAsc('attr').toString()), + { + method: 'orderAsc', + attribute: 'attr', + }, )); test('orderDesc', () => assertEquals( - Query.orderDesc('attr').toString(), - `{"method":"orderDesc","attribute":"attr"}`, + parsed(Query.orderDesc('attr').toString()), + { + method: 'orderDesc', + attribute: 'attr', + }, )); test('orderRandom', () => assertEquals( - Query.orderRandom().toString(), - `{"method":"orderRandom"}`, + parsed(Query.orderRandom().toString()), + { + method: 'orderRandom', + }, )); test('cursorBefore', () => assertEquals( - Query.cursorBefore('attr').toString(), - `{"method":"cursorBefore","attribute":"attr"}`, + parsed(Query.cursorBefore('attr').toString()), + { + method: 'cursorBefore', + values: ['attr'], + }, )); test('cursorAfter', () => assertEquals( - Query.cursorAfter('attr').toString(), - `{"method":"cursorAfter","attribute":"attr"}`, + parsed(Query.cursorAfter('attr').toString()), + { + method: 'cursorAfter', + values: ['attr'], + }, )); test('limit', () => assertEquals( - Query.limit(1).toString(), - `{"method":"limit","values":[1]}`, + parsed(Query.limit(1).toString()), + { + method: 'limit', + values: [1], + }, )); test('offset', () => assertEquals( - Query.offset(1).toString(), - `{"method":"offset","values":[1]}`, + parsed(Query.offset(1).toString()), + { + method: 'offset', + values: [1], + }, )); test('notContains', () => assertEquals( - Query.notContains('attr', 'value').toString(), - `{"method":"notContains","attribute":"attr","values":["value"]}`, + parsed(Query.notContains('attr', 'value').toString()), + { + method: 'notContains', + attribute: 'attr', + values: ['value'], + }, )); test('notSearch', () => assertEquals( - Query.notSearch('attr', 'keyword1 keyword2').toString(), - '{"method":"notSearch","attribute":"attr","values":["keyword1 keyword2"]}', + parsed(Query.notSearch('attr', 'keyword1 keyword2').toString()), + { + method: 'notSearch', + attribute: 'attr', + values: ['keyword1 keyword2'], + }, )); describe('notBetween', () => { test('with integers', () => assertEquals( - Query.notBetween('attr', 1, 2).toString(), - `{"method":"notBetween","attribute":"attr","values":[1,2]}`, + parsed(Query.notBetween('attr', 1, 2).toString()), + { + method: 'notBetween', + attribute: 'attr', + values: [1, 2], + }, )); test('with doubles', () => assertEquals( - Query.notBetween('attr', 1.2, 2.2).toString(), - `{"method":"notBetween","attribute":"attr","values":[1.2,2.2]}`, + parsed(Query.notBetween('attr', 1.2, 2.2).toString()), + { + method: 'notBetween', + attribute: 'attr', + values: [1.2, 2.2], + }, )); test('with strings', () => assertEquals( - Query.notBetween('attr', "a", "z").toString(), - `{"method":"notBetween","attribute":"attr","values":["a","z"]}`, + parsed(Query.notBetween('attr', "a", "z").toString()), + { + method: 'notBetween', + attribute: 'attr', + values: ['a', 'z'], + }, )); }); test('notStartsWith', () => assertEquals( - Query.notStartsWith('attr', 'prefix').toString(), - `{"method":"notStartsWith","attribute":"attr","values":["prefix"]}`, + parsed(Query.notStartsWith('attr', 'prefix').toString()), + { + method: 'notStartsWith', + attribute: 'attr', + values: ['prefix'], + }, )); test('notEndsWith', () => assertEquals( - Query.notEndsWith('attr', 'suffix').toString(), - `{"method":"notEndsWith","attribute":"attr","values":["suffix"]}`, + parsed(Query.notEndsWith('attr', 'suffix').toString()), + { + method: 'notEndsWith', + attribute: 'attr', + values: ['suffix'], + }, )); test('regex', () => assertEquals( - Query.regex('attr', 'pattern.*').toString(), - `{"method":"regex","attribute":"attr","values":["pattern.*"]}`, + parsed(Query.regex('attr', 'pattern.*').toString()), + { + method: 'regex', + attribute: 'attr', + values: ['pattern.*'], + }, )); test('exists', () => assertEquals( - Query.exists(['attr1', 'attr2']).toString(), - `{"method":"exists","values":["attr1","attr2"]}`, + parsed(Query.exists(['attr1', 'attr2']).toString()), + { + method: 'exists', + values: ['attr1', 'attr2'], + }, )); test('notExists', () => assertEquals( - Query.notExists(['attr1', 'attr2']).toString(), - `{"method":"notExists","values":["attr1","attr2"]}`, + parsed(Query.notExists(['attr1', 'attr2']).toString()), + { + method: 'notExists', + values: ['attr1', 'attr2'], + }, )); test('elemMatch', () => { @@ -246,118 +355,208 @@ describe('Query', () => { }); test('createdBefore', () => assertEquals( - Query.createdBefore('2023-01-01').toString(), - `{"method":"lessThan","attribute":"$createdAt","values":["2023-01-01"]}`, + parsed(Query.createdBefore('2023-01-01').toString()), + { + method: 'lessThan', + attribute: '$createdAt', + values: ['2023-01-01'], + }, )); test('createdAfter', () => assertEquals( - Query.createdAfter('2023-01-01').toString(), - `{"method":"greaterThan","attribute":"$createdAt","values":["2023-01-01"]}`, + parsed(Query.createdAfter('2023-01-01').toString()), + { + method: 'greaterThan', + attribute: '$createdAt', + values: ['2023-01-01'], + }, )); test('createdBetween', () => assertEquals( - Query.createdBetween('2023-01-01', '2023-12-31').toString(), - `{"method":"between","attribute":"$createdAt","values":["2023-01-01","2023-12-31"]}`, + parsed(Query.createdBetween('2023-01-01', '2023-12-31').toString()), + { + method: 'between', + attribute: '$createdAt', + values: ['2023-01-01', '2023-12-31'], + }, )); test('updatedBefore', () => assertEquals( - Query.updatedBefore('2023-01-01').toString(), - `{"method":"lessThan","attribute":"$updatedAt","values":["2023-01-01"]}`, + parsed(Query.updatedBefore('2023-01-01').toString()), + { + method: 'lessThan', + attribute: '$updatedAt', + values: ['2023-01-01'], + }, )); test('updatedAfter', () => assertEquals( - Query.updatedAfter('2023-01-01').toString(), - `{"method":"greaterThan","attribute":"$updatedAt","values":["2023-01-01"]}`, + parsed(Query.updatedAfter('2023-01-01').toString()), + { + method: 'greaterThan', + attribute: '$updatedAt', + values: ['2023-01-01'], + }, )); test('updatedBetween', () => assertEquals( - Query.updatedBetween('2023-01-01', '2023-12-31').toString(), - `{"method":"between","attribute":"$updatedAt","values":["2023-01-01","2023-12-31"]}`, + parsed(Query.updatedBetween('2023-01-01', '2023-12-31').toString()), + { + method: 'between', + attribute: '$updatedAt', + values: ['2023-01-01', '2023-12-31'], + }, )); test('count with alias', () => assertEquals( - Query.count('*', 'total').toString(), - '{"method":"count","attribute":"*","values":["total"]}', + parsed(Query.count('*', 'total').toString()), + { + method: 'count', + attribute: '*', + values: ['total'], + }, )); test('count without alias', () => assertEquals( - Query.count().toString(), - '{"method":"count","attribute":"*"}', + parsed(Query.count().toString()), + { + method: 'count', + attribute: '*', + }, )); test('countDistinct', () => assertEquals( - Query.countDistinct('year', 'uniqueYears').toString(), - '{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}', + parsed(Query.countDistinct('year', 'uniqueYears').toString()), + { + method: 'countDistinct', + attribute: 'year', + values: ['uniqueYears'], + }, )); test('sum', () => assertEquals( - Query.sum('price', 'total').toString(), - '{"method":"sum","attribute":"price","values":["total"]}', + parsed(Query.sum('price', 'total').toString()), + { + method: 'sum', + attribute: 'price', + values: ['total'], + }, )); test('avg', () => assertEquals( - Query.avg('price', 'avgPrice').toString(), - '{"method":"avg","attribute":"price","values":["avgPrice"]}', + parsed(Query.avg('price', 'avgPrice').toString()), + { + method: 'avg', + attribute: 'price', + values: ['avgPrice'], + }, )); test('min', () => assertEquals( - Query.min('price', 'lowest').toString(), - '{"method":"min","attribute":"price","values":["lowest"]}', + parsed(Query.min('price', 'lowest').toString()), + { + method: 'min', + attribute: 'price', + values: ['lowest'], + }, )); test('max', () => assertEquals( - Query.max('price', 'highest').toString(), - '{"method":"max","attribute":"price","values":["highest"]}', + parsed(Query.max('price', 'highest').toString()), + { + method: 'max', + attribute: 'price', + values: ['highest'], + }, )); test('stddev', () => assertEquals( - Query.stddev('price', 'sd').toString(), - '{"method":"stddev","attribute":"price","values":["sd"]}', + parsed(Query.stddev('price', 'sd').toString()), + { + method: 'stddev', + attribute: 'price', + values: ['sd'], + }, )); test('stddevPop', () => assertEquals( - Query.stddevPop('price', 'sdp').toString(), - '{"method":"stddevPop","attribute":"price","values":["sdp"]}', + parsed(Query.stddevPop('price', 'sdp').toString()), + { + method: 'stddevPop', + attribute: 'price', + values: ['sdp'], + }, )); test('stddevSamp', () => assertEquals( - Query.stddevSamp('price', 'sds').toString(), - '{"method":"stddevSamp","attribute":"price","values":["sds"]}', + parsed(Query.stddevSamp('price', 'sds').toString()), + { + method: 'stddevSamp', + attribute: 'price', + values: ['sds'], + }, )); test('variance', () => assertEquals( - Query.variance('price', 'var').toString(), - '{"method":"variance","attribute":"price","values":["var"]}', + parsed(Query.variance('price', 'var').toString()), + { + method: 'variance', + attribute: 'price', + values: ['var'], + }, )); test('varPop', () => assertEquals( - Query.varPop('price', 'vp').toString(), - '{"method":"varPop","attribute":"price","values":["vp"]}', + parsed(Query.varPop('price', 'vp').toString()), + { + method: 'varPop', + attribute: 'price', + values: ['vp'], + }, )); test('varSamp', () => assertEquals( - Query.varSamp('price', 'vs').toString(), - '{"method":"varSamp","attribute":"price","values":["vs"]}', + parsed(Query.varSamp('price', 'vs').toString()), + { + method: 'varSamp', + attribute: 'price', + values: ['vs'], + }, )); test('bitAnd', () => assertEquals( - Query.bitAnd('flags', 'band').toString(), - '{"method":"bitAnd","attribute":"flags","values":["band"]}', + parsed(Query.bitAnd('flags', 'band').toString()), + { + method: 'bitAnd', + attribute: 'flags', + values: ['band'], + }, )); test('bitOr', () => assertEquals( - Query.bitOr('flags', 'bor').toString(), - '{"method":"bitOr","attribute":"flags","values":["bor"]}', + parsed(Query.bitOr('flags', 'bor').toString()), + { + method: 'bitOr', + attribute: 'flags', + values: ['bor'], + }, )); test('bitXor', () => assertEquals( - Query.bitXor('flags', 'bxor').toString(), - '{"method":"bitXor","attribute":"flags","values":["bxor"]}', + parsed(Query.bitXor('flags', 'bxor').toString()), + { + method: 'bitXor', + attribute: 'flags', + values: ['bxor'], + }, )); test('groupBy', () => assertEquals( - Query.groupBy(['status', 'year']).toString(), - '{"method":"groupBy","values":["status","year"]}', + parsed(Query.groupBy(['status', 'year']).toString()), + { + method: 'groupBy', + values: ['status', 'year'], + }, )); test('having nests parsed queries', () => { @@ -369,33 +568,54 @@ describe('Query', () => { }); test('distinct', () => assertEquals( - Query.distinct().toString(), - '{"method":"distinct"}', + parsed(Query.distinct().toString()), + { + method: 'distinct', + }, )); test('join', () => assertEquals( - Query.join('orders', '$id', 'customerId').toString(), - '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}', + parsed(Query.join('orders', '$id', 'customerId').toString()), + { + method: 'join', + attribute: 'orders', + values: ['$id', '=', 'customerId'], + }, )); test('leftJoin with alias', () => assertEquals( - Query.leftJoin('orders', '$id', 'customerId', '=', 'ord').toString(), - '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', + parsed(Query.leftJoin('orders', '$id', 'customerId', '=', 'ord').toString()), + { + method: 'leftJoin', + attribute: 'orders', + values: ['$id', '=', 'customerId', 'ord'], + }, )); test('rightJoin', () => assertEquals( - Query.rightJoin('orders', '$id', 'customerId').toString(), - '{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}', + parsed(Query.rightJoin('orders', '$id', 'customerId').toString()), + { + method: 'rightJoin', + attribute: 'orders', + values: ['$id', '=', 'customerId'], + }, )); test('fullOuterJoin', () => assertEquals( - Query.fullOuterJoin('orders', '$id', 'customerId').toString(), - '{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}', + parsed(Query.fullOuterJoin('orders', '$id', 'customerId').toString()), + { + method: 'fullOuterJoin', + attribute: 'orders', + values: ['$id', '=', 'customerId'], + }, )); test('on', () => assertEquals( - Query.on('$id', 'customerId').toString(), - '{"method":"on","values":["$id","=","customerId"]}', + parsed(Query.on('$id', 'customerId').toString()), + { + method: 'on', + values: ['$id', '=', 'customerId'], + }, )); test('nested leftJoin', () => { @@ -411,35 +631,55 @@ describe('Query', () => { }); test('crossJoin', () => assertEquals( - Query.crossJoin('orders', 'ord').toString(), - '{"method":"crossJoin","attribute":"orders","values":["ord"]}', + parsed(Query.crossJoin('orders', 'ord').toString()), + { + method: 'crossJoin', + attribute: 'orders', + values: ['ord'], + }, )); test('vectorDot', () => assertEquals( - Query.vectorDot('embedding', [0.1, 0.2]).toString(), - '{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}', + parsed(Query.vectorDot('embedding', [0.1, 0.2]).toString()), + { + method: 'vectorDot', + attribute: 'embedding', + values: [[0.1, 0.2]], + }, )); test('notCovers', () => assertEquals( - Query.notCovers('location', [1, 2]).toString(), - '{"method":"notCovers","attribute":"location","values":[[1,2]]}', + parsed(Query.notCovers('location', [1, 2]).toString()), + { + method: 'notCovers', + attribute: 'location', + values: [[1, 2]], + }, )); test('spatialEquals', () => assertEquals( - Query.spatialEquals('location', [1, 2]).toString(), - '{"method":"spatialEquals","attribute":"location","values":[[1,2]]}', + parsed(Query.spatialEquals('location', [1, 2]).toString()), + { + method: 'spatialEquals', + attribute: 'location', + values: [[1, 2]], + }, )); test('notSpatialEquals', () => assertEquals( - Query.notSpatialEquals('location', [1, 2]).toString(), - '{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}', + parsed(Query.notSpatialEquals('location', [1, 2]).toString()), + { + method: 'notSpatialEquals', + attribute: 'location', + values: [[1, 2]], + }, )); test('page', () => { - const queries = Query.page(2, 10); + const queries = Query.page(2, 10).map(parsed); assertEquals(queries, [ - '{"method":"limit","values":[10]}', - '{"method":"offset","values":[10]}', + { method: 'limit', values: [10] }, + { method: 'offset', values: [10] }, ]); }); diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index 07a6b7bb94..da590c1924 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -1,35 +1,37 @@ const { Query, normalizeQueries, flattenParam } = require('../dist/query'); +const expectQuery = (query) => expect(JSON.parse(query)); + const tests = [ { description: 'with a string', value: 's', - expectedValues: '["s"]', + expectedValues: ['s'], }, { description: 'with a integer', value: 1, - expectedValues: '[1]', + expectedValues: [1], }, { description: 'with a double', value: 1.2, - expectedValues: '[1.2]', + expectedValues: [1.2], }, { description: 'with a whole number double', value: 1.0, - expectedValues: '[1]', + expectedValues: [1], }, { description: 'with a bool', value: false, - expectedValues: '[false]', + expectedValues: [false], }, { description: 'with a list', value: ['a', 'b', 'c'], - expectedValues: '["a","b","c"]', + expectedValues: ['a', 'b', 'c'], }, ]; @@ -37,9 +39,11 @@ describe('Query', () => { describe('basic filter equal', () => { for (const t of tests) { test(t.description, () => - expect(Query.equal('attr', t.value)).toEqual( - `{"method":"equal","attribute":"attr","values":${t.expectedValues}}`, - ), + expectQuery(Query.equal('attr', t.value)).toEqual({ + method: 'equal', + attribute: 'attr', + values: t.expectedValues, + }), ); } }); @@ -47,9 +51,11 @@ describe('Query', () => { describe('basic filter notEqual', () => { for (const t of tests) { test(t.description, () => - expect(Query.notEqual('attr', t.value)).toEqual( - `{"method":"notEqual","attribute":"attr","values":${t.expectedValues}}`, - ), + expectQuery(Query.notEqual('attr', t.value)).toEqual({ + method: 'notEqual', + attribute: 'attr', + values: t.expectedValues, + }), ); } }); @@ -57,9 +63,11 @@ describe('Query', () => { describe('basic filter lessThan', () => { for (const t of tests) { test(t.description, () => - expect(Query.lessThan('attr', t.value)).toEqual( - `{"method":"lessThan","attribute":"attr","values":${t.expectedValues}}`, - ), + expectQuery(Query.lessThan('attr', t.value)).toEqual({ + method: 'lessThan', + attribute: 'attr', + values: t.expectedValues, + }), ); } }); @@ -67,9 +75,11 @@ describe('Query', () => { describe('basic filter lessThanEqual', () => { for (const t of tests) { test(t.description, () => - expect(Query.lessThanEqual('attr', t.value)).toEqual( - `{"method":"lessThanEqual","attribute":"attr","values":${t.expectedValues}}`, - ), + expectQuery(Query.lessThanEqual('attr', t.value)).toEqual({ + method: 'lessThanEqual', + attribute: 'attr', + values: t.expectedValues, + }), ); } }); @@ -77,9 +87,11 @@ describe('Query', () => { describe('basic filter greaterThan', () => { for (const t of tests) { test(t.description, () => - expect(Query.greaterThan('attr', t.value)).toEqual( - `{"method":"greaterThan","attribute":"attr","values":${t.expectedValues}}`, - ), + expectQuery(Query.greaterThan('attr', t.value)).toEqual({ + method: 'greaterThan', + attribute: 'attr', + values: t.expectedValues, + }), ); } }); @@ -87,156 +99,210 @@ describe('Query', () => { describe('basic filter greaterThanEqual', () => { for (const t of tests) { test(t.description, () => - expect(Query.greaterThanEqual('attr', t.value)).toEqual( - `{"method":"greaterThanEqual","attribute":"attr","values":${t.expectedValues}}`, - ), + expectQuery(Query.greaterThanEqual('attr', t.value)).toEqual({ + method: 'greaterThanEqual', + attribute: 'attr', + values: t.expectedValues, + }), ); } }); test('search', () => - expect(Query.search('attr', 'keyword1 keyword2')).toEqual( - `{"method":"search","attribute":"attr","values":["keyword1 keyword2"]}`, - )); + expectQuery(Query.search('attr', 'keyword1 keyword2')).toEqual({ + method: 'search', + attribute: 'attr', + values: ['keyword1 keyword2'], + })); test('isNull', () => - expect(Query.isNull('attr')).toEqual( - `{"method":"isNull","attribute":"attr"}`, - )); + expectQuery(Query.isNull('attr')).toEqual({ + method: 'isNull', + attribute: 'attr', + })); test('isNotNull', () => - expect(Query.isNotNull('attr')).toEqual( - `{"method":"isNotNull","attribute":"attr"}`, - )); + expectQuery(Query.isNotNull('attr')).toEqual({ + method: 'isNotNull', + attribute: 'attr', + })); describe('between', () => { test('with integers', () => - expect(Query.between('attr', 1, 2)).toEqual( - `{"method":"between","attribute":"attr","values":[1,2]}`, - )); + expectQuery(Query.between('attr', 1, 2)).toEqual({ + method: 'between', + attribute: 'attr', + values: [1, 2], + })); test('with doubles', () => - expect(Query.between('attr', 1.2, 2.2)).toEqual( - `{"method":"between","attribute":"attr","values":[1.2,2.2]}`, - )); + expectQuery(Query.between('attr', 1.2, 2.2)).toEqual({ + method: 'between', + attribute: 'attr', + values: [1.2, 2.2], + })); test('with strings', () => - expect(Query.between('attr', 'a', 'z')).toEqual( - `{"method":"between","attribute":"attr","values":["a","z"]}`, - )); + expectQuery(Query.between('attr', 'a', 'z')).toEqual({ + method: 'between', + attribute: 'attr', + values: ['a', 'z'], + })); }); test('select', () => - expect(Query.select(['attr1', 'attr2'])).toEqual( - `{"method":"select","values":["attr1","attr2"]}`, - )); + expectQuery(Query.select(['attr1', 'attr2'])).toEqual({ + method: 'select', + values: ['attr1', 'attr2'], + })); test('orderAsc', () => - expect(Query.orderAsc('attr')).toEqual( - `{"method":"orderAsc","attribute":"attr"}`, - )); + expectQuery(Query.orderAsc('attr')).toEqual({ + method: 'orderAsc', + attribute: 'attr', + })); test('orderDesc', () => - expect(Query.orderDesc('attr')).toEqual( - `{"method":"orderDesc","attribute":"attr"}`, - )); + expectQuery(Query.orderDesc('attr')).toEqual({ + method: 'orderDesc', + attribute: 'attr', + })); test('cursorBefore', () => - expect(Query.cursorBefore('attr')).toEqual( - '{"method":"cursorBefore","values":["attr"]}', - )); + expectQuery(Query.cursorBefore('attr')).toEqual({ + method: 'cursorBefore', + values: ['attr'], + })); test('cursorAfter', () => - expect(Query.cursorAfter('attr')).toEqual( - '{"method":"cursorAfter","values":["attr"]}', - )); + expectQuery(Query.cursorAfter('attr')).toEqual({ + method: 'cursorAfter', + values: ['attr'], + })); test('limit', () => - expect(Query.limit(1)).toEqual('{"method":"limit","values":[1]}')); + expectQuery(Query.limit(1)).toEqual({ method: 'limit', values: [1] })); test('offset', () => - expect(Query.offset(1)).toEqual('{"method":"offset","values":[1]}')); + expectQuery(Query.offset(1)).toEqual({ + method: 'offset', + values: [1], + })); test('count with alias', () => - expect(Query.count('*', 'total')).toEqual( - '{"method":"count","attribute":"*","values":["total"]}', - )); + expectQuery(Query.count('*', 'total')).toEqual({ + method: 'count', + attribute: '*', + values: ['total'], + })); test('count without alias', () => - expect(Query.count()).toEqual('{"method":"count","attribute":"*"}')); + expectQuery(Query.count()).toEqual({ + method: 'count', + attribute: '*', + })); test('countDistinct', () => - expect(Query.countDistinct('year', 'uniqueYears')).toEqual( - '{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}', - )); + expectQuery(Query.countDistinct('year', 'uniqueYears')).toEqual({ + method: 'countDistinct', + attribute: 'year', + values: ['uniqueYears'], + })); test('sum', () => - expect(Query.sum('price', 'total')).toEqual( - '{"method":"sum","attribute":"price","values":["total"]}', - )); + expectQuery(Query.sum('price', 'total')).toEqual({ + method: 'sum', + attribute: 'price', + values: ['total'], + })); test('avg', () => - expect(Query.avg('price', 'avgPrice')).toEqual( - '{"method":"avg","attribute":"price","values":["avgPrice"]}', - )); + expectQuery(Query.avg('price', 'avgPrice')).toEqual({ + method: 'avg', + attribute: 'price', + values: ['avgPrice'], + })); test('min', () => - expect(Query.min('price', 'lowest')).toEqual( - '{"method":"min","attribute":"price","values":["lowest"]}', - )); + expectQuery(Query.min('price', 'lowest')).toEqual({ + method: 'min', + attribute: 'price', + values: ['lowest'], + })); test('max', () => - expect(Query.max('price', 'highest')).toEqual( - '{"method":"max","attribute":"price","values":["highest"]}', - )); + expectQuery(Query.max('price', 'highest')).toEqual({ + method: 'max', + attribute: 'price', + values: ['highest'], + })); test('stddev', () => - expect(Query.stddev('price', 'sd')).toEqual( - '{"method":"stddev","attribute":"price","values":["sd"]}', - )); + expectQuery(Query.stddev('price', 'sd')).toEqual({ + method: 'stddev', + attribute: 'price', + values: ['sd'], + })); test('stddevPop', () => - expect(Query.stddevPop('price', 'sdp')).toEqual( - '{"method":"stddevPop","attribute":"price","values":["sdp"]}', - )); + expectQuery(Query.stddevPop('price', 'sdp')).toEqual({ + method: 'stddevPop', + attribute: 'price', + values: ['sdp'], + })); test('stddevSamp', () => - expect(Query.stddevSamp('price', 'sds')).toEqual( - '{"method":"stddevSamp","attribute":"price","values":["sds"]}', - )); + expectQuery(Query.stddevSamp('price', 'sds')).toEqual({ + method: 'stddevSamp', + attribute: 'price', + values: ['sds'], + })); test('variance', () => - expect(Query.variance('price', 'var')).toEqual( - '{"method":"variance","attribute":"price","values":["var"]}', - )); + expectQuery(Query.variance('price', 'var')).toEqual({ + method: 'variance', + attribute: 'price', + values: ['var'], + })); test('varPop', () => - expect(Query.varPop('price', 'vp')).toEqual( - '{"method":"varPop","attribute":"price","values":["vp"]}', - )); + expectQuery(Query.varPop('price', 'vp')).toEqual({ + method: 'varPop', + attribute: 'price', + values: ['vp'], + })); test('varSamp', () => - expect(Query.varSamp('price', 'vs')).toEqual( - '{"method":"varSamp","attribute":"price","values":["vs"]}', - )); + expectQuery(Query.varSamp('price', 'vs')).toEqual({ + method: 'varSamp', + attribute: 'price', + values: ['vs'], + })); test('bitAnd', () => - expect(Query.bitAnd('flags', 'band')).toEqual( - '{"method":"bitAnd","attribute":"flags","values":["band"]}', - )); + expectQuery(Query.bitAnd('flags', 'band')).toEqual({ + method: 'bitAnd', + attribute: 'flags', + values: ['band'], + })); test('bitOr', () => - expect(Query.bitOr('flags', 'bor')).toEqual( - '{"method":"bitOr","attribute":"flags","values":["bor"]}', - )); + expectQuery(Query.bitOr('flags', 'bor')).toEqual({ + method: 'bitOr', + attribute: 'flags', + values: ['bor'], + })); test('bitXor', () => - expect(Query.bitXor('flags', 'bxor')).toEqual( - '{"method":"bitXor","attribute":"flags","values":["bxor"]}', - )); + expectQuery(Query.bitXor('flags', 'bxor')).toEqual({ + method: 'bitXor', + attribute: 'flags', + values: ['bxor'], + })); test('groupBy', () => - expect(Query.groupBy(['status', 'year'])).toEqual( - '{"method":"groupBy","values":["status","year"]}', - )); + expectQuery(Query.groupBy(['status', 'year'])).toEqual({ + method: 'groupBy', + values: ['status', 'year'], + })); test('having nests parsed queries', () => { const having = JSON.parse( @@ -249,34 +315,45 @@ describe('Query', () => { }); test('distinct', () => - expect(Query.distinct()).toEqual('{"method":"distinct"}')); + expectQuery(Query.distinct()).toEqual({ method: 'distinct' })); test('join', () => - expect(Query.join('orders', '$id', 'customerId')).toEqual( - '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}', - )); + expectQuery(Query.join('orders', '$id', 'customerId')).toEqual({ + method: 'join', + attribute: 'orders', + values: ['$id', '=', 'customerId'], + })); test('leftJoin with alias', () => - expect( + expectQuery( Query.leftJoin('orders', '$id', 'customerId', '=', 'ord'), - ).toEqual( - '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', - )); + ).toEqual({ + method: 'leftJoin', + attribute: 'orders', + values: ['$id', '=', 'customerId', 'ord'], + })); test('rightJoin', () => - expect(Query.rightJoin('orders', '$id', 'customerId')).toEqual( - '{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}', - )); + expectQuery(Query.rightJoin('orders', '$id', 'customerId')).toEqual({ + method: 'rightJoin', + attribute: 'orders', + values: ['$id', '=', 'customerId'], + })); test('fullOuterJoin', () => - expect(Query.fullOuterJoin('orders', '$id', 'customerId')).toEqual( - '{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}', + expectQuery(Query.fullOuterJoin('orders', '$id', 'customerId')).toEqual( + { + method: 'fullOuterJoin', + attribute: 'orders', + values: ['$id', '=', 'customerId'], + }, )); test('on', () => - expect(Query.on('$id', 'customerId')).toEqual( - '{"method":"on","values":["$id","=","customerId"]}', - )); + expectQuery(Query.on('$id', 'customerId')).toEqual({ + method: 'on', + values: ['$id', '=', 'customerId'], + })); test('nested leftJoin', () => { const query = JSON.parse( @@ -293,35 +370,45 @@ describe('Query', () => { }); test('crossJoin', () => - expect(Query.crossJoin('orders', 'ord')).toEqual( - '{"method":"crossJoin","attribute":"orders","values":["ord"]}', - )); + expectQuery(Query.crossJoin('orders', 'ord')).toEqual({ + method: 'crossJoin', + attribute: 'orders', + values: ['ord'], + })); test('vectorDot', () => - expect(Query.vectorDot('embedding', [0.1, 0.2])).toEqual( - '{"method":"vectorDot","attribute":"embedding","values":[[0.1,0.2]]}', - )); + expectQuery(Query.vectorDot('embedding', [0.1, 0.2])).toEqual({ + method: 'vectorDot', + attribute: 'embedding', + values: [[0.1, 0.2]], + })); test('notCovers', () => - expect(Query.notCovers('location', [1, 2])).toEqual( - '{"method":"notCovers","attribute":"location","values":[[1,2]]}', - )); + expectQuery(Query.notCovers('location', [1, 2])).toEqual({ + method: 'notCovers', + attribute: 'location', + values: [[1, 2]], + })); test('spatialEquals', () => - expect(Query.spatialEquals('location', [1, 2])).toEqual( - '{"method":"spatialEquals","attribute":"location","values":[[1,2]]}', - )); + expectQuery(Query.spatialEquals('location', [1, 2])).toEqual({ + method: 'spatialEquals', + attribute: 'location', + values: [[1, 2]], + })); test('notSpatialEquals', () => - expect(Query.notSpatialEquals('location', [1, 2])).toEqual( - '{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}', - )); + expectQuery(Query.notSpatialEquals('location', [1, 2])).toEqual({ + method: 'notSpatialEquals', + attribute: 'location', + values: [[1, 2]], + })); test('page', () => { - const queries = Query.page(2, 10); + const queries = Query.page(2, 10).map((query) => JSON.parse(query)); expect(queries).toEqual([ - '{"method":"limit","values":[10]}', - '{"method":"offset","values":[10]}', + { method: 'limit', values: [10] }, + { method: 'offset', values: [10] }, ]); }); @@ -374,7 +461,7 @@ describe('Query', () => { [1, 2], [3, 4], ]; - expect(flattenParam(geometry)).toBe(geometry); + expect(flattenParam(geometry)).toEqual(geometry); }); test('flattenParam keeps query string list', () => { diff --git a/templates/python/test/test_query.py.twig b/templates/python/test/test_query.py.twig index f5ff6f9856..6f090b339c 100644 --- a/templates/python/test/test_query.py.twig +++ b/templates/python/test/test_query.py.twig @@ -4,20 +4,24 @@ import unittest from {{ namespace | caseSnake }}.query import Query +def parsed(query): + return json.loads(query) + + class BasicFilterQueryTest: - def __init__(self, description: str, value, expected_values: str): + def __init__(self, description: str, value, expected_values): self.description = description self.value = value self.expected_values = expected_values tests = [ - BasicFilterQueryTest('with a string', 's', '["s"]'), - BasicFilterQueryTest('with an integer', 1, '[1]'), - BasicFilterQueryTest('with a double', 1.2, '[1.2]'), - BasicFilterQueryTest('with a whole number double', 1.0, '[1.0]'), - BasicFilterQueryTest('with a bool', False, '[false]'), - BasicFilterQueryTest('with a list', ['a', 'b', 'c'], '["a","b","c"]'), + BasicFilterQueryTest('with a string', 's', ['s']), + BasicFilterQueryTest('with an integer', 1, [1]), + BasicFilterQueryTest('with a double', 1.2, [1.2]), + BasicFilterQueryTest('with a whole number double', 1.0, [1.0]), + BasicFilterQueryTest('with a bool', False, [False]), + BasicFilterQueryTest('with a list', ['a', 'b', 'c'], ['a', 'b', 'c']), ] @@ -26,144 +30,172 @@ class TestQueryMethods(unittest.TestCase): def test_equal(self): for t in tests: self.assertEqual( - Query.equal('attr', t.value), - {% verbatim %}f'{{"method":"equal","attribute":"attr","values":{t.expected_values}}}',{% endverbatim %} + parsed(Query.equal('attr', t.value)), + {'method': 'equal', 'attribute': 'attr', 'values': t.expected_values}, t.description, ) def test_not_equal(self): for t in tests: self.assertEqual( - Query.not_equal('attr', t.value), - {% verbatim %}f'{{"method":"notEqual","attribute":"attr","values":{t.expected_values}}}',{% endverbatim %} + parsed(Query.not_equal('attr', t.value)), + {'method': 'notEqual', 'attribute': 'attr', 'values': t.expected_values}, t.description, ) def test_less_than(self): for t in tests: self.assertEqual( - Query.less_than('attr', t.value), - {% verbatim %}f'{{"method":"lessThan","attribute":"attr","values":{t.expected_values}}}',{% endverbatim %} + parsed(Query.less_than('attr', t.value)), + {'method': 'lessThan', 'attribute': 'attr', 'values': t.expected_values}, t.description, ) def test_less_than_equal(self): for t in tests: self.assertEqual( - Query.less_than_equal('attr', t.value), - {% verbatim %}f'{{"method":"lessThanEqual","attribute":"attr","values":{t.expected_values}}}',{% endverbatim %} + parsed(Query.less_than_equal('attr', t.value)), + {'method': 'lessThanEqual', 'attribute': 'attr', 'values': t.expected_values}, t.description, ) def test_greater_than(self): for t in tests: self.assertEqual( - Query.greater_than('attr', t.value), - {% verbatim %}f'{{"method":"greaterThan","attribute":"attr","values":{t.expected_values}}}',{% endverbatim %} + parsed(Query.greater_than('attr', t.value)), + {'method': 'greaterThan', 'attribute': 'attr', 'values': t.expected_values}, t.description, ) def test_greater_than_equal(self): for t in tests: self.assertEqual( - Query.greater_than_equal('attr', t.value), - {% verbatim %}f'{{"method":"greaterThanEqual","attribute":"attr","values":{t.expected_values}}}',{% endverbatim %} + parsed(Query.greater_than_equal('attr', t.value)), + {'method': 'greaterThanEqual', 'attribute': 'attr', 'values': t.expected_values}, t.description, ) def test_search(self): self.assertEqual( - Query.search('attr', 'keyword1 keyword2'), - '{"method":"search","attribute":"attr","values":["keyword1 keyword2"]}', + parsed(Query.search('attr', 'keyword1 keyword2')), + {'method': 'search', 'attribute': 'attr', 'values': ['keyword1 keyword2']}, ) def test_is_null(self): - self.assertEqual(Query.is_null('attr'), '{"method":"isNull","attribute":"attr"}') + self.assertEqual(parsed(Query.is_null('attr')), {'method': 'isNull', 'attribute': 'attr'}) def test_is_not_null(self): - self.assertEqual(Query.is_not_null('attr'), '{"method":"isNotNull","attribute":"attr"}') + self.assertEqual(parsed(Query.is_not_null('attr')), {'method': 'isNotNull', 'attribute': 'attr'}) def test_between_with_integers(self): - self.assertEqual(Query.between('attr', 1, 2), '{"method":"between","attribute":"attr","values":[1,2]}') + self.assertEqual( + parsed(Query.between('attr', 1, 2)), {'method': 'between', 'attribute': 'attr', 'values': [1, 2]} + ) def test_between_with_doubles(self): - self.assertEqual(Query.between('attr', 1.0, 2.0), '{"method":"between","attribute":"attr","values":[1.0,2.0]}') + self.assertEqual( + parsed(Query.between('attr', 1.0, 2.0)), {'method': 'between', 'attribute': 'attr', 'values': [1.0, 2.0]} + ) def test_between_with_strings(self): - self.assertEqual(Query.between('attr', 'a', 'z'), '{"method":"between","attribute":"attr","values":["a","z"]}') + self.assertEqual( + parsed(Query.between('attr', 'a', 'z')), {'method': 'between', 'attribute': 'attr', 'values': ['a', 'z']} + ) def test_select(self): - self.assertEqual(Query.select(['attr1', 'attr2']), '{"method":"select","values":["attr1","attr2"]}') + self.assertEqual(parsed(Query.select(['attr1', 'attr2'])), {'method': 'select', 'values': ['attr1', 'attr2']}) def test_order_asc(self): - self.assertEqual(Query.order_asc('attr'), '{"method":"orderAsc","attribute":"attr"}') + self.assertEqual(parsed(Query.order_asc('attr')), {'method': 'orderAsc', 'attribute': 'attr'}) def test_order_desc(self): - self.assertEqual(Query.order_desc('attr'), '{"method":"orderDesc","attribute":"attr"}') + self.assertEqual(parsed(Query.order_desc('attr')), {'method': 'orderDesc', 'attribute': 'attr'}) def test_cursor_before(self): - self.assertEqual(Query.cursor_before('custom'), '{"method":"cursorBefore","values":["custom"]}') + self.assertEqual(parsed(Query.cursor_before('custom')), {'method': 'cursorBefore', 'values': ['custom']}) def test_cursor_after(self): - self.assertEqual(Query.cursor_after('custom'), '{"method":"cursorAfter","values":["custom"]}') + self.assertEqual(parsed(Query.cursor_after('custom')), {'method': 'cursorAfter', 'values': ['custom']}) def test_limit(self): - self.assertEqual(Query.limit(1), '{"method":"limit","values":[1]}') + self.assertEqual(parsed(Query.limit(1)), {'method': 'limit', 'values': [1]}) def test_offset(self): - self.assertEqual(Query.offset(1), '{"method":"offset","values":[1]}') + self.assertEqual(parsed(Query.offset(1)), {'method': 'offset', 'values': [1]}) def test_count(self): - self.assertEqual(Query.count('*', 'total'), '{"method":"count","attribute":"*","values":["total"]}') + self.assertEqual(parsed(Query.count('*', 'total')), {'method': 'count', 'attribute': '*', 'values': ['total']}) def test_count_distinct(self): self.assertEqual( - Query.count_distinct('year', 'uniqueYears'), - '{"method":"countDistinct","attribute":"year","values":["uniqueYears"]}', + parsed(Query.count_distinct('year', 'uniqueYears')), + {'method': 'countDistinct', 'attribute': 'year', 'values': ['uniqueYears']}, ) def test_sum(self): - self.assertEqual(Query.sum('price', 'total'), '{"method":"sum","attribute":"price","values":["total"]}') + self.assertEqual( + parsed(Query.sum('price', 'total')), {'method': 'sum', 'attribute': 'price', 'values': ['total']} + ) def test_avg(self): - self.assertEqual(Query.avg('price', 'avgPrice'), '{"method":"avg","attribute":"price","values":["avgPrice"]}') + self.assertEqual( + parsed(Query.avg('price', 'avgPrice')), {'method': 'avg', 'attribute': 'price', 'values': ['avgPrice']} + ) def test_min(self): - self.assertEqual(Query.min('price', 'lowest'), '{"method":"min","attribute":"price","values":["lowest"]}') + self.assertEqual( + parsed(Query.min('price', 'lowest')), {'method': 'min', 'attribute': 'price', 'values': ['lowest']} + ) def test_max(self): - self.assertEqual(Query.max('price', 'highest'), '{"method":"max","attribute":"price","values":["highest"]}') + self.assertEqual( + parsed(Query.max('price', 'highest')), {'method': 'max', 'attribute': 'price', 'values': ['highest']} + ) def test_stddev(self): - self.assertEqual(Query.stddev('price', 'sd'), '{"method":"stddev","attribute":"price","values":["sd"]}') + self.assertEqual( + parsed(Query.stddev('price', 'sd')), {'method': 'stddev', 'attribute': 'price', 'values': ['sd']} + ) def test_stddev_pop(self): self.assertEqual( - Query.stddev_pop('price', 'sdp'), '{"method":"stddevPop","attribute":"price","values":["sdp"]}' + parsed(Query.stddev_pop('price', 'sdp')), {'method': 'stddevPop', 'attribute': 'price', 'values': ['sdp']} ) def test_stddev_samp(self): self.assertEqual( - Query.stddev_samp('price', 'sds'), '{"method":"stddevSamp","attribute":"price","values":["sds"]}' + parsed(Query.stddev_samp('price', 'sds')), {'method': 'stddevSamp', 'attribute': 'price', 'values': ['sds']} ) def test_variance(self): - self.assertEqual(Query.variance('price', 'var'), '{"method":"variance","attribute":"price","values":["var"]}') + self.assertEqual( + parsed(Query.variance('price', 'var')), {'method': 'variance', 'attribute': 'price', 'values': ['var']} + ) def test_var_pop(self): - self.assertEqual(Query.var_pop('price', 'vp'), '{"method":"varPop","attribute":"price","values":["vp"]}') + self.assertEqual( + parsed(Query.var_pop('price', 'vp')), {'method': 'varPop', 'attribute': 'price', 'values': ['vp']} + ) def test_var_samp(self): - self.assertEqual(Query.var_samp('price', 'vs'), '{"method":"varSamp","attribute":"price","values":["vs"]}') + self.assertEqual( + parsed(Query.var_samp('price', 'vs')), {'method': 'varSamp', 'attribute': 'price', 'values': ['vs']} + ) def test_bit_and(self): - self.assertEqual(Query.bit_and('flags', 'band'), '{"method":"bitAnd","attribute":"flags","values":["band"]}') + self.assertEqual( + parsed(Query.bit_and('flags', 'band')), {'method': 'bitAnd', 'attribute': 'flags', 'values': ['band']} + ) def test_bit_or(self): - self.assertEqual(Query.bit_or('flags', 'bor'), '{"method":"bitOr","attribute":"flags","values":["bor"]}') + self.assertEqual( + parsed(Query.bit_or('flags', 'bor')), {'method': 'bitOr', 'attribute': 'flags', 'values': ['bor']} + ) def test_bit_xor(self): - self.assertEqual(Query.bit_xor('flags', 'bxor'), '{"method":"bitXor","attribute":"flags","values":["bxor"]}') + self.assertEqual( + parsed(Query.bit_xor('flags', 'bxor')), {'method': 'bitXor', 'attribute': 'flags', 'values': ['bxor']} + ) def test_having(self): query = json.loads(Query.having([Query.greater_than('total', 1)])) @@ -172,12 +204,12 @@ class TestQueryMethods(unittest.TestCase): def test_join(self): self.assertEqual( - Query.join('orders', '$id', 'customerId'), - '{"method":"join","attribute":"orders","values":["$id","=","customerId"]}', + parsed(Query.join('orders', '$id', 'customerId')), + {'method': 'join', 'attribute': 'orders', 'values': ['$id', '=', 'customerId']}, ) def test_on(self): - self.assertEqual(Query.on('$id', 'customerId'), '{"method":"on","values":["$id","=","customerId"]}') + self.assertEqual(parsed(Query.on('$id', 'customerId')), {'method': 'on', 'values': ['$id', '=', 'customerId']}) def test_nested_left_join(self): query = json.loads( @@ -197,63 +229,64 @@ class TestQueryMethods(unittest.TestCase): self.assertEqual(query['values'][2]['method'], 'equal') def test_group_by(self): - self.assertEqual(Query.group_by(['status']), '{"method":"groupBy","values":["status"]}') + self.assertEqual(parsed(Query.group_by(['status'])), {'method': 'groupBy', 'values': ['status']}) def test_distinct(self): - self.assertEqual(Query.distinct(), '{"method":"distinct"}') + self.assertEqual(parsed(Query.distinct()), {'method': 'distinct'}) def test_covers(self): self.assertEqual( - Query.covers('location', [1, 2]), '{"method":"covers","attribute":"location","values":[[1,2]]}' + parsed(Query.covers('location', [1, 2])), {'method': 'covers', 'attribute': 'location', 'values': [[1, 2]]} ) def test_left_join(self): self.assertEqual( - Query.left_join('orders', '$id', 'customerId', '=', 'ord'), - '{"method":"leftJoin","attribute":"orders","values":["$id","=","customerId","ord"]}', + parsed(Query.left_join('orders', '$id', 'customerId', '=', 'ord')), + {'method': 'leftJoin', 'attribute': 'orders', 'values': ['$id', '=', 'customerId', 'ord']}, ) def test_right_join(self): self.assertEqual( - Query.right_join('orders', '$id', 'customerId'), - '{"method":"rightJoin","attribute":"orders","values":["$id","=","customerId"]}', + parsed(Query.right_join('orders', '$id', 'customerId')), + {'method': 'rightJoin', 'attribute': 'orders', 'values': ['$id', '=', 'customerId']}, ) def test_full_outer_join(self): self.assertEqual( - Query.full_outer_join('orders', '$id', 'customerId'), - '{"method":"fullOuterJoin","attribute":"orders","values":["$id","=","customerId"]}', + parsed(Query.full_outer_join('orders', '$id', 'customerId')), + {'method': 'fullOuterJoin', 'attribute': 'orders', 'values': ['$id', '=', 'customerId']}, ) def test_cross_join(self): self.assertEqual( - Query.cross_join('orders', 'ord'), '{"method":"crossJoin","attribute":"orders","values":["ord"]}' + parsed(Query.cross_join('orders', 'ord')), {'method': 'crossJoin', 'attribute': 'orders', 'values': ['ord']} ) def test_not_covers(self): self.assertEqual( - Query.not_covers('location', [1, 2]), '{"method":"notCovers","attribute":"location","values":[[1,2]]}' + parsed(Query.not_covers('location', [1, 2])), + {'method': 'notCovers', 'attribute': 'location', 'values': [[1, 2]]}, ) def test_spatial_equals(self): self.assertEqual( - Query.spatial_equals('location', [1, 2]), - '{"method":"spatialEquals","attribute":"location","values":[[1,2]]}', + parsed(Query.spatial_equals('location', [1, 2])), + {'method': 'spatialEquals', 'attribute': 'location', 'values': [[1, 2]]}, ) def test_not_spatial_equals(self): self.assertEqual( - Query.not_spatial_equals('location', [1, 2]), - '{"method":"notSpatialEquals","attribute":"location","values":[[1,2]]}', + parsed(Query.not_spatial_equals('location', [1, 2])), + {'method': 'notSpatialEquals', 'attribute': 'location', 'values': [[1, 2]]}, ) def test_page(self): - queries = Query.page(2, 10) + queries = [parsed(query) for query in Query.page(2, 10)] self.assertEqual( queries, [ - '{"method":"limit","values":[10]}', - '{"method":"offset","values":[10]}', + {'method': 'limit', 'values': [10]}, + {'method': 'offset', 'values': [10]}, ], ) From 5652f2f5818d134b5e6abf89e1c022f907f79766 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Tue, 22 Sep 2026 19:32:55 +1200 Subject: [PATCH 35/42] fix(unity): emit Query/Builder.cs in the Unity file plan Unity overrides getFiles() instead of inheriting the .NET plan, so the shared dotnet/Package/Query.cs.twig it renders referenced Query.Builder with nothing emitting the partial that declares it. Co-Authored-By: Claude Fable 5.1 --- src/SDK/Language/Unity.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SDK/Language/Unity.php b/src/SDK/Language/Unity.php index 71bf634279..dbf489da32 100644 --- a/src/SDK/Language/Unity.php +++ b/src/SDK/Language/Unity.php @@ -159,6 +159,11 @@ public function getFiles(): array 'destination' => 'Assets/Runtime/Core/Query.cs', 'template' => 'dotnet/Package/Query.cs.twig', ], + [ + 'scope' => 'default', + 'destination' => 'Assets/Runtime/Core/Query/Builder.cs', + 'template' => 'dotnet/Package/Query/Builder.cs.twig', + ], [ 'scope' => 'default', 'destination' => 'Assets/Runtime/Core/Role.cs', From 89301801cce117f91f0c143f74cb3ee2f1527640 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 16:43:40 +1300 Subject: [PATCH 36/42] fix(cli): build against a Go SDK with the same services as the CLI configureSDK merged only exclude.services into the defaults, so the CLI's exclude.methods override replaced the whole exclude block and the CLI regained documentsDB and vectorsDB. Against the published sdk-for-go that went unnoticed; against the locally generated SDK pinned through go.mod's replace, those packages do not exist and the CLI fails to build. Merge every exclude kind, and generate the pinned SDK on the CLI's server platform. Co-Authored-By: Claude Opus 5.5 --- example.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/example.php b/example.php index 4548bfcf97..e261338013 100644 --- a/example.php +++ b/example.php @@ -82,13 +82,11 @@ function configureSDK(SDK $sdk, array $overrides = []): SDK { ], ]; - // Deep-merge exclude services so overrides add to defaults rather than replacing - if (isset($overrides['exclude']['services']) && isset($defaults['exclude']['services'])) { - $overrides['exclude']['services'] = [ - ...$defaults['exclude']['services'], - ...$overrides['exclude']['services'], - ]; + $exclude = $defaults['exclude']; + foreach ($overrides['exclude'] ?? [] as $kind => $rules) { + $exclude[$kind] = [...($exclude[$kind] ?? []), ...$rules]; } + $overrides['exclude'] = $exclude; $config = [...$defaults, ...$overrides]; @@ -267,6 +265,7 @@ function buildStaticSpecification(): Specification { // CLI go.mod replace-pins github.com/appwrite/sdk-for-go/v7 to ../go. $go = new SDK(new Go(), buildSpecification($spec)); configureSDK($go, [ + 'platform' => 'server', 'gitUserName' => 'appwrite', 'gitRepoName' => 'sdk-for-go', 'version' => '7.2.0-rc.3', From f2714c1fd5023b9ee486010f63957e76ce50f611 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 17:21:26 +1300 Subject: [PATCH 37/42] fix(cli): keep the local go sdk replace out of released modules go.mod replaced sdk-for-go with ../go for every non-test CLI, so a released CLI only built inside this repository's examples layout. The replace now comes from an sdkPath parameter that only example.php sets. Co-Authored-By: Claude Opus 5.5 --- example.php | 1 + src/SDK/Language/CLI.php | 11 +++++++++++ templates/cli/go.mod.twig | 6 ++---- tests/generation/GenerationTest.php | 28 ++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/example.php b/example.php index e261338013..75fb607f18 100644 --- a/example.php +++ b/example.php @@ -277,6 +277,7 @@ function buildStaticSpecification(): Specification { $language->setLogo($cliLogo); $language->setLogoUnescaped($cliLogoUnescaped); $language->setNPMPackage('appwrite-cli'); + $language->setSdkPath('../go'); $sdk = new SDK($language, buildSpecification($spec)); $sdk->setTest(false); diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 8de2d0b2a1..c7d50acf32 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -42,6 +42,7 @@ class CLI extends Go // silently break every installer if they did not. 'npmPackage' => 'appwrite-cli', 'sdkImportPath' => self::SDK_MODULE, + 'sdkPath' => '', // ASCII art above the main help screen. Templates escape it for their // target syntax rather than sharing one encoded representation. 'logo' => '', @@ -98,6 +99,16 @@ public function setNPMPackage(string $name): self return $this; } + /** + * Local directory go.mod replaces the published Go SDK with. Empty keeps the published module. + */ + public function setSdkPath(string $path): self + { + $this->setParam('sdkPath', $path); + + return $this; + } + /** * Configure the Homebrew tap (`/homebrew-`) hosting the formula. */ diff --git a/templates/cli/go.mod.twig b/templates/cli/go.mod.twig index 41b2669e60..f5b371eb7a 100644 --- a/templates/cli/go.mod.twig +++ b/templates/cli/go.mod.twig @@ -22,9 +22,7 @@ require ( // Floor raised above the v1.3.6 huh v1.0.0 asks for, which misrepaints on resize. require github.com/charmbracelet/bubbletea v1.3.10 // indirect -{% if not sdk.test %} +{% if language.params.sdkPath %} -// Live spec can add methods the last published Go SDK does not have yet. -// example.php generates that SDK next to this module as examples/go. -replace {{ language.params.sdkImportPath }} => ../go +replace {{ language.params.sdkImportPath }} => {{ language.params.sdkPath }} {% endif %} diff --git a/tests/generation/GenerationTest.php b/tests/generation/GenerationTest.php index 6c1298a661..15711e8876 100644 --- a/tests/generation/GenerationTest.php +++ b/tests/generation/GenerationTest.php @@ -540,6 +540,34 @@ public function testRequestModelsComeFromBodyOnlySchemas(): void $this->assertStringNotContainsString('request model', $go['models/mock.go']); } + /** + * A released CLI has to build from its own module, so only a CLI pinned + * to a local Go SDK may replace the published one. + */ + public function testCliReplacesGoSdkOnlyWhenPinned(): void + { + $this->assertStringNotContainsString('replace ', $this->generateCliModule(new CLI(), 'release')); + $this->assertStringContainsString( + 'replace ' . CLI::SDK_MODULE . ' => ../go', + $this->generateCliModule(new CLI()->setSdkPath('../go'), 'pinned'), + ); + } + + private function generateCliModule(CLI $language, string $key): string + { + $dir = self::OUTPUT . '/cli-module/' . $key; + $this->removeDirectory($dir); + + new SDK($language, Parser::parse((string) \file_get_contents(self::FIXTURE))) + ->setName('test') + ->setVersion('0.0.1') + ->setPlatform('server') + ->setNamespace('appwrite') + ->generate($dir); + + return (string) \file_get_contents($dir . '/go.mod'); + } + /** * The generated tree for an inline document, for a shape the shared * fixture does not carry. From a666f408de7a03e242b5d6ea394d3a8b0feb0567 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 17:21:26 +1300 Subject: [PATCH 38/42] fix(kotlin): send built queries in json request bodies prepareRequest flattened builders for GET and multipart but serialised the original params for JSON bodies, so a Query.Builder went out as an object instead of its query list. Co-Authored-By: Claude Opus 5.5 --- .../src/main/java/io/package/Client.kt.twig | 7 +++---- .../main/kotlin/io/appwrite/Client.kt.twig | 7 +++---- .../io/appwrite/JsonRequestBodyTest.kt.twig | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Client.kt.twig b/templates/android/library/src/main/java/io/package/Client.kt.twig index b8e5c7a530..171cdab5d9 100644 --- a/templates/android/library/src/main/java/io/package/Client.kt.twig +++ b/templates/android/library/src/main/java/io/package/Client.kt.twig @@ -337,9 +337,8 @@ class Client @JvmOverloads constructor( ): Request { // flatten() resolves Query/Query.Builder values before the null filter, // so a builder that renders to null is dropped like any other null. - val filteredParams = params - .mapValues { flatten(it.value) } - .filterValues { it != null } + val flattenedParams = params.mapValues { flatten(it.value) } + val filteredParams = flattenedParams.filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) @@ -402,7 +401,7 @@ class Client @JvmOverloads constructor( } builder.build() } else { - params + flattenedParams .toJsonRequestBody() .toRequestBody("application/json".toMediaType()) } diff --git a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig index c3b89a7e1d..13a2b61d09 100644 --- a/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig +++ b/templates/kotlin/src/main/kotlin/io/appwrite/Client.kt.twig @@ -239,9 +239,8 @@ class Client @JvmOverloads constructor( headers: Map = mapOf(), params: Map = mapOf(), ): Request { - val filteredParams = params - .mapValues { flatten(it.value) } - .filterValues { it != null } + val flattenedParams = params.mapValues { flatten(it.value) } + val filteredParams = flattenedParams.filterValues { it != null } val requestHeaders = this.headers.toHeaders().newBuilder() .addAll(headers.toHeaders()) @@ -304,7 +303,7 @@ class Client @JvmOverloads constructor( } builder.build() } else { - params + flattenedParams .toJsonRequestBody() .toRequestBody("application/json".toMediaType()) } diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/JsonRequestBodyTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/JsonRequestBodyTest.kt.twig index 27d10f5d63..82d3a891f1 100644 --- a/templates/kotlin/src/test/kotlin/io/appwrite/JsonRequestBodyTest.kt.twig +++ b/templates/kotlin/src/test/kotlin/io/appwrite/JsonRequestBodyTest.kt.twig @@ -55,6 +55,25 @@ class JsonRequestBodyTest { assertFalse(body.contains("permissions")) } + @Test + fun jsonBodySendsBuiltQueries() = runBlocking { + val request = Client().prepareRequest( + method = "POST", + path = "/v1/tablesdb/db/tables/tbl/rows", + headers = mapOf("content-type" to "application/json"), + params = mapOf( + "queries" to Query.builder().equal("title", "Spiderman").limit(1), + "permissions" to null, + ), + ) + + val body = requestBodyUtf8(request.body!!) + assertFalse(body.contains("\"queries\":{")) + assertTrue(body.contains("\"queries\":[\"{\\\"method\\\":\\\"equal\\\"")) + assertTrue(body.contains("\\\"method\\\":\\\"limit\\\"")) + assertFalse(body.contains("permissions")) + } + @Test fun getOmitsNullQueryParams() = runBlocking { val request = Client().prepareRequest( From f34dd8605ceb96f633bc8852d3850b6c3b24e0e2 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 17:21:26 +1300 Subject: [PATCH 39/42] fix(android): keep nested join and having queries intact under r8 Parsing nested queries back into Query relied on Gson reflection over fields R8 renames, so shrunk apps sent nested ON and HAVING queries without their method. Parse them into maps, as or/and/elemMatch do. Co-Authored-By: Claude Opus 5.5 --- .../android/library/src/main/java/io/package/Query.kt.twig | 4 ++-- .../kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) 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 a81784d8eb..adf28d2741 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 @@ -669,7 +669,7 @@ class Query( * @param queries Nested query strings. * @returns The query string. */ - fun having(queries: List) = Query("having", null, queries.map { it.fromJson() }).toJson() + fun having(queries: List) = Query("having", null, queries.map { it.fromJson>() }).toJson() /** * Select distinct rows. @@ -887,7 +887,7 @@ class Query( if (alias.isNotEmpty()) { values.add(alias) } - on.forEach { values.add(it.fromJson()) } + on.forEach { values.add(it.fromJson>()) } return Query(method, table, values).toJson() } diff --git a/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig b/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig index 36ba7a58d9..72588a736c 100644 --- a/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig +++ b/templates/kotlin/src/test/kotlin/io/appwrite/QueryTest.kt.twig @@ -80,7 +80,9 @@ class QueryTest { @Test fun having() { - assertEquals("having", method(Query.having(listOf(Query.greaterThan("total", 1))))) + val json = JsonParser.parseString(Query.having(listOf(Query.greaterThan("total", 1)))).asJsonObject + assertEquals("having", json.get("method").asString) + assertEquals("greaterThan", json.get("values").asJsonArray[0].asJsonObject.get("method").asString) } @Test From 4ad82274277dd69d43f476a1b87310f99ee030ad Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 17:21:27 +1300 Subject: [PATCH 40/42] feat(cli): expose nested join on in the typed query builder The generated QueryBuilder only offered the scalar join form, so typed callbacks could not express the nested ON conditions the SDKs accept. Co-Authored-By: Claude Opus 5.5 --- .../generator/testdata/client.databases.ts | 9 ++--- .../generator/testdata/client.types.ts | 9 ++--- .../generator/testdata/server.databases.ts | 9 ++--- .../generator/testdata/server.types.ts | 9 ++--- .../typegen/templates/databases.ts.hbs | 9 ++--- .../internal/typegen/templates/types.ts.hbs | 9 ++--- .../cli/internal/typegen/templates_test.go | 34 +++++++++++++++++++ 7 files changed, 64 insertions(+), 24 deletions(-) diff --git a/templates/cli/internal/generator/testdata/client.databases.ts b/templates/cli/internal/generator/testdata/client.databases.ts index c8dc415099..6941a7c0e8 100644 --- a/templates/cli/internal/generator/testdata/client.databases.ts +++ b/templates/cli/internal/generator/testdata/client.databases.ts @@ -43,10 +43,11 @@ const createQueryBuilder = (): QueryBuilder => ({ groupBy: (attributes) => Query.groupBy(attributes), having: (queries) => Query.having(queries), distinct: () => Query.distinct(), - join: (table, left, right, operator = '=', alias = '') => Query.join(table, left, right, operator, alias), - leftJoin: (table, left, right, operator = '=', alias = '') => Query.leftJoin(table, left, right, operator, alias), - rightJoin: (table, left, right, operator = '=', alias = '') => Query.rightJoin(table, left, right, operator, alias), - fullOuterJoin: (table, left, right, operator = '=', alias = '') => Query.fullOuterJoin(table, left, right, operator, alias), + on: (left, right, operator = '=') => Query.on(left, right, operator), + join: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias), + leftJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), + rightJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), + fullOuterJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), crossJoin: (table, alias = '') => Query.crossJoin(table, alias), vectorDot: (attribute, vector) => Query.vectorDot(attribute, vector), vectorCosine: (attribute, vector) => Query.vectorCosine(attribute, vector), diff --git a/templates/cli/internal/generator/testdata/client.types.ts b/templates/cli/internal/generator/testdata/client.types.ts index 8c795fc15d..c155f3b9a2 100644 --- a/templates/cli/internal/generator/testdata/client.types.ts +++ b/templates/cli/internal/generator/testdata/client.types.ts @@ -129,10 +129,11 @@ export type QueryBuilder = { groupBy: (attributes: string[]) => string; having: (queries: string[]) => string; distinct: () => string; - join: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - leftJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - rightJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - fullOuterJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + on: (left: string, right: string, operator?: string) => string; + join: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + leftJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + rightJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + fullOuterJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; crossJoin: (table: string, alias?: string) => string; vectorDot: (attribute: string, vector: number[]) => string; vectorCosine: (attribute: string, vector: number[]) => string; diff --git a/templates/cli/internal/generator/testdata/server.databases.ts b/templates/cli/internal/generator/testdata/server.databases.ts index da1c09cd9f..24ceff642b 100644 --- a/templates/cli/internal/generator/testdata/server.databases.ts +++ b/templates/cli/internal/generator/testdata/server.databases.ts @@ -43,10 +43,11 @@ const createQueryBuilder = (): QueryBuilder => ({ groupBy: (attributes) => Query.groupBy(attributes), having: (queries) => Query.having(queries), distinct: () => Query.distinct(), - join: (table, left, right, operator = '=', alias = '') => Query.join(table, left, right, operator, alias), - leftJoin: (table, left, right, operator = '=', alias = '') => Query.leftJoin(table, left, right, operator, alias), - rightJoin: (table, left, right, operator = '=', alias = '') => Query.rightJoin(table, left, right, operator, alias), - fullOuterJoin: (table, left, right, operator = '=', alias = '') => Query.fullOuterJoin(table, left, right, operator, alias), + on: (left, right, operator = '=') => Query.on(left, right, operator), + join: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias), + leftJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), + rightJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), + fullOuterJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), crossJoin: (table, alias = '') => Query.crossJoin(table, alias), vectorDot: (attribute, vector) => Query.vectorDot(attribute, vector), vectorCosine: (attribute, vector) => Query.vectorCosine(attribute, vector), diff --git a/templates/cli/internal/generator/testdata/server.types.ts b/templates/cli/internal/generator/testdata/server.types.ts index 0e363e73be..dfd3d1b5c9 100644 --- a/templates/cli/internal/generator/testdata/server.types.ts +++ b/templates/cli/internal/generator/testdata/server.types.ts @@ -129,10 +129,11 @@ export type QueryBuilder = { groupBy: (attributes: string[]) => string; having: (queries: string[]) => string; distinct: () => string; - join: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - leftJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - rightJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - fullOuterJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + on: (left: string, right: string, operator?: string) => string; + join: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + leftJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + rightJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + fullOuterJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; crossJoin: (table: string, alias?: string) => string; vectorDot: (attribute: string, vector: number[]) => string; vectorCosine: (attribute: string, vector: number[]) => string; diff --git a/templates/cli/internal/typegen/templates/databases.ts.hbs b/templates/cli/internal/typegen/templates/databases.ts.hbs index 7947c08c3c..099c6e28a8 100644 --- a/templates/cli/internal/typegen/templates/databases.ts.hbs +++ b/templates/cli/internal/typegen/templates/databases.ts.hbs @@ -47,10 +47,11 @@ const createQueryBuilder = (): QueryBuilder => ({ groupBy: (attributes) => Query.groupBy(attributes), having: (queries) => Query.having(queries), distinct: () => Query.distinct(), - join: (table, left, right, operator = '=', alias = '') => Query.join(table, left, right, operator, alias), - leftJoin: (table, left, right, operator = '=', alias = '') => Query.leftJoin(table, left, right, operator, alias), - rightJoin: (table, left, right, operator = '=', alias = '') => Query.rightJoin(table, left, right, operator, alias), - fullOuterJoin: (table, left, right, operator = '=', alias = '') => Query.fullOuterJoin(table, left, right, operator, alias), + on: (left, right, operator = '=') => Query.on(left, right, operator), + join: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.join(table, leftOrAliasOrOn, rightOrOn, operator, alias), + leftJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.leftJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), + rightJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.rightJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), + fullOuterJoin: (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query.fullOuterJoin(table, leftOrAliasOrOn, rightOrOn, operator, alias), crossJoin: (table, alias = '') => Query.crossJoin(table, alias), vectorDot: (attribute, vector) => Query.vectorDot(attribute, vector), vectorCosine: (attribute, vector) => Query.vectorCosine(attribute, vector), diff --git a/templates/cli/internal/typegen/templates/types.ts.hbs b/templates/cli/internal/typegen/templates/types.ts.hbs index bc44186e3a..a89a1017e3 100644 --- a/templates/cli/internal/typegen/templates/types.ts.hbs +++ b/templates/cli/internal/typegen/templates/types.ts.hbs @@ -79,10 +79,11 @@ export type QueryBuilder = { groupBy: (attributes: string[]) => string; having: (queries: string[]) => string; distinct: () => string; - join: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - leftJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - rightJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; - fullOuterJoin: (table: string, left: string, right: string, operator?: string, alias?: string) => string; + on: (left: string, right: string, operator?: string) => string; + join: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + leftJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + rightJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; + fullOuterJoin: (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string; crossJoin: (table: string, alias?: string) => string; vectorDot: (attribute: string, vector: number[]) => string; vectorCosine: (attribute: string, vector: number[]) => string; diff --git a/templates/cli/internal/typegen/templates_test.go b/templates/cli/internal/typegen/templates_test.go index 532cc0ca1f..248342a6e6 100644 --- a/templates/cli/internal/typegen/templates_test.go +++ b/templates/cli/internal/typegen/templates_test.go @@ -60,3 +60,37 @@ func TestRenderConstantsTemplate(t *testing.T) { t.Errorf("API_KEY block rendered despite requiresApiKey being false:\n%s", withoutKey) } } + +// The typed QueryBuilder must accept the same join forms as the SDK's Query, so +// a query written against one compiles against the other. +func TestQueryBuilderSupportsNestedJoinConditions(t *testing.T) { + joins := []string{"join", "leftJoin", "rightJoin", "fullOuterJoin"} + + types, err := Template(TemplateTypes) + if err != nil { + t.Fatal(err) + } + declarations := []string{"on: (left: string, right: string, operator?: string) => string;"} + for _, join := range joins { + declarations = append(declarations, join+": (table: string, leftOrAliasOrOn: string | string[], rightOrOn?: string | string[], operator?: string, alias?: string) => string;") + } + for _, want := range declarations { + if !strings.Contains(types, want) { + t.Errorf("%s does not declare %q", TemplateTypes, want) + } + } + + databases, err := Template(TemplateDatabases) + if err != nil { + t.Fatal(err) + } + implementations := []string{"on: (left, right, operator = '=') => Query.on(left, right, operator),"} + for _, join := range joins { + implementations = append(implementations, join+": (table, leftOrAliasOrOn, rightOrOn = '', operator = '=', alias = '') => Query."+join+"(table, leftOrAliasOrOn, rightOrOn, operator, alias),") + } + for _, want := range implementations { + if !strings.Contains(databases, want) { + t.Errorf("%s does not implement %q", TemplateDatabases, want) + } + } +} From be1e57e544e158e46b091ecc193741f47fd00e76 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 17:21:27 +1300 Subject: [PATCH 41/42] fix: flatten only query builders, not nested string arrays Clients spliced any nested list of strings into its parent, assuming it was a built query list, so a JSON value such as [['a','b']] was sent as ['a','b']. Builders are now recognised by type and every other value is sent unchanged. Co-Authored-By: Claude Opus 5.5 --- templates/apple/Sources/Client.swift.twig | 50 ++++------ templates/dart/lib/src/client_mixin.dart.twig | 32 ++----- templates/dart/test/query_test.dart.twig | 49 +++++++++- templates/deno/src/query.ts.twig | 29 +++--- templates/deno/test/query.test.ts.twig | 19 ++++ templates/dotnet/Package/Query.cs.twig | 36 ------- .../flutter/lib/src/client_mixin.dart.twig | 32 ++----- templates/go/client.go.twig | 41 ++------ templates/go/client_test.go.twig | 82 ++++++++++++++++ templates/node/test/client.test.js.twig | 53 ++++++++++ templates/node/test/query.test.js.twig | 24 +++++ templates/php/src/Client.php.twig | 14 --- templates/php/tests/ClientTest.php.twig | 54 +++++++++++ templates/python/package/client.py.twig | 9 +- templates/python/package/query.py.twig | 13 --- templates/python/test/test_query.py.twig | 63 ++++++++++-- templates/react-native/src/query.ts.twig | 29 +++--- templates/ruby/lib/container/query.rb.twig | 13 --- templates/rust/src/client.rs.twig | 96 +++++++++++-------- templates/rust/src/query/mod.rs.twig | 47 --------- templates/swift/Sources/Client.swift.twig | 50 ++++------ templates/swift/Tests/Tests.swift.twig | 49 ++++++++-- templates/web/src/query.ts.twig | 29 +++--- 23 files changed, 540 insertions(+), 373 deletions(-) diff --git a/templates/apple/Sources/Client.swift.twig b/templates/apple/Sources/Client.swift.twig index dc435aa8a2..af7d7f0637 100644 --- a/templates/apple/Sources/Client.swift.twig +++ b/templates/apple/Sources/Client.swift.twig @@ -758,53 +758,35 @@ open class Client { } public func flatten(_ value: Any?) -> Any? { + return flattened(value).value + } + + private func flattened(_ value: Any?) -> (value: Any?, changed: Bool) { if let builder = value as? Query.Builder { - return builder.build() + return (builder.build(), true) } if let query = value as? Query { - return query.description - } - if let builders = value as? [Query.Builder] { - return builders.flatMap { $0.build() } + return (query.description, true) } - if let queries = value as? [Query] { - return queries.map { $0.description } - } - if let strings = value as? [String] { - return strings + if value is [String] { + return (value, false) } guard let items = arrayElements(value) else { - return value + return (value, false) } var out: [Any] = [] var changed = false for item in items { - let flat = flatten(item) - if let strings = flat as? [String], !(item is any StringProtocol) { - out.append(contentsOf: strings) - changed = true - continue - } - if item is Query.Builder || item is Query { - if let list = arrayElements(flat) { - out.append(contentsOf: list) - } else if let flat { - out.append(flat) - } - changed = true - continue - } - if arrayElements(item) != nil, let list = arrayElements(flat), list.allSatisfy({ $0 is String }) { - out.append(contentsOf: list) - changed = true - continue - } - if let flat { - out.append(flat) + let flat = flattened(item) + changed = changed || flat.changed + if item is Query.Builder, let queries = flat.value as? [String] { + out.append(contentsOf: queries) + } else if let entry = flat.value { + out.append(entry) } } - return changed ? out : value + return changed ? (out, true) : (value, false) } private func arrayElements(_ value: Any?) -> [Any]? { diff --git a/templates/dart/lib/src/client_mixin.dart.twig b/templates/dart/lib/src/client_mixin.dart.twig index 688a0bdc17..3332200868 100644 --- a/templates/dart/lib/src/client_mixin.dart.twig +++ b/templates/dart/lib/src/client_mixin.dart.twig @@ -1,4 +1,5 @@ import 'package:http/http.dart' as http; +import '../{{ language.params.packageName }}.dart' show QueryBuilder; import 'exception.dart'; import 'response.dart'; import 'dart:convert'; @@ -160,31 +161,14 @@ mixin ClientMixin { } dynamic _flattenValue(dynamic value) { - if (value == null) { - return null; + if (value is QueryBuilder) { + return value.build(); } - try { - return _flattenValue(value.build()); - } catch (_) {} - if (value is! List) { + if (value is! List || !value.any((item) => item is QueryBuilder)) { return value; } - - final out = []; - var changed = false; - for (final item in value) { - final flat = _flattenValue(item); - if (item is! String && - flat is List && - flat.every((entry) => entry is String)) { - out.addAll(flat); - changed = true; - continue; - } - if (flat != item) { - changed = true; - } - out.add(flat); - } - return changed ? out : value; + return [ + for (final item in value) + if (item is QueryBuilder) ...item.build() else item, + ]; } diff --git a/templates/dart/test/query_test.dart.twig b/templates/dart/test/query_test.dart.twig index 6e026d6856..4e9eafcf62 100644 --- a/templates/dart/test/query_test.dart.twig +++ b/templates/dart/test/query_test.dart.twig @@ -573,7 +573,7 @@ void main() { expect(built, expected); }); -{% if 'dart' in language.params.packageName %} +{% if language.name == 'Dart' %} test('flatten spreads wrapped builder', () { final client = Client(); @@ -593,5 +593,52 @@ void main() { final client = Client(); expect(client.flatten([Query.limit(1)]), [Query.limit(1)]); }); + + test('flatten keeps nested string lists', () { + final client = Client(); + final value = [ + ['a', 'b'], + ]; + expect(client.flatten(value), [ + ['a', 'b'], + ]); + }); + + test('flatten expands builder value', () { + final client = Client(); + expect(client.flatten(Query.builder().limit(1).offset(2)), [ + Query.limit(1), + Query.offset(2), + ]); + }); + + test('flatten splices builders into query list', () { + final client = Client(); + expect( + client.flatten([ + Query.limit(1), + Query.builder().offset(2).orderAsc('name'), + ]), + [Query.limit(1), Query.offset(2), Query.orderAsc('name')], + ); + }); + + test('flatten does not call build on other objects', () { + final client = Client(); + final value = _Buildable(); + expect(client.flatten(value), same(value)); + expect(value.calls, 0); + }); {% endif %} } +{% if language.name == 'Dart' %} + +class _Buildable { + int calls = 0; + + List build() { + calls++; + return ['built']; + } +} +{% endif %} diff --git a/templates/deno/src/query.ts.twig b/templates/deno/src/query.ts.twig index 2757508793..2d35a37d1c 100644 --- a/templates/deno/src/query.ts.twig +++ b/templates/deno/src/query.ts.twig @@ -1020,24 +1020,27 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { return out; } -function isQueryList(value: unknown[]): boolean { - return value.every( - (item) => - item instanceof Query.Builder || - item instanceof Query || - typeof item === "string" || - (Array.isArray(item) && isQueryList(item)) - ); +function isQueryValue(value: unknown): value is Query | Query.Builder { + return value instanceof Query.Builder || value instanceof Query; } export function flattenParam(value: unknown): unknown { - if (value instanceof Query.Builder || value instanceof Query) { - return normalizeQueries(value as QueryInput); + if (isQueryValue(value)) { + return normalizeQueries(value); } - if (Array.isArray(value) && isQueryList(value)) { - return normalizeQueries(value as QueryInput); + if (!Array.isArray(value) || !value.some(isQueryValue)) { + return value; } - return value; + + const out: unknown[] = []; + for (const item of value) { + if (isQueryValue(item)) { + out.push(...normalizeQueries(item)); + } else { + out.push(item); + } + } + return out; } export const QueryBuilder = Query.Builder; diff --git a/templates/deno/test/query.test.ts.twig b/templates/deno/test/query.test.ts.twig index 6bbadc6356..3f221dec7a 100644 --- a/templates/deno/test/query.test.ts.twig +++ b/templates/deno/test/query.test.ts.twig @@ -728,4 +728,23 @@ describe('Query', () => { test('flattenParam keeps query string list', () => { assertEquals(flattenParam([Query.limit(1)]), [Query.limit(1)]); }); + + test('flattenParam keeps nested string arrays', () => { + assertEquals(flattenParam([['a', 'b']]), [['a', 'b']]); + }); + + test('flattenParam builds a builder value', () => { + assertEquals(flattenParam(Query.builder().equal('status', 'active').limit(1)), [ + Query.equal('status', 'active'), + Query.limit(1), + ]); + }); + + test('flattenParam splices builders among query strings', () => { + assertEquals(flattenParam([Query.equal('status', 'active'), Query.builder().limit(1).offset(2)]), [ + Query.equal('status', 'active'), + Query.limit(1), + Query.offset(2), + ]); + }); }) diff --git a/templates/dotnet/Package/Query.cs.twig b/templates/dotnet/Package/Query.cs.twig index 47638cdfe1..ed8c38e2c1 100644 --- a/templates/dotnet/Package/Query.cs.twig +++ b/templates/dotnet/Package/Query.cs.twig @@ -609,12 +609,6 @@ namespace {{ spec.info.title | caseUcfirst }} changed = true; continue; } - if (item is IEnumerable nestedItems && item is not string && IsStringList(flat)) - { - output.AddRange(((IEnumerable)flat!).Cast()); - changed = true; - continue; - } if (!Equals(flat, item)) { changed = true; @@ -625,36 +619,6 @@ namespace {{ spec.info.title | caseUcfirst }} return changed ? output : value; } - private static bool IsStringList(object? value) - { - if (value is not IEnumerable enumerable || value is string) - { - return false; - } - foreach (var item in enumerable) - { - if (item is not string) - { - return false; - } - } - return true; - } - - internal static List Normalize(object? queries) - { - var flat = Flatten(queries); - if (flat is string s) - { - return new List { s }; - } - if (flat is IEnumerable enumerable && flat is not string) - { - return enumerable.Cast().Select(item => item?.ToString() ?? string.Empty).ToList(); - } - return new List(); - } - private static string JoinQuery(string method, string table, string left, string right, string @operator, string alias) { var values = new List { left, @operator, right }; diff --git a/templates/flutter/lib/src/client_mixin.dart.twig b/templates/flutter/lib/src/client_mixin.dart.twig index 3f2c42a6eb..cbf68936db 100644 --- a/templates/flutter/lib/src/client_mixin.dart.twig +++ b/templates/flutter/lib/src/client_mixin.dart.twig @@ -1,4 +1,5 @@ import 'package:http/http.dart' as http; +import '../{{ language.params.packageName }}.dart' show QueryBuilder; import 'exception.dart'; import 'response.dart'; import 'dart:convert'; @@ -165,31 +166,14 @@ mixin ClientMixin { } dynamic _flattenValue(dynamic value) { - if (value == null) { - return null; + if (value is QueryBuilder) { + return value.build(); } - try { - return _flattenValue(value.build()); - } catch (_) {} - if (value is! List) { + if (value is! List || !value.any((item) => item is QueryBuilder)) { return value; } - - final out = []; - var changed = false; - for (final item in value) { - final flat = _flattenValue(item); - if (item is! String && - flat is List && - flat.every((entry) => entry is String)) { - out.addAll(flat); - changed = true; - continue; - } - if (flat != item) { - changed = true; - } - out.add(flat); - } - return changed ? out : value; + return [ + for (final item in value) + if (item is QueryBuilder) ...item.build() else item, + ]; } diff --git a/templates/go/client.go.twig b/templates/go/client.go.twig index 7498985df7..922d6d9050 100644 --- a/templates/go/client.go.twig +++ b/templates/go/client.go.twig @@ -833,27 +833,8 @@ func flattenValue(value interface{}) interface{} { for i := 0; i < reflected.Len(); i++ { item := reflected.Index(i).Interface() flat := flattenValue(item) - switch item.(type) { - case *query.Builder, query.Builder: - if list, ok := flat.([]string); ok { - for _, entry := range list { - out = append(out, entry) - } - changed = true - continue - } - } - if nested, ok := item.([]string); ok { - for _, entry := range nested { - out = append(out, entry) - } - if len(nested) > 0 { - changed = true - } - continue - } - if nested, ok := item.([]interface{}); ok && isStringSlice(flattenValue(nested)) { - for _, entry := range flattenValue(nested).([]interface{}) { + if isBuilder(item) { + for _, entry := range flat.([]string) { out = append(out, entry) } changed = true @@ -870,16 +851,12 @@ func flattenValue(value interface{}) interface{} { return out } -func isStringSlice(value interface{}) bool { - items, ok := value.([]interface{}) - if !ok { - _, ok := value.([]string) - return ok - } - for _, item := range items { - if _, ok := item.(string); !ok { - return false - } +func isBuilder(value interface{}) bool { + switch v := value.(type) { + case *query.Builder: + return v != nil + case query.Builder: + return true } - return true + return false } diff --git a/templates/go/client_test.go.twig b/templates/go/client_test.go.twig index 179459a270..bcf65e425c 100644 --- a/templates/go/client_test.go.twig +++ b/templates/go/client_test.go.twig @@ -3,6 +3,8 @@ package client import ( "crypto/tls" + "encoding/json" + "io" "net/http" "net/http/httptest" "net/url" @@ -295,4 +297,84 @@ func TestFlattenValueSpreadsWrappedBuilder(t *testing.T) { t.Errorf("Expected %#v, got %#v", want, got) } } +func TestFlattenValueKeepsNestedStringArrays(t *testing.T) { + for _, test := range []struct { + name string + value interface{} + }{ + {name: "typed", value: [][]string{[]string{"a", "b"}}}, + {name: "untyped", value: []interface{}{[]interface{}{"a", "b"}}}, + {name: "mixed", value: []interface{}{"x", []string{"a", "b"}}}, + } { + t.Run(test.name, func(t *testing.T) { + got := flattenValue(test.value) + if !reflect.DeepEqual(got, test.value) { + t.Errorf("Expected %#v to pass through, got %#v", test.value, got) + } + }) + } +} + +func TestFlattenValueBuildsBuilder(t *testing.T) { + builder := query.NewBuilder().Equal("status", "active").Limit(1) + got := flattenValue(builder) + want := []string{query.Equal("status", "active"), query.Limit(1)} + if !reflect.DeepEqual(got, want) { + t.Errorf("Expected %#v, got %#v", want, got) + } +} + +func TestFlattenValueSpreadsBuildersBesideStrings(t *testing.T) { + builder := query.NewBuilder().Limit(1) + got := flattenValue([]interface{}{query.Offset(2), builder, *builder}) + want := []interface{}{query.Offset(2), query.Limit(1), query.Limit(1)} + if !reflect.DeepEqual(got, want) { + t.Errorf("Expected %#v, got %#v", want, got) + } +} + +func TestCallSendsFlattenedParams(t *testing.T) { + var body map[string]interface{} + var rawQuery url.Values + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rawQuery = r.URL.Query() + if r.Method != http.MethodGet { + data, _ := io.ReadAll(r.Body) + json.Unmarshal(data, &body) + } + w.Header().Set("content-type", "application/json") + w.Write([]byte("{}")) + })) + defer server.Close() + + c := New() + c.Endpoint = server.URL + builder := query.NewBuilder().Limit(1) + + _, err := c.Call("POST", "/", map[string]interface{}{"content-type": "application/json"}, map[string]interface{}{ + "values": []interface{}{[]interface{}{"a", "b"}}, + "queries": []interface{}{query.Offset(2), builder}, + }) + if err != nil { + t.Fatalf("Expected the call to succeed, got %v", err) + } + wantValues := []interface{}{[]interface{}{"a", "b"}} + if !reflect.DeepEqual(body["values"], wantValues) { + t.Errorf("Expected values %#v in the JSON body, got %#v", wantValues, body["values"]) + } + wantQueries := []interface{}{query.Offset(2), query.Limit(1)} + if !reflect.DeepEqual(body["queries"], wantQueries) { + t.Errorf("Expected queries %#v in the JSON body, got %#v", wantQueries, body["queries"]) + } + + _, err = c.Call("GET", "/", map[string]interface{}{}, map[string]interface{}{ + "queries": builder, + }) + if err != nil { + t.Fatalf("Expected the call to succeed, got %v", err) + } + if got := rawQuery.Get("queries[0]"); got != query.Limit(1) { + t.Errorf("Expected queries[0] to be %q in the query string, got %q", query.Limit(1), got) + } +} {% endautoescape %} diff --git a/templates/node/test/client.test.js.twig b/templates/node/test/client.test.js.twig index a8bec62234..bfbbda8f63 100644 --- a/templates/node/test/client.test.js.twig +++ b/templates/node/test/client.test.js.twig @@ -1,5 +1,6 @@ const { Client } = require('../dist/client'); const { InputFile } = require('../dist/inputFile'); +const { Query } = require('../dist/query'); const { fetch: mockedFetch, Response, Dispatcher } = require('undici'); jest.mock('undici', () => ({ @@ -45,6 +46,58 @@ describe('Client', () => { }); }); +describe('Client.prepareRequest', () => { + const url = () => new URL('https://cloud.appwrite.io/v1/rows'); + const params = () => ({ + data: [['a', 'b']], + queries: Query.builder().equal('status', 'active').limit(1), + filters: [Query.equal('status', 'active'), Query.builder().limit(1)], + }); + const queries = [Query.equal('status', 'active'), Query.limit(1)]; + + test('sends nested string arrays and flattened builders as JSON', () => { + const { options } = new Client().prepareRequest( + 'POST', + url(), + { 'content-type': 'application/json' }, + params(), + ); + + expect(JSON.parse(options.body)).toEqual({ + data: [['a', 'b']], + queries, + filters: queries, + }); + }); + + test('sends nested string arrays and flattened builders as query string', () => { + const { uri } = new Client().prepareRequest('GET', url(), {}, params()); + const search = new URL(uri).searchParams; + + expect(search.get('data[0][0]')).toBe('a'); + expect(search.get('data[0][1]')).toBe('b'); + expect(search.has('data[0]')).toBe(false); + expect([search.get('queries[0]'), search.get('queries[1]')]).toEqual( + queries, + ); + expect([search.get('filters[0]'), search.get('filters[1]')]).toEqual( + queries, + ); + }); + + test('sends flattened builders as multipart fields', () => { + const { options } = new Client().prepareRequest( + 'POST', + url(), + { 'content-type': 'multipart/form-data' }, + params(), + ); + + expect(options.body.getAll('queries[]')).toEqual(queries); + expect(options.body.getAll('filters[]')).toEqual(queries); + }); +}); + describe('Client.chunkedUpload', () => { const url = new URL( 'https://cloud.appwrite.io/v1/storage/buckets/bucket/files', diff --git a/templates/node/test/query.test.js.twig b/templates/node/test/query.test.js.twig index da590c1924..53c3bc898b 100644 --- a/templates/node/test/query.test.js.twig +++ b/templates/node/test/query.test.js.twig @@ -467,4 +467,28 @@ describe('Query', () => { test('flattenParam keeps query string list', () => { expect(flattenParam([Query.limit(1)])).toEqual([Query.limit(1)]); }); + + test('flattenParam keeps nested string arrays', () => { + const value = [['a', 'b']]; + expect(flattenParam(value)).toEqual([['a', 'b']]); + }); + + test('flattenParam builds a builder value', () => { + expect( + flattenParam(Query.builder().equal('status', 'active').limit(1)), + ).toEqual([Query.equal('status', 'active'), Query.limit(1)]); + }); + + test('flattenParam splices builders among query strings', () => { + expect( + flattenParam([ + Query.equal('status', 'active'), + Query.builder().limit(1).offset(2), + ]), + ).toEqual([ + Query.equal('status', 'active'), + Query.limit(1), + Query.offset(2), + ]); + }); }); diff --git a/templates/php/src/Client.php.twig b/templates/php/src/Client.php.twig index 4026a3c1cf..8e08356de4 100644 --- a/templates/php/src/Client.php.twig +++ b/templates/php/src/Client.php.twig @@ -417,11 +417,6 @@ class Client $changed = true; continue; } - if (\is_array($item) && \array_is_list($item) && self::isStringList($flat)) { - array_push($out, ...$flat); - $changed = true; - continue; - } if ($flat !== $item) { $changed = true; } @@ -431,15 +426,6 @@ class Client return $changed ? $out : $value; } - private static function isStringList(mixed $value): bool - { - if (!\is_array($value) || !\array_is_list($value)) { - return false; - } - - return array_all($value, fn ($item): bool => \is_string($item)); - } - /** * Convert flattened params into Utopia multipart parts. * diff --git a/templates/php/tests/ClientTest.php.twig b/templates/php/tests/ClientTest.php.twig index 3fb9b17784..fedfa81a5f 100644 --- a/templates/php/tests/ClientTest.php.twig +++ b/templates/php/tests/ClientTest.php.twig @@ -87,6 +87,27 @@ final class ClientTest extends TestCase array_values((array) Client::flatten([Query::limit(1)])) ); } + + public function testFlattenKeepsNestedStringArrays(): void + { + $this->assertSame([['a', 'b']], Client::flatten([['a', 'b']])); + } + + public function testFlattenBuilderParamBecomesQueryStrings(): void + { + $this->assertSame( + [Query::equal('status', ['active']), Query::limit(1)], + Client::flatten(Query::builder()->equal('status', ['active'])->limit(1)) + ); + } + + public function testFlattenSplicesBuildersBesideNestedStringArrays(): void + { + $this->assertSame( + [Query::limit(1), Query::offset(2), ['a', 'b']], + Client::flatten([Query::builder()->limit(1)->offset(2), ['a', 'b']]) + ); + } {% for headerKey, header in globalHeaders %} public function testSet{{headerKey | caseUcfirst}}ReturnsClient(): void @@ -154,6 +175,39 @@ final class ClientTest extends TestCase $this->assertSame('injected', $received[0]->getHeaderLine('x-test')); } + public function testInjectedTransportReceivesFlattenedParams(): void + { + $received = []; + $transport = $this->createMock(ClientInterface::class); + $transport->expects($this->exactly(2))->method('sendRequest') + ->willReturnCallback(function (RequestInterface $request) use (&$received): ResponseInterface { + $received[] = $request; + + return new Response(body: new Stream('{}')) + ->withHeader('content-type', 'application/json'); + }); + + $client = new Client($transport)->setEndpoint('https://example.com/v1'); + $params = [ + 'queries' => Query::builder()->limit(1)->offset(2), + 'tags' => [['a', 'b']], + ]; + + $client->call(Client::METHOD_POST, '/items', ['content-type' => 'application/json'], $params); + $client->call(Client::METHOD_GET, '/items', [], $params); + + $this->assertCount(2, $received); + $this->assertSame( + ['queries' => [Query::limit(1), Query::offset(2)], 'tags' => [['a', 'b']]], + json_decode((string) $received[0]->getBody(), true) + ); + parse_str($received[1]->getUri()->getQuery(), $query); + $this->assertSame( + ['queries' => [Query::limit(1), Query::offset(2)], 'tags' => [['a', 'b']]], + $query + ); + } + public function testInjectedTransportSurvivesFailureAndConfigurationChanges(): void { $received = []; diff --git a/templates/python/package/client.py.twig b/templates/python/package/client.py.twig index 0d89038983..a64c959f6e 100644 --- a/templates/python/package/client.py.twig +++ b/templates/python/package/client.py.twig @@ -300,13 +300,10 @@ class Client: def flatten(self, data, prefix='', stringify=False): output = {} - i = 0 + entries = data.items() if isinstance(data, dict) else enumerate(data) - for key in data: - value = data[key] if isinstance(data, dict) else key - finalKey = prefix + '[' + key + ']' if prefix else key - finalKey = prefix + '[' + str(i) + ']' if isinstance(data, list) else finalKey - i += 1 + for key, value in entries: + finalKey = f'{prefix}[{key}]' if prefix or isinstance(data, list) else key if isinstance(value, list) or isinstance(value, dict): output = {**output, **self.flatten(value, finalKey, stringify)} diff --git a/templates/python/package/query.py.twig b/templates/python/package/query.py.twig index 1c55faae95..261845d484 100644 --- a/templates/python/package/query.py.twig +++ b/templates/python/package/query.py.twig @@ -437,25 +437,12 @@ class Query: out.append(flat) changed = True continue - if isinstance(item, list) and isinstance(flat, list) and all(isinstance(entry, str) for entry in flat): - out.extend(flat) - changed = True - continue if flat is not item: changed = True out.append(flat) return out if changed else value - @staticmethod - def _normalize(queries): - flat = Query._flatten(queries) - if isinstance(flat, list): - return [str(item) for item in flat] - if isinstance(flat, str): - return [flat] - return [] - @staticmethod def _create_join(method, table, left, right, operator, alias): if isinstance(left, list): diff --git a/templates/python/test/test_query.py.twig b/templates/python/test/test_query.py.twig index 6f090b339c..a7965f75a5 100644 --- a/templates/python/test/test_query.py.twig +++ b/templates/python/test/test_query.py.twig @@ -1,6 +1,11 @@ import json import unittest +from urllib.parse import parse_qsl +from urllib.parse import urlsplit +import requests_mock + +from {{ namespace | caseSnake }}.client import Client from {{ namespace | caseSnake }}.query import Query @@ -309,12 +314,6 @@ class TestQueryMethods(unittest.TestCase): ) self.assertEqual(expected, built) - def test_normalize_builder(self): - builder = Query.builder().equal('status', 'active').limit(10) - self.assertEqual(builder.build(), Query._normalize(builder)) - self.assertEqual(builder.build(), Query._normalize([builder])) - self.assertEqual(builder.build(), Query._normalize([builder.build()])) - def test_flatten_spreads_wrapped_builder(self): self.assertEqual([Query.limit(1)], Query._flatten([Query.builder().limit(1)])) @@ -324,3 +323,55 @@ class TestQueryMethods(unittest.TestCase): def test_flatten_keeps_query_string_list(self): self.assertEqual([Query.limit(1)], Query._flatten([Query.limit(1)])) + + def test_flatten_keeps_nested_string_arrays(self): + nested = [['a', 'b']] + self.assertEqual([['a', 'b']], Query._flatten(nested)) + + def test_flatten_builder_param_becomes_query_strings(self): + builder = Query.builder().equal('status', 'active').limit(1) + self.assertEqual([Query.equal('status', 'active'), Query.limit(1)], Query._flatten(builder)) + + def test_flatten_splices_builders_beside_nested_string_arrays(self): + builder = Query.builder().limit(1).offset(2) + self.assertEqual( + [Query.limit(1), Query.offset(2), ['a', 'b']], + Query._flatten([builder, ['a', 'b']]), + ) + + @requests_mock.Mocker() + def test_client_sends_flattened_params(self, m): + m.register_uri( + requests_mock.ANY, + requests_mock.ANY, + json={}, + headers={'Content-Type': 'application/json'}, + ) + client = Client().set_endpoint('https://example.com/v1') + queries = [Query.limit(1), Query.offset(2)] + + def params(): + return {'queries': Query.builder().limit(1).offset(2), 'tags': [['a', 'b']]} + + client.call('post', '/items', {'content-type': 'application/json'}, params()) + client.call('get', '/items', {'content-type': 'application/json'}, params()) + client.call('post', '/items', {'content-type': 'multipart/form-data'}, params()) + + json_request, get_request, multipart_request = m.request_history + self.assertEqual( + {'queries': queries, 'tags': [['a', 'b']]}, + json.loads(json_request.body), + ) + self.assertEqual( + [ + ('queries[0]', queries[0]), + ('queries[1]', queries[1]), + ('tags[0][0]', 'a'), + ('tags[0][1]', 'b'), + ], + parse_qsl(urlsplit(get_request.url).query), + ) + multipart_body = multipart_request.body.decode() + self.assertIn('name="tags[0][0]"\r\n\r\na\r\n', multipart_body) + self.assertIn('name="tags[0][1]"\r\n\r\nb\r\n', multipart_body) + self.assertIn(f'name="queries[1]"\r\n\r\n{queries[1]}\r\n', multipart_body) diff --git a/templates/react-native/src/query.ts.twig b/templates/react-native/src/query.ts.twig index ecd79ae473..be2dff7077 100644 --- a/templates/react-native/src/query.ts.twig +++ b/templates/react-native/src/query.ts.twig @@ -1245,24 +1245,27 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { return out; } -function isQueryList(value: unknown[]): boolean { - return value.every( - (item) => - item instanceof Query.Builder || - item instanceof Query || - typeof item === 'string' || - (Array.isArray(item) && isQueryList(item)), - ); +function isQueryValue(value: unknown): value is Query | Query.Builder { + return value instanceof Query.Builder || value instanceof Query; } export function flattenParam(value: unknown): unknown { - if (value instanceof Query.Builder || value instanceof Query) { - return normalizeQueries(value as QueryInput); + if (isQueryValue(value)) { + return normalizeQueries(value); } - if (Array.isArray(value) && isQueryList(value)) { - return normalizeQueries(value as QueryInput); + if (!Array.isArray(value) || !value.some(isQueryValue)) { + return value; } - return value; + + const out: unknown[] = []; + for (const item of value) { + if (isQueryValue(item)) { + out.push(...normalizeQueries(item)); + } else { + out.push(item); + } + } + return out; } export const QueryBuilder = Query.Builder; diff --git a/templates/ruby/lib/container/query.rb.twig b/templates/ruby/lib/container/query.rb.twig index eb45c23df4..a353ef1cdf 100644 --- a/templates/ruby/lib/container/query.rb.twig +++ b/templates/ruby/lib/container/query.rb.twig @@ -424,11 +424,6 @@ module {{spec.info.title | caseUcfirst}} changed = true next end - if item.is_a?(Array) && flat.is_a?(Array) && flat.all? { |entry| entry.is_a?(String) } - out.concat(flat) - changed = true - next - end changed = true if flat != item out << flat end @@ -436,14 +431,6 @@ module {{spec.info.title | caseUcfirst}} changed ? out : value end - def _normalize(queries) - flat = _flatten(queries) - return flat.map(&:to_s) if flat.is_a?(Array) - return [flat] if flat.is_a?(String) - - [] - end - private def create_join(method, table, left, right, operator, alias_name) diff --git a/templates/rust/src/client.rs.twig b/templates/rust/src/client.rs.twig index 7aad98164a..1df80573e8 100644 --- a/templates/rust/src/client.rs.twig +++ b/templates/rust/src/client.rs.twig @@ -3,7 +3,6 @@ use crate::error::{{ spec.info.title | caseUcfirst }}Error; use crate::error::Result; use crate::input_file::InputFile; -use crate::query::Query; use arc_swap::ArcSwap; use reqwest::{header::HeaderMap, multipart, Client as HttpClient, Method, Response}; use serde::de::DeserializeOwned; @@ -383,34 +382,6 @@ impl Client { result } - fn normalize_request_params(params: HashMap) -> HashMap { - params - .into_iter() - .map(|(key, value)| (key, Self::flatten_value(value))) - .collect() - } - - pub fn flatten_value(value: Value) -> Value { - match value { - Value::Array(items) if Self::is_query_string_tree(&items) => Value::Array( - Query::normalize_value(&Value::Array(items)) - .into_iter() - .map(Value::String) - .collect(), - ), - other => other, - } - } - - fn is_query_string_tree(items: &[Value]) -> bool { - !items.is_empty() - && items.iter().all(|item| match item { - Value::String(_) => true, - Value::Array(nested) => Self::is_query_string_tree(nested), - _ => false, - }) - } - pub async fn call( &self, method: Method, @@ -441,7 +412,7 @@ impl Client { }); let mut request_builder; - if let Some(params) = params.map(Self::normalize_request_params) { + if let Some(params) = params { if method == Method::GET { let mut url_with_params = Url::parse(&url).map_err(|e| { {{ spec.info.title | caseUcfirst }}Error::new(0, format!("Invalid URL: {}", e), None, String::new()) @@ -508,7 +479,7 @@ impl Client { } } - if let Some(params) = params.map(Self::normalize_request_params) { + if let Some(params) = params { if method == Method::GET { let mut url_with_params = Url::parse(&url).map_err(|e| { {{ spec.info.title | caseUcfirst }}Error::new(0, format!("Invalid URL: {}", e), None, String::new()) @@ -599,7 +570,7 @@ impl Client { let url = format!("{}{}", state.config.endpoint, path); let mut request_builder; - if let Some(params) = params.map(Self::normalize_request_params) { + if let Some(params) = params { if method == Method::GET { let mut url_with_params = Url::parse(&url).map_err(|e| { {{ spec.info.title | caseUcfirst }}Error::new(0, format!("Invalid URL: {}", e), None, String::new()) @@ -1301,6 +1272,7 @@ impl Client { #[cfg(test)] mod tests { use super::*; + use crate::query::Query; #[test] fn test_client_creation() { @@ -1318,19 +1290,59 @@ mod tests { assert_eq!(client.endpoint(), "https://custom.example.com/v1"); } - #[test] - fn flatten_value_keeps_numeric_nested_arrays() { - let geometry = serde_json::json!([[1, 2], [3, 4]]); - assert_eq!(Client::flatten_value(geometry.clone()), geometry); + async fn mock_server() -> wiremock::MockServer { + let server = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::any()) + .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({}))) + .mount(&server) + .await; + server } - #[test] - fn flatten_value_spreads_nested_query_strings() { - let nested = serde_json::json!([["{\"method\":\"limit\",\"values\":[1]}"]]); - let flat = Client::flatten_value(nested); + fn params() -> HashMap { + HashMap::from([ + ("values".to_string(), serde_json::json!([["a", "b"]])), + ( + "queries".to_string(), + serde_json::json!(Query::builder().limit(1)), + ), + ]) + } + + #[tokio::test] + async fn json_body_keeps_nested_string_arrays() { + let server = mock_server().await; + let client = Client::new().set_endpoint(server.uri()); + let headers = HashMap::from([("content-type".to_string(), "application/json".to_string())]); + + let _: Value = client + .call(Method::POST, "/", Some(headers), Some(params())) + .await + .unwrap(); + + let requests = server.received_requests().await.unwrap(); + let body: Value = serde_json::from_slice(&requests[0].body).unwrap(); + assert_eq!(body["values"], serde_json::json!([["a", "b"]])); assert_eq!( - flat, - serde_json::json!(["{\"method\":\"limit\",\"values\":[1]}"]) + body["queries"], + serde_json::json!([Query::limit(1).to_string()]) ); } + + #[tokio::test] + async fn query_string_keeps_nested_string_arrays() { + let server = mock_server().await; + let client = Client::new().set_endpoint(server.uri()); + + let _: Value = client + .call(Method::GET, "/", None, Some(params())) + .await + .unwrap(); + + let requests = server.received_requests().await.unwrap(); + let pairs: Vec<(String, String)> = requests[0].url.query_pairs().into_owned().collect(); + assert!(pairs.contains(&("values[]".to_string(), "[\"a\",\"b\"]".to_string()))); + assert!(pairs.contains(&("queries[]".to_string(), Query::limit(1).to_string()))); + assert_eq!(pairs.len(), 2); + } } diff --git a/templates/rust/src/query/mod.rs.twig b/templates/rust/src/query/mod.rs.twig index 0a3eea6057..f74d033d6f 100644 --- a/templates/rust/src/query/mod.rs.twig +++ b/templates/rust/src/query/mod.rs.twig @@ -691,23 +691,6 @@ impl Query { Builder::new() } - pub(crate) fn normalize(value: impl Serialize) -> Vec { - match serde_json::to_value(value) { - Ok(v) => Self::normalize_value(&v), - Err(_) => vec![], - } - } - - pub(crate) fn normalize_value(value: &Value) -> Vec { - match value { - Value::Null => vec![], - Value::String(s) => vec![s.clone()], - Value::Array(items) => items.iter().flat_map(Self::normalize_value).collect(), - Value::Object(_) => vec![value.to_string()], - _ => vec![], - } - } - fn aliased(method: &str, attribute: impl Into, alias: impl Into) -> Self { let alias = alias.into(); let values = if alias.is_empty() { @@ -1670,34 +1653,4 @@ mod tests { ] ); } - - #[test] - fn test_normalize_builder() { - let builder = Query::builder().equal("status", "active").limit(10); - let expected = builder.build(); - assert_eq!(Query::normalize(builder.clone()), expected); - assert_eq!(Query::normalize(vec![builder.clone()]), expected); - assert_eq!(Query::normalize(vec![builder.build()]), expected); - } - - #[test] - fn test_normalize_page_wrapped_in_array() { - let normalized = Query::normalize(vec![ - Value::String(Query::equal("status", "active").to_string()), - Value::Array( - Query::page(2, 10) - .into_iter() - .map(|query| Value::String(query.to_string())) - .collect(), - ), - ]); - assert_eq!( - normalized, - vec![ - Query::equal("status", "active").to_string(), - Query::limit(10).to_string(), - Query::offset(10).to_string(), - ] - ); - } } diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 5fa1d03a06..5e00f3d83a 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -724,53 +724,35 @@ open class Client { } public func flatten(_ value: Any?) -> Any? { + return flattened(value).value + } + + private func flattened(_ value: Any?) -> (value: Any?, changed: Bool) { if let builder = value as? Query.Builder { - return builder.build() + return (builder.build(), true) } if let query = value as? Query { - return query.description - } - if let builders = value as? [Query.Builder] { - return builders.flatMap { $0.build() } + return (query.description, true) } - if let queries = value as? [Query] { - return queries.map { $0.description } - } - if let strings = value as? [String] { - return strings + if value is [String] { + return (value, false) } guard let items = arrayElements(value) else { - return value + return (value, false) } var out: [Any] = [] var changed = false for item in items { - let flat = flatten(item) - if let strings = flat as? [String], !(item is any StringProtocol) { - out.append(contentsOf: strings) - changed = true - continue - } - if item is Query.Builder || item is Query { - if let list = arrayElements(flat) { - out.append(contentsOf: list) - } else if let flat { - out.append(flat) - } - changed = true - continue - } - if arrayElements(item) != nil, let list = arrayElements(flat), list.allSatisfy({ $0 is String }) { - out.append(contentsOf: list) - changed = true - continue - } - if let flat { - out.append(flat) + let flat = flattened(item) + changed = changed || flat.changed + if item is Query.Builder, let queries = flat.value as? [String] { + out.append(contentsOf: queries) + } else if let entry = flat.value { + out.append(entry) } } - return changed ? out : value + return changed ? (out, true) : (value, false) } private func arrayElements(_ value: Any?) -> [Any]? { diff --git a/templates/swift/Tests/Tests.swift.twig b/templates/swift/Tests/Tests.swift.twig index 521568bc09..267523b7c8 100644 --- a/templates/swift/Tests/Tests.swift.twig +++ b/templates/swift/Tests/Tests.swift.twig @@ -122,19 +122,21 @@ class Tests: XCTestCase { XCTAssertEqual(method(queries[1]), "offset") } - func queryStrings(_ value: Any?) -> [String]? { - if let list = value as? [String] { - return list + func queries(_ value: Any?) -> [NSDictionary]? { + guard let list = value as? [Any] else { + return nil } - if let list = value as? [Any] { - return list.map { String(describing: $0) } + return list.compactMap { item in + guard let raw = item as? String else { + return nil + } + return try? JSONSerialization.jsonObject(with: Data(raw.utf8)) as? NSDictionary } - return nil } func testFlattenSpreadsWrappedBuilder() { let client = Client() - XCTAssertEqual(queryStrings(client.flatten([Query.builder().limit(1)] as [Any])), [Query.limit(1)]) + XCTAssertEqual(queries(client.flatten([Query.builder().limit(1)] as [Any])), queries([Query.limit(1)])) } func testFlattenKeepsNestedNumericArrays() { @@ -145,6 +147,37 @@ class Tests: XCTestCase { func testFlattenKeepsQueryStringList() { let client = Client() - XCTAssertEqual(queryStrings(client.flatten([Query.limit(1)] as [Any])), [Query.limit(1)]) + XCTAssertEqual(queries(client.flatten([Query.limit(1)] as [Any])), queries([Query.limit(1)])) + } + + func testFlattenKeepsNestedStringArrays() { + let client = Client() + let nested = [["a", "b"]] + XCTAssertEqual(client.flatten(nested) as? [[String]], nested) + let untyped: [Any] = [["a", "b"]] + XCTAssertEqual((client.flatten(untyped) as? [Any])?.first as? [String], ["a", "b"]) + XCTAssertEqual((client.flatten(["x", ["a", "b"]] as [Any]) as? [Any])?.count, 2) + } + + func testFlattenBuildsBuilder() { + let client = Client() + let builder = Query.builder().equal("status", value: "active").limit(1) + XCTAssertEqual(queries(client.flatten(builder)), queries(builder.build())) + } + + func testFlattenSpreadsBuildersBesideStrings() { + let client = Client() + let builder = Query.builder().limit(1) + XCTAssertEqual( + queries(client.flatten([Query.offset(2), builder] as [Any])), + queries([Query.offset(2), Query.limit(1)]) + ) + } + + func testFlattenSpreadsBuildersInsideNestedList() { + let client = Client() + let builder = Query.builder().limit(1) + let flat = client.flatten([[builder]] as [Any]) as? [Any] + XCTAssertEqual(queries(flat?.first), queries([Query.limit(1)])) } } diff --git a/templates/web/src/query.ts.twig b/templates/web/src/query.ts.twig index 8fdb9ec9da..f4c2447a1c 100644 --- a/templates/web/src/query.ts.twig +++ b/templates/web/src/query.ts.twig @@ -1426,24 +1426,27 @@ export function normalizeQueries(queries?: QueryInput | null): string[] { return out; } -function isQueryList(value: unknown[]): boolean { - return value.every( - (item) => - item instanceof Query.Builder || - item instanceof Query || - typeof item === 'string' || - (Array.isArray(item) && isQueryList(item)), - ); +function isQueryValue(value: unknown): value is Query | Query.Builder { + return value instanceof Query.Builder || value instanceof Query; } export function flattenParam(value: unknown): unknown { - if (value instanceof Query.Builder || value instanceof Query) { - return normalizeQueries(value as QueryInput); + if (isQueryValue(value)) { + return normalizeQueries(value); } - if (Array.isArray(value) && isQueryList(value)) { - return normalizeQueries(value as QueryInput); + if (!Array.isArray(value) || !value.some(isQueryValue)) { + return value; } - return value; + + const out: unknown[] = []; + for (const item of value) { + if (isQueryValue(item)) { + out.push(...normalizeQueries(item)); + } else { + out.push(item); + } + } + return out; } export const QueryBuilder = Query.Builder; From 8bcead5dd7b7748f0b9e8ac5b43e03b6f96b452b Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Thu, 15 Oct 2026 17:30:24 +1300 Subject: [PATCH 42/42] test(go): read request bodies with ioutil for go 1.13 io.ReadAll only exists from Go 1.16, and the generated SDK still builds on 1.13. Co-Authored-By: Claude Opus 5.5 --- templates/go/client_test.go.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/go/client_test.go.twig b/templates/go/client_test.go.twig index bcf65e425c..db5807e2ac 100644 --- a/templates/go/client_test.go.twig +++ b/templates/go/client_test.go.twig @@ -4,7 +4,7 @@ package client import ( "crypto/tls" "encoding/json" - "io" + "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -339,7 +339,7 @@ func TestCallSendsFlattenedParams(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rawQuery = r.URL.Query() if r.Method != http.MethodGet { - data, _ := io.ReadAll(r.Body) + data, _ := ioutil.ReadAll(r.Body) json.Unmarshal(data, &body) } w.Header().Set("content-type", "application/json")