diff --git a/lib/connectors/mongodb-connector.ts b/lib/connectors/mongodb-connector.ts index 902c4ea..6ea460b 100644 --- a/lib/connectors/mongodb-connector.ts +++ b/lib/connectors/mongodb-connector.ts @@ -109,6 +109,19 @@ export class MongoDBConnector implements Connector { }, {}); } + if (queryDescription.whereNulls) { + wheres = queryDescription.whereNulls.reduce((prev, curr) => { + const mongoOperator = "$exists"; + + return { + ...prev, + [curr.field]: { + [mongoOperator]: curr.notNull, + }, + }; + }, {}); + } + let results: any[] = []; switch (queryDescription.type) { diff --git a/lib/model.ts b/lib/model.ts index 0ee72ed..f4bbddd 100644 --- a/lib/model.ts +++ b/lib/model.ts @@ -635,6 +635,32 @@ export class Model { return this; } + + /** Add a `where` clause to your query that gets a record if field is null + * + * await Flight.whereNull("id").get(); + * + */ + static whereNull( + this: T, + field: string + ) { + this._currentQuery.whereNull(this.formatFieldToDatabase(field) as string); + return this; + } + + /** Add a `where` clause to your query that gets a record if field is not null + * + * await Flight.whereNotNull("id").get(); + * + */ + static whereNotNull( + this: T, + field: string + ) { + this._currentQuery.whereNotNull(this.formatFieldToDatabase(field) as string); + return this; + } /** Update one or multiple records. Also update `updated_at` if `timestamps` is `true`. * diff --git a/lib/query-builder.ts b/lib/query-builder.ts index cd9e0e6..6aa8dc3 100644 --- a/lib/query-builder.ts +++ b/lib/query-builder.ts @@ -36,6 +36,11 @@ export type WhereInClause = { possibleValues: FieldValue[]; }; +export type WhereNullClause = { + field: string; + notNull: boolean; +} + export type OrderByClauses = { [field: string]: OrderDirection; }; @@ -48,6 +53,7 @@ export type QueryDescription = { orderBy?: OrderByClauses; groupBy?: string; wheres?: WhereClause[]; + whereNulls?: WhereNullClause[]; whereIn?: WhereInClause; joins?: JoinClause[]; leftOuterJoins?: JoinClause[]; @@ -202,6 +208,38 @@ export class QueryBuilder { return this; } + whereNull( + field: string, + notNull = false, + ) { + if (!this._query.whereNulls) { + this._query.whereNulls = []; + } + + const existingWhereForFieldIndex = this._query.whereNulls.findIndex((where) => + where.field === field + ); + + const whereNullClause: WhereNullClause = { + field, + notNull: notNull, + } + + if (existingWhereForFieldIndex === -1) { + this._query.whereNulls.push(whereNullClause); + } else { + this._query.whereNulls[existingWhereForFieldIndex] = whereNullClause; + } + + return this; + } + + whereNotNull( + field: string + ) { + return this.whereNull(field, true); + } + update(values: Values) { this._query.type = "update"; this._query.values = values; diff --git a/lib/translators/sql-translator.ts b/lib/translators/sql-translator.ts index 9b2e67a..8cfd2ed 100644 --- a/lib/translators/sql-translator.ts +++ b/lib/translators/sql-translator.ts @@ -86,6 +86,16 @@ export class SQLTranslator implements Translator { }); } + if (query.whereNulls) { + query.whereNulls.forEach((whereNull) => { + if (whereNull.notNull) { + queryBuilder = queryBuilder.whereNotNull(whereNull.field); + } else { + queryBuilder = queryBuilder.whereNull(whereNull.field); + } + }); + } + if (query.joins) { query.joins.forEach((join) => { queryBuilder = queryBuilder.join( diff --git a/tests/connection.ts b/tests/connection.ts index ba54c23..d451210 100644 --- a/tests/connection.ts +++ b/tests/connection.ts @@ -1,5 +1,5 @@ import { config } from "https://deno.land/x/dotenv/mod.ts"; -import { Database } from "../mod.ts"; +import { Database, MySQLConnector, SQLite3Connector } from "../mod.ts"; const env = config(); @@ -16,25 +16,21 @@ const defaultSQLiteOptions = { }; const getMySQLConnection = (options = {}, debug = true): Database => { - const connection: Database = new Database( - { dialect: "mysql", debug }, - { - ...defaultMySQLOptions, - ...options, - }, - ); + const connector = new MySQLConnector({ + ...defaultMySQLOptions, + ...options + }); + const connection: Database = new Database({ connector, debug }) return connection; }; const getSQLiteConnection = (options = {}, debug = true): Database => { - const connection: Database = new Database( - { dialect: "sqlite3", debug }, - { - ...defaultSQLiteOptions, - ...options, - }, - ); + const connector = new SQLite3Connector({ + ...defaultSQLiteOptions, + ...options + }); + const connection: Database = new Database({ connector, debug }); return connection; }; diff --git a/tests/units/queries/sqlite/response.test.ts b/tests/units/queries/sqlite/response.test.ts index bd73253..2dced74 100644 --- a/tests/units/queries/sqlite/response.test.ts +++ b/tests/units/queries/sqlite/response.test.ts @@ -107,3 +107,45 @@ Deno.test("SQLite: Response model", async () => { await connection.close(); }); + +Deno.test("SQLite: Response model, Query Null Fields", async () => { + const connection = getSQLiteConnection(); + connection.link([Article]); + await connection.sync({ drop: true }); + + await Article.create([ + { title: "hola" }, + { title: "hola mundo!", content: "not the first article!" }, + ]); + + const selectNullFieldResponse = await Article.whereNull('content').all(); + + assertEquals( + selectNullFieldResponse.length, + 1, + "Select expected only one record" + ); + + const selectNullFieldResponseChain = await Article.where('title', 'hola').whereNull('content').all(); + + assertEquals( + selectNullFieldResponseChain.length, + 1, + "Select expected only one record" + ); + + assertEquals( + selectNullFieldResponse[0].title, + 'hola', + "Select expected record with null content" + ) + + const selectNotNullFieldResponseChain = await Article.where('title', 'hola').whereNotNull('content').all(); + assertEquals( + selectNotNullFieldResponseChain.length, + 0, + "Select expected no record" + ); + + await connection.close(); +}); \ No newline at end of file