diff --git a/package.json b/package.json index 5f462ac..82c8e9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@endeavorance/prequel", - "version": "2.1.0", + "version": "2.2.0", "exports": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { diff --git a/src/prequel.ts b/src/prequel.ts index 0018eb4..b2c8097 100644 --- a/src/prequel.ts +++ b/src/prequel.ts @@ -27,7 +27,10 @@ export class PrequelKVStore { primary: true, autoincrement: true, }, - key: "TEXT", + key: { + type: "TEXT", + unique: true, + }, value: "TEXT", }); } diff --git a/src/table.ts b/src/table.ts index a8593b1..b9f7beb 100644 --- a/src/table.ts +++ b/src/table.ts @@ -136,27 +136,36 @@ export class Table { .map((colName) => `$${colName}`) .join(", "); - this._createTableSQL = `CREATE TABLE IF NOT EXISTS "${name}" (${columnSQL});`; - const insertSql = `INSERT INTO "${name}" (${allCols}) VALUES (${allColVars}) RETURNING *;`; - const updateSql = `UPDATE "${name}" SET ${updateColumnSQL} WHERE "${this._primaryColumnName}" = $${this._primaryColumnName} RETURNING *;`; - const getByIdSql = `SELECT * FROM "${name}" WHERE "${this._primaryColumnName}" = ? LIMIT 1;`; - const delByIdSql = `DELETE FROM "${name}" WHERE "${this._primaryColumnName}" = ?;`; - const deleteRowSql = `DELETE FROM "${name}" WHERE "${this._primaryColumnName}" = $${this._primaryColumnName};`; - const truncateQuery = `DELETE FROM "${name}";`; - // Ensure the table exists in the database + this._createTableSQL = `CREATE TABLE IF NOT EXISTS "${name}" (${columnSQL});`; this.db.exec(this._createTableSQL); // Prepare common queries - this._insertQuery = this.db.query(insertSql); - this._updateQuery = this.db.query(updateSql); - this._findQuery = this.db.query(getByIdSql); - this._deleteQuery = this.db.query(delByIdSql); + this._insertQuery = this.db.query( + `INSERT INTO "${name}" (${allCols}) VALUES (${allColVars}) RETURNING *;`, + ); + + this._updateQuery = this.db.query( + `UPDATE "${name}" SET ${updateColumnSQL} WHERE "${this._primaryColumnName}" = $${this._primaryColumnName} RETURNING *;`, + ); + + this._findQuery = this.db.query( + `SELECT * FROM "${name}" WHERE "${this._primaryColumnName}" = ? LIMIT 1;`, + ); + + this._deleteQuery = this.db.query( + `DELETE FROM "${name}" WHERE "${this._primaryColumnName}" = ?;`, + ); + this._sizeQuery = this.db.query<{ count: number }, []>( `SELECT count(*) as count FROM ${name}`, ); - this._deleteRowQuery = this.db.query(deleteRowSql); - this._truncateQuery = this.db.query(truncateQuery); + + this._deleteRowQuery = this.db.query( + `DELETE FROM "${name}" WHERE "${this._primaryColumnName}" = $${this._primaryColumnName};`, + ); + + this._truncateQuery = this.db.query(`DELETE FROM "${name}"`); // If using with a Prequel instance, ensure the table is attached // This is only for when creating tables directly and passing in a Prequel instance @@ -424,12 +433,34 @@ export class Table { * The keys are column names and the values are the values to match. * @returns An array of rows that match the specified conditions. */ - public findAllWhere(conditions: Partial): RowShape[] { + public findAllWhere( + conditions: Partial, + options?: { + order?: string; + limit?: number; + skip?: number; + }, + ): RowShape[] { const keys = Object.keys(conditions); const whereParts = keys.map((key) => `"${key}" = $${key}`); const whereClause = whereParts.join(" AND "); + + const optionParts: string[] = []; + + if (options?.order) { + optionParts.push(`ORDER BY ${options.order}`); + } + + if (options?.limit) { + optionParts.push(`LIMIT ${options.limit}`); + + if (options?.skip) { + optionParts.push(`OFFSET ${options.skip}`); + } + } + const query = this._db.prepare( - `SELECT * FROM ${this} WHERE ${whereClause};`, + `SELECT * FROM ${this} WHERE ${whereClause} ${optionParts.join(" ")};`, ); return query.all(shapeForQuery(conditions)); diff --git a/test/table.test.ts b/test/table.test.ts index dfe8bce..ba48354 100644 --- a/test/table.test.ts +++ b/test/table.test.ts @@ -222,6 +222,45 @@ test(".findAllWhere() returns a row based on basic criteria", () => { expect(manyJacks[2]).toEqual({ user_id: 3, name: "Jack" }); }); +test(".findAllWhere() can set a limit on returned rows", () => { + const table = makeTestTable(); + table.insertPartial({ name: "Jack" }); + table.insertPartial({ name: "Jack" }); + table.insertPartial({ name: "Jack" }); + + const manyJacks = table.findAllWhere({ name: "Jack" }, { limit: 1 }); + + expect(manyJacks).toHaveLength(1); + expect(manyJacks[0]).toEqual({ user_id: 1, name: "Jack" }); +}); + +test(".findAllWhere() can set an offset on a limit", () => { + const table = makeTestTable(); + table.insertPartial({ name: "Jack" }); + table.insertPartial({ name: "Jack" }); + table.insertPartial({ name: "Jack" }); + + const manyJacks = table.findAllWhere({ name: "Jack" }, { limit: 1, skip: 1 }); + + expect(manyJacks).toHaveLength(1); + expect(manyJacks[0]).toEqual({ user_id: 2, name: "Jack" }); +}); + +test(".findAllWhere() can set an order on the results", () => { + const table = makeTestTable(); + table.insertPartial({ name: "Jack" }); + table.insertPartial({ name: "Jack" }); + table.insertPartial({ name: "Jack" }); + + const manyJacks = table.findAllWhere( + { name: "Jack" }, + { order: "user_id DESC" }, + ); + + expect(manyJacks).toHaveLength(3); + expect(manyJacks[0]).toEqual({ user_id: 3, name: "Jack" }); +}); + test(".findOneById() returns a single element by its primary column", () => { const table = makeTestTable();