Skip to content

Commit

Permalink
Added support for expressions in including (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
leo authored Nov 14, 2024
1 parent 4367966 commit 230db7b
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 11 deletions.
38 changes: 27 additions & 11 deletions src/instructions/selecting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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(', ');
Expand Down
42 changes: 42 additions & 0 deletions tests/instructions/including.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,48 @@ test('get single record including ephemeral field', () => {
]);
});

test('get single record including ephemeral field containing expression', () => {
const queries: Array<Query> = [
{
get: {
account: {
including: {
name: {
[RONIN_MODEL_SYMBOLS.EXPRESSION]: `${RONIN_MODEL_SYMBOLS.FIELD}firstName || ' ' || ${RONIN_MODEL_SYMBOLS.FIELD}lastName`,
},
},
},
},
},
];

const models: Array<Model> = [
{
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<Query> = [
{
Expand Down

0 comments on commit 230db7b

Please sign in to comment.