From 230db7b5a2bc8221cc17355f40db146b3b98cab0 Mon Sep 17 00:00:00 2001 From: Leo Lamprecht Date: Thu, 14 Nov 2024 16:19:29 +0100 Subject: [PATCH] Added support for expressions in `including` (#32) --- src/instructions/selecting.ts | 38 +++++++++++++++++-------- tests/instructions/including.test.ts | 42 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/instructions/selecting.ts b/src/instructions/selecting.ts index edd81ac..ea36480 100644 --- a/src/instructions/selecting.ts +++ b/src/instructions/selecting.ts @@ -2,7 +2,11 @@ import type { Model } from '@/src/types/model'; import type { Instructions } from '@/src/types/query'; import { flatten } from '@/src/utils/helpers'; import { getFieldFromModel } from '@/src/utils/model'; -import { getSymbol, prepareStatementValue } from '@/src/utils/statement'; +import { + getSymbol, + parseFieldExpression, + prepareStatementValue, +} from '@/src/utils/statement'; /** * Generates the SQL syntax for the `selecting` query instruction, which allows for @@ -38,19 +42,29 @@ export const handleSelecting = ( // If additional fields (that are not part of the model) were provided in the // `including` instruction, add ephemeral (non-stored) columns for those fields. if (instructions.including) { + // Filter out any fields whose value is a sub query, as those fields are instead + // converted into SQL JOINs in the `handleIncluding` function, which, in the case of + // sub queries resulting in a single record, is more performance-efficient, and in + // the case of sub queries resulting in multiple records, it's the only way to + // include multiple rows of another table. const filteredObject = Object.entries(instructions.including) - // Filter out any fields whose value is a sub query, as those fields are instead - // converted into SQL JOINs in the `handleIncluding` function, which, in the case of - // sub queries resulting in a single record, is more performance-efficient, and in - // the case of sub queries resulting in multiple records, it's the only way to - // include multiple rows of another table. - .filter(([_, value]) => { + .map(([key, value]) => { const symbol = getSymbol(value); - const hasQuery = symbol?.type === 'query'; - if (hasQuery) isJoining = true; - return !hasQuery; - }); + if (symbol) { + if (symbol.type === 'query') { + isJoining = true; + return null; + } + + if (symbol.type === 'expression') { + value = parseFieldExpression(model, 'including', symbol.value); + } + } + + return [key, value]; + }) + .filter((entry) => entry !== null); // Flatten the object to handle deeply nested ephemeral fields, which are the result // of developers providing objects as values in the `including` instruction. @@ -62,6 +76,8 @@ export const handleSelecting = ( statement += newObjectEntries // Format the fields into a comma-separated list of SQL columns. .map(([key, value]) => { + if (typeof value === 'string' && value.startsWith('"')) + return `(${value}) as "${key}"`; return `${prepareStatementValue(statementParams, value)} as "${key}"`; }) .join(', '); diff --git a/tests/instructions/including.test.ts b/tests/instructions/including.test.ts index dc28a73..2048b4c 100644 --- a/tests/instructions/including.test.ts +++ b/tests/instructions/including.test.ts @@ -312,6 +312,48 @@ test('get single record including ephemeral field', () => { ]); }); +test('get single record including ephemeral field containing expression', () => { + const queries: Array = [ + { + get: { + account: { + including: { + name: { + [RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD}firstName || ' ' || ${RONIN_MODEL_SYMBOLS.FIELD}lastName`, + }, + }, + }, + }, + }, + ]; + + const models: Array = [ + { + slug: 'account', + fields: [ + { + slug: 'firstName', + type: 'string', + }, + { + slug: 'lastName', + type: 'string', + }, + ], + }, + ]; + + const statements = compileQueries(queries, models); + + expect(statements).toEqual([ + { + statement: `SELECT *, ("firstName" || ' ' || "lastName") as "name" FROM "accounts" LIMIT 1`, + params: [], + returning: true, + }, + ]); +}); + test('get single record including deeply nested ephemeral field', () => { const queries: Array = [ {