Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ export type EntityLoaderFieldNameConstructorFn<

/**
* Specification for a search field that is a manually constructed SQLFragment. Useful for complex search fields that require
* transformations or combinations of multiple fields, such as `COALESCE(NULLIF(display_name, ''), split_part(full_name, '/', 2))`
* to search by display name with fallback to full name.
* transformations to make nullable fields non-null or to make combinations of multiple fields.
*/
export type EntityLoaderSearchFieldSQLFragmentFnSpecification<
TFields extends Record<string, any>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,12 @@ export class PostgresEntityDatabaseAdapter<
orderBySpecification.nulls,
);
} else {
const orderDirection = orderBySpecification.order === 'asc' ? 'ASC' : 'DESC';
const nullsSuffix = orderBySpecification.nulls
? ` NULLS ${orderBySpecification.nulls === NullsOrdering.FIRST ? 'FIRST' : 'LAST'}`
: '';
ret = ret.orderByRaw(
`(${orderBySpecification.columnFragment.sql}) ${orderBySpecification.order}${nullsSuffix}`,
`(${orderBySpecification.columnFragment.sql}) ${orderDirection}${nullsSuffix}`,
orderBySpecification.columnFragment.getKnexBindings((fieldName) =>
getDatabaseFieldForEntityField(this.entityConfiguration, fieldName),
),
Expand Down
39 changes: 31 additions & 8 deletions packages/entity-database-adapter-knex/src/SQLOperator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ export class SQLFragment<TFields extends Record<string, any>> {
if (match === '??' && binding.type === 'identifier') {
// For identifiers, show them quoted as they would appear
return `"${binding.name.replace(/"/g, '""')}"`;
} else if (match === '??' && binding.type === 'entityField') {
// For entity fields, show the entity field name as the identifier for debugging
return `"${binding.fieldName.toString()}"`;
} else if (match === '?' && binding.type === 'value') {
return SQLFragment.formatDebugValue(binding.value);
} else {
Expand Down Expand Up @@ -309,6 +312,18 @@ type PickSupportedSQLValueKeys<T> = {
[K in keyof T]: T[K] extends SupportedSQLValue ? K : never;
}[keyof T];

type PickStringValueKeys<T> = {
[K in keyof T]: T[K] extends string | null | undefined ? K : never;
}[keyof T];

type JsonSerializable =
| string
| number
| boolean
| null
| readonly JsonSerializable[]
| { readonly [key: string]: JsonSerializable };

/**
* Common SQL helper functions for building queries
*/
Expand Down Expand Up @@ -385,7 +400,7 @@ export const SQLFragmentHelpers = {
* ```
*/
like<TFields extends Record<string, any>>(
fieldName: keyof TFields,
fieldName: PickStringValueKeys<TFields>,
pattern: string,
): SQLFragment<TFields> {
return sql`${entityField(fieldName)} LIKE ${pattern}`;
Expand All @@ -395,7 +410,7 @@ export const SQLFragmentHelpers = {
* NOT LIKE helper
*/
notLike<TFields extends Record<string, any>>(
fieldName: keyof TFields,
fieldName: PickStringValueKeys<TFields>,
pattern: string,
): SQLFragment<TFields> {
return sql`${entityField(fieldName)} NOT LIKE ${pattern}`;
Expand All @@ -405,7 +420,7 @@ export const SQLFragmentHelpers = {
* ILIKE helper for case-insensitive matching
*/
ilike<TFields extends Record<string, any>>(
fieldName: keyof TFields,
fieldName: PickStringValueKeys<TFields>,
pattern: string,
): SQLFragment<TFields> {
return sql`${entityField(fieldName)} ILIKE ${pattern}`;
Expand All @@ -415,7 +430,7 @@ export const SQLFragmentHelpers = {
* NOT ILIKE helper for case-insensitive non-matching
*/
notIlike<TFields extends Record<string, any>>(
fieldName: keyof TFields,
fieldName: PickStringValueKeys<TFields>,
pattern: string,
): SQLFragment<TFields> {
return sql`${entityField(fieldName)} NOT ILIKE ${pattern}`;
Expand Down Expand Up @@ -506,19 +521,27 @@ export const SQLFragmentHelpers = {
*/
jsonContains<TFields extends Record<string, any>>(
fieldName: keyof TFields,
value: unknown,
value: JsonSerializable,
): SQLFragment<TFields> {
return sql`${entityField(fieldName)} @> ${JSON.stringify(value)}::jsonb`;
const serialized = JSON.stringify(value);
if (serialized === undefined) {
throw new Error('jsonContains: value is not JSON-serializable');
}
return sql`${entityField(fieldName)} @> ${serialized}::jsonb`;
},

/**
* JSON contained by operator (\<\@\)
*/
jsonContainedBy<TFields extends Record<string, any>>(
fieldName: keyof TFields,
value: unknown,
value: JsonSerializable,
): SQLFragment<TFields> {
return sql`${entityField(fieldName)} <@ ${JSON.stringify(value)}::jsonb`;
const serialized = JSON.stringify(value);
if (serialized === undefined) {
throw new Error('jsonContainedBy: value is not JSON-serializable');
}
return sql`${entityField(fieldName)} <@ ${serialized}::jsonb`;
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3668,9 +3668,9 @@ describe('postgres entity integration', () => {
}

const paginationSpec: PaginationSpecification<PostgresTestEntityFields> = {
strategy: PaginationStrategy.TRIGRAM_SEARCH as const,
strategy: PaginationStrategy.TRIGRAM_SEARCH,
term: 'Johnson',
fields: ['label' as const],
fields: ['label'],
threshold: 0.2,
extraOrderByFields: [
{
Expand Down