/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ import { generateSQLExpressions } from 'api/utils/sqlite-query'; import { module, test } from 'qunit'; module('Unit | Utility | sqlite-query', function (hooks) { const date = new Date(2025, 1, 1); const isoDateString = date.toISOString(); hooks.beforeEach(function () { String.prototype.removeExtraWhiteSpace = function () { return this.replace(/^\s+/gm, '').trim(); }; }); hooks.afterEach(function () { delete String.prototype.removeExtraWhiteSpace; }); // TODO: Add a normal LIKE search when we add a resource that uses it test('it generates search correctly with FTS5', function (assert) { const query = { search: 'favorite' }; const { sql, parameters } = generateSQLExpressions('target', query); assert.strictEqual( sql, ` SELECT * FROM target WHERE rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?) ORDER BY created_time DESC`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, ['"favorite"*']); }); test('it grabs resources not in the model mapping', function (assert) { const query = { filters: { id: [{ equals: 'tokenKey' }] }, }; const { sql, parameters } = generateSQLExpressions('token', query); assert.strictEqual( sql, ` SELECT * FROM token WHERE (id = ?)`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, ['tokenKey']); }); test('it executes count queries correctly', function (assert) { const select = { select: ['count(*) as total'], }; const { sql, parameters } = generateSQLExpressions('target', {}, select); assert.strictEqual( sql, ` SELECT count(*) as total FROM target ORDER BY created_time DESC`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, []); }); test.each( 'it generates filters correctly', { equals: { query: { filters: { type: [{ equals: 'ssh' }], }, }, expectedWhereClause: 'WHERE (type = ?)', expectedParams: ['ssh'], }, notEquals: { query: { filters: { type: [{ notEquals: 'ssh' }], }, }, expectedWhereClause: 'WHERE (type != ?)', expectedParams: ['ssh'], }, contains: { query: { filters: { type: [{ contains: 'ssh' }], }, }, expectedWhereClause: 'WHERE (type LIKE ?)', expectedParams: ['%ssh%'], }, greaterThan: { query: { filters: { created_time: [{ gt: date }], numberField: [{ gt: 10 }], }, }, expectedWhereClause: 'WHERE (created_time > ?) AND (numberField > ?)', expectedParams: [isoDateString, 10], }, lessThan: { query: { filters: { created_time: [{ lt: date }], numberField: [{ lt: 10 }], }, }, expectedWhereClause: 'WHERE (created_time < ?) AND (numberField < ?)', expectedParams: [isoDateString, 10], }, greaterThanOrEqual: { query: { filters: { created_time: [{ gte: date }], numberField: [{ gte: 10 }], }, }, expectedWhereClause: 'WHERE (created_time >= ?) AND (numberField >= ?)', expectedParams: [isoDateString, 10], }, lessThanOrEqual: { query: { filters: { created_time: [{ lte: date }], numberField: [{ lte: 10 }], }, }, expectedWhereClause: 'WHERE (created_time <= ?) AND (numberField <= ?)', expectedParams: [isoDateString, 10], }, logicalOperators: { query: { filters: { id: { logicalOperator: 'and', values: [{ notEquals: 'id1' }, { notEquals: 'id2' }], }, status: { logicalOperator: 'or', values: [{ equals: 'active' }, { equals: 'pending' }], }, type: [{ equals: 'ssh' }], }, }, expectedWhereClause: 'WHERE (id != ? AND id != ?) AND (status = ? OR status = ?) AND (type = ?)', expectedParams: ['id1', 'id2', 'active', 'pending', 'ssh'], }, }, function (assert, { query, expectedWhereClause, expectedParams }) { const { sql, parameters } = generateSQLExpressions('target', query); assert.strictEqual( sql, ` SELECT * FROM target ${expectedWhereClause} ORDER BY created_time DESC`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, expectedParams); }, ); test.each( 'it generates sort direction correctly', [ ['asc', 'ASC'], ['desc', 'DESC'], ], function (assert, [direction, expectedDirection]) { const query = { sort: { attribute: 'name', direction }, }; const { sql, parameters } = generateSQLExpressions('target', query); assert.strictEqual( sql, ` SELECT * FROM target ORDER BY name ${expectedDirection}`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, []); }, ); test.each( 'it generates pagination clause', [ { page: 1, pageSize: 10, expectedParams: [10, 0] }, { page: 3, pageSize: 10, expectedParams: [10, 20] }, { page: 7, pageSize: 50, expectedParams: [50, 300] }, ], function (assert, { page, pageSize, expectedParams }) { const { sql, parameters } = generateSQLExpressions( 'target', {}, { page, pageSize }, ); assert.strictEqual( sql, ` SELECT * FROM target ORDER BY created_time DESC LIMIT ? OFFSET ?`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, expectedParams); }, ); test('it generates FTS5 search with filters', function (assert) { const query = { search: 'favorite', filters: { id: { logicalOperator: 'and', values: [{ notEquals: 'id1' }, { notEquals: 'id2' }], }, status: [{ equals: 'active' }, { equals: 'pending' }], }, }; const { sql, parameters } = generateSQLExpressions('target', query); assert.strictEqual( sql, ` SELECT * FROM target WHERE (id != ? AND id != ?) AND (status = ? OR status = ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?) ORDER BY created_time DESC`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, [ 'id1', 'id2', 'active', 'pending', '"favorite"*', ]); }); test('it generates SQL with all clauses combined', function (assert) { const query = { search: 'favorite', filters: { type: [{ equals: 'ssh' }], status: { logicalOperator: 'or', values: [{ equals: 'active' }, { equals: 'pending' }], }, created_time: [{ gte: date }], }, sort: { attribute: 'name', direction: 'desc' }, }; const { sql, parameters } = generateSQLExpressions('target', query, { page: 2, pageSize: 15, select: ['data'], }); assert.strictEqual( sql, ` SELECT data FROM target WHERE (type = ?) AND (status = ? OR status = ?) AND (created_time >= ?) AND rowid IN (SELECT rowid FROM target_fts WHERE target_fts MATCH ?) ORDER BY name DESC LIMIT ? OFFSET ?`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, [ 'ssh', 'active', 'pending', isoDateString, '"favorite"*', 15, 15, ]); }); test.each( 'it handles empty and invalid fields', { emptyObject: {}, emptySearch: { search: '' }, emptyFilter: { filters: {} }, emptyFilterAttributes: { filters: { type: [] } }, invalidSortAttribute: { attribute: 'test', direction: 'asc' }, onlyPage: { page: 10 }, onlyPageSize: { pageSize: 5 }, }, function (assert, query) { const { sql, parameters } = generateSQLExpressions('target', query); assert.strictEqual( sql, ` SELECT * FROM target ORDER BY created_time DESC`.removeExtraWhiteSpace(), ); assert.deepEqual(parameters, []); }, ); });