@tstdl/base 0.93.9 → 0.93.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/audit/module.d.ts +3 -3
- package/audit/module.js +3 -3
- package/document-management/api/document-management.api.d.ts +6 -6
- package/document-management/service-models/document.service-model.d.ts +3 -3
- package/injector/decorators.d.ts +7 -0
- package/injector/decorators.js +10 -6
- package/injector/injector.js +73 -30
- package/orm/decorators.d.ts +35 -3
- package/orm/decorators.js +6 -0
- package/orm/query.d.ts +65 -30
- package/orm/query.js +2 -6
- package/orm/repository.types.d.ts +72 -1
- package/orm/server/drizzle/schema-converter.js +31 -2
- package/orm/server/query-converter.d.ts +5 -7
- package/orm/server/query-converter.js +69 -15
- package/orm/server/repository.d.ts +19 -7
- package/orm/server/repository.js +144 -11
- package/orm/sqls.d.ts +153 -8
- package/orm/sqls.js +161 -8
- package/package.json +5 -5
- package/schema/schemas/object.js +1 -1
- package/test/drizzle/0000_sudden_sphinx.sql +9 -0
- package/test/drizzle/meta/0000_snapshot.json +79 -0
- package/test/drizzle/meta/_journal.json +13 -0
- package/test/drizzle.config.d.ts +2 -0
- package/test/drizzle.config.js +11 -0
- package/test/index.d.ts +3 -0
- package/test/index.js +3 -0
- package/test/module.d.ts +6 -0
- package/test/module.js +17 -0
- package/test/schemas.d.ts +3 -0
- package/test/schemas.js +4 -0
- package/test/test.model.d.ts +8 -0
- package/test/test.model.js +345 -0
- package/test1.d.ts +1 -0
- package/test1.js +59 -0
- package/test2.d.ts +1 -0
- package/test2.js +32 -0
- package/test3.d.ts +1 -0
- package/test3.js +47 -0
- package/test4.d.ts +23 -0
- package/test4.js +168 -0
- package/test5.d.ts +1 -0
- package/test5.js +22 -0
- package/test6.d.ts +1 -0
- package/test6.js +53 -0
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
* Defines types used by ORM repositories for operations like loading, updating, and creating entities.
|
|
4
4
|
* Includes types for ordering, loading options, and entity data structures for create/update operations.
|
|
5
5
|
*/
|
|
6
|
-
import type { Paths, Record, TypedOmit } from '../types/index.js';
|
|
6
|
+
import type { Paths, Record, SimplifyObject, TypedOmit } from '../types/index.js';
|
|
7
7
|
import type { UntaggedDeep } from '../types/tagged.js';
|
|
8
8
|
import type { SQL, SQLWrapper } from 'drizzle-orm';
|
|
9
9
|
import type { PartialDeep } from 'type-fest';
|
|
10
10
|
import type { Entity, EntityMetadata, EntityWithoutMetadata } from './entity.js';
|
|
11
|
+
import type { FullTextSearchQuery, Query } from './query.js';
|
|
12
|
+
import type { TsHeadlineOptions } from './sqls.js';
|
|
11
13
|
type WithSql<T> = {
|
|
12
14
|
[P in keyof T]: T[P] extends Record ? WithSql<T[P]> : (T[P] | SQL);
|
|
13
15
|
};
|
|
@@ -53,6 +55,75 @@ export type LoadManyOptions<T extends EntityWithoutMetadata> = LoadOptions<T> &
|
|
|
53
55
|
limit?: number;
|
|
54
56
|
distinct?: boolean | TargetColumn<T>[];
|
|
55
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Options for ranking search results.
|
|
60
|
+
*/
|
|
61
|
+
export type RankOptions = {
|
|
62
|
+
/**
|
|
63
|
+
* Array of four numbers to weight D, C, B, and A labels respectively.
|
|
64
|
+
* Defaults to PostgreSQL's default {0.1, 0.2, 0.4, 1.0}.
|
|
65
|
+
*/
|
|
66
|
+
weights?: [number, number, number, number];
|
|
67
|
+
/**
|
|
68
|
+
* Specifies how a document's length should impact its rank.
|
|
69
|
+
* It's a bitmask, e.g., 0 (default) ignores length, 2 divides by document length.
|
|
70
|
+
* See PostgreSQL documentation for `ts_rank_cd` for more details.
|
|
71
|
+
*/
|
|
72
|
+
normalization?: number;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Options for highlighting search results.
|
|
76
|
+
*/
|
|
77
|
+
export type HighlightOptions<T extends EntityWithoutMetadata> = {
|
|
78
|
+
/**
|
|
79
|
+
* The source to generate the highlight from. Can be one or more property paths or a raw SQL expression.
|
|
80
|
+
*/
|
|
81
|
+
source: TargetColumnPaths<T> | SQL<string>;
|
|
82
|
+
} & TsHeadlineOptions;
|
|
83
|
+
/**
|
|
84
|
+
* Options for the `search` method.
|
|
85
|
+
* @template T - The entity type.
|
|
86
|
+
*/
|
|
87
|
+
export type SearchOptions<T extends EntityWithoutMetadata> = SimplifyObject<FullTextSearchQuery<T>['$fts'] & TypedOmit<LoadManyOptions<T>, 'order'> & {
|
|
88
|
+
/**
|
|
89
|
+
* An additional filter to apply to the search query.
|
|
90
|
+
*/
|
|
91
|
+
filter?: Query<T>;
|
|
92
|
+
/**
|
|
93
|
+
* How to order the search results.
|
|
94
|
+
*/
|
|
95
|
+
order?: Order<T> | ((columns: {
|
|
96
|
+
score: SQL | SQL.Aliased<number>;
|
|
97
|
+
}) => Order<T>);
|
|
98
|
+
/**
|
|
99
|
+
* Whether to include a relevance score with each result. Only applicable for vector searches.
|
|
100
|
+
* - If `true`, the default score is included.
|
|
101
|
+
* - If a function is provided, it customizes the score calculation using the original score.
|
|
102
|
+
* - If no order is specified, results are ordered by score descending, when score is enabled.
|
|
103
|
+
* @default true
|
|
104
|
+
*/
|
|
105
|
+
score?: boolean | ((originalScore: SQL<number>) => SQL<number>);
|
|
106
|
+
/**
|
|
107
|
+
* Enable and configure ranking of search results.
|
|
108
|
+
* - If `true` (default), results are ordered by score descending using default ranking options.
|
|
109
|
+
* - If an `RankOptions` object is provided, ranking is customized.
|
|
110
|
+
* @default true
|
|
111
|
+
*/
|
|
112
|
+
rank?: boolean | RankOptions;
|
|
113
|
+
/**
|
|
114
|
+
* Enable and configure highlighting of search results.
|
|
115
|
+
*/
|
|
116
|
+
highlight?: TargetColumnPaths<T> | SQL<string> | HighlightOptions<T>;
|
|
117
|
+
}>;
|
|
118
|
+
/**
|
|
119
|
+
* Represents a single result from a full-text search operation.
|
|
120
|
+
* @template T - The entity type.
|
|
121
|
+
*/
|
|
122
|
+
export type SearchResult<T extends EntityWithoutMetadata> = {
|
|
123
|
+
entity: T;
|
|
124
|
+
score?: number;
|
|
125
|
+
highlight?: string;
|
|
126
|
+
};
|
|
56
127
|
/**
|
|
57
128
|
* Options for update operations (currently inherits from LoadOptions, primarily for ordering).
|
|
58
129
|
* @template T - The entity type.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { sql, SQL } from 'drizzle-orm';
|
|
1
2
|
import { toCamelCase, toSnakeCase } from 'drizzle-orm/casing';
|
|
2
3
|
import { boolean, check, doublePrecision, foreignKey, index, integer, jsonb, pgSchema, primaryKey, text, unique, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
|
3
4
|
import { MultiKeyMap } from '../../../data-structures/multi-key-map.js';
|
|
4
5
|
import { tryGetEnumName } from '../../../enumeration/enumeration.js';
|
|
5
6
|
import { NotSupportedError } from '../../../errors/not-supported.error.js';
|
|
6
7
|
import { JsonPath } from '../../../json-path/json-path.js';
|
|
8
|
+
import { setweight, toTsVector } from '../../../orm/sqls.js';
|
|
7
9
|
import { reflectionRegistry } from '../../../reflection/registry.js';
|
|
8
10
|
import { ArraySchema, BooleanSchema, DefaultSchema, EnumerationSchema, getObjectSchema, NullableSchema, NumberSchema, ObjectSchema, OptionalSchema, StringSchema, Uint8ArraySchema } from '../../../schema/index.js';
|
|
9
11
|
import { compareByValueSelectionToOrder, orderRest } from '../../../utils/comparison.js';
|
|
@@ -53,9 +55,12 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
53
55
|
function buildIndex(table, data, columnName) {
|
|
54
56
|
const columns = (data.columns ?? [columnName]).map((columnValue) => {
|
|
55
57
|
assertDefined(columnValue, 'Missing column name for index.');
|
|
58
|
+
if (columnValue instanceof SQL) {
|
|
59
|
+
return columnValue;
|
|
60
|
+
}
|
|
56
61
|
const [columnName, columnOrder] = isString(columnValue) ? [columnValue] : columnValue;
|
|
57
|
-
const order = columnOrder ?? data.order ?? 'asc';
|
|
58
62
|
let column = getColumn(table, columnName);
|
|
63
|
+
const order = columnOrder ?? data.order ?? 'asc';
|
|
59
64
|
column = column[order]();
|
|
60
65
|
if (data.options?.nulls == 'first') {
|
|
61
66
|
column = column.nullsFirst();
|
|
@@ -66,13 +71,37 @@ export function _getDrizzleTableFromType(type, fallbackSchemaName) {
|
|
|
66
71
|
return column;
|
|
67
72
|
});
|
|
68
73
|
const indexFn = (data.options?.unique == true) ? uniqueIndex : index;
|
|
69
|
-
|
|
74
|
+
const containsSql = columns.some((column) => column instanceof SQL);
|
|
75
|
+
const indexName = data.options?.name ?? (containsSql
|
|
76
|
+
? assertDefinedPass(data.options?.name, 'Index with SQL expressions must have a name.')
|
|
77
|
+
: getIndexName(tableName, columns, { naming: data.options?.naming }));
|
|
78
|
+
const indexColumns = (data.options?.using == 'gin'
|
|
79
|
+
? buildGinIndexColumns(columns, data.options)
|
|
80
|
+
: columns);
|
|
81
|
+
let builder = indexFn(indexName).using(data.options?.using ?? 'btree', ...indexColumns);
|
|
70
82
|
if (isDefined(data.options?.where)) {
|
|
71
83
|
const query = convertQuery(data.options.where(table), table, columnDefinitionsMap);
|
|
72
84
|
builder = builder.where(query.inlineParams());
|
|
73
85
|
}
|
|
74
86
|
return builder;
|
|
75
87
|
}
|
|
88
|
+
function buildGinIndexColumns(columns, options) {
|
|
89
|
+
const vectors = columns.map((column) => {
|
|
90
|
+
if (column instanceof SQL) {
|
|
91
|
+
return column;
|
|
92
|
+
}
|
|
93
|
+
const tsVector = toTsVector(options.language ?? 'simple', column);
|
|
94
|
+
const weight = options.weights?.[column.name];
|
|
95
|
+
if (isDefined(weight)) {
|
|
96
|
+
return setweight(tsVector, weight);
|
|
97
|
+
}
|
|
98
|
+
return tsVector;
|
|
99
|
+
});
|
|
100
|
+
if (options.vectors == 'separate') {
|
|
101
|
+
return vectors;
|
|
102
|
+
}
|
|
103
|
+
return [sql `(${sql.join(vectors, sql ` || `)})`];
|
|
104
|
+
}
|
|
76
105
|
function buildPrimaryKey(table) {
|
|
77
106
|
const columns = primaryKeyColumnDefinitions.map((columnDefinition) => getColumn(table, columnDefinition.name));
|
|
78
107
|
return primaryKey({
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module
|
|
3
|
-
* Converts a generic query object structure into a Drizzle ORM SQL condition.
|
|
4
|
-
* Supports logical operators ($and, $or, $nor) and various comparison operators
|
|
5
|
-
* ($eq, $neq, $in, $nin, $lt, $lte, $gt, $gte, $regex).
|
|
6
|
-
*/
|
|
7
1
|
import { SQL } from 'drizzle-orm';
|
|
8
|
-
import type {
|
|
2
|
+
import type { Record } from '../../types/index.js';
|
|
3
|
+
import type { FtsParser, Query } from '../query.js';
|
|
9
4
|
import type { ColumnDefinition, PgTableFromType } from './types.js';
|
|
10
5
|
/**
|
|
11
6
|
* Converts a query object into a Drizzle SQL condition.
|
|
@@ -20,3 +15,6 @@ import type { ColumnDefinition, PgTableFromType } from './types.js';
|
|
|
20
15
|
* @throws {Error} If an unsupported query type is encountered.
|
|
21
16
|
*/
|
|
22
17
|
export declare function convertQuery(query: Query, table: PgTableFromType, columnDefinitionsMap: Map<string, ColumnDefinition>): SQL;
|
|
18
|
+
export declare function getTsQuery(text: string | SQL, language: string | SQL, parser: FtsParser): SQL;
|
|
19
|
+
export declare function getTsVector(fields: readonly string[], language: string | SQL, table: PgTableFromType, columnDefinitionsMap: Map<string, ColumnDefinition>, weights?: Partial<Record<string, 'A' | 'B' | 'C' | 'D'>>): SQL;
|
|
20
|
+
export declare function getColumnConcatenation(fields: readonly string[], table: PgTableFromType, columnDefinitionsMap: Map<string, ColumnDefinition>): SQL;
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module
|
|
3
|
-
* Converts a generic query object structure into a Drizzle ORM SQL condition.
|
|
4
|
-
* Supports logical operators ($and, $or, $nor) and various comparison operators
|
|
5
|
-
* ($eq, $neq, $in, $nin, $lt, $lte, $gt, $gte, $regex).
|
|
6
|
-
*/
|
|
7
1
|
import { and, eq, gt, gte, inArray, isNotNull, isNull, isSQLWrapper, lt, lte, ne, not, notInArray, or, SQL, sql } from 'drizzle-orm';
|
|
2
|
+
import { match } from 'ts-pattern';
|
|
8
3
|
import { NotSupportedError } from '../../errors/not-supported.error.js';
|
|
9
4
|
import { hasOwnProperty, objectEntries } from '../../utils/object/object.js';
|
|
10
5
|
import { assertDefinedPass, isDefined, isPrimitive, isRegExp, isString, isUndefined } from '../../utils/type-guards.js';
|
|
6
|
+
import { isSimilar, phraseToTsQuery, plainToTsQuery, setweight, toTsQuery, toTsVector, websearchToTsQuery } from '../sqls.js';
|
|
11
7
|
const sqlTrue = sql `true`;
|
|
12
8
|
/**
|
|
13
9
|
* Converts a query object into a Drizzle SQL condition.
|
|
@@ -60,10 +56,27 @@ export function convertQuery(query, table, columnDefinitionsMap) {
|
|
|
60
56
|
}
|
|
61
57
|
break;
|
|
62
58
|
}
|
|
59
|
+
case '$fts': {
|
|
60
|
+
const ftsQuery = value;
|
|
61
|
+
const method = ftsQuery.method ?? 'vector';
|
|
62
|
+
const sqlValue = match(method)
|
|
63
|
+
.with('vector', () => {
|
|
64
|
+
const tsquery = getTsQuery(ftsQuery.text, ftsQuery.vector?.language ?? 'simple', ftsQuery.vector?.parser ?? 'raw');
|
|
65
|
+
const tsvector = getTsVector(ftsQuery.fields, ftsQuery.vector?.language ?? 'simple', table, columnDefinitionsMap, ftsQuery.vector?.weights);
|
|
66
|
+
return sql `${tsvector} @@ ${tsquery}`;
|
|
67
|
+
})
|
|
68
|
+
.with('trigram', () => {
|
|
69
|
+
const searchExpression = getColumnConcatenation(ftsQuery.fields, table, columnDefinitionsMap);
|
|
70
|
+
return isSimilar(searchExpression, ftsQuery.text);
|
|
71
|
+
})
|
|
72
|
+
.exhaustive();
|
|
73
|
+
conditions.push(sqlValue);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
63
76
|
default: {
|
|
64
77
|
const columnDef = assertDefinedPass(columnDefinitionsMap.get(property), `Could not map property ${property} to column.`);
|
|
65
78
|
const column = table[columnDef.name];
|
|
66
|
-
const condition = getCondition(property, value, column);
|
|
79
|
+
const condition = getCondition(property, value, column, table, columnDefinitionsMap);
|
|
67
80
|
conditions.push(condition);
|
|
68
81
|
break;
|
|
69
82
|
}
|
|
@@ -79,10 +92,8 @@ export function convertQuery(query, table, columnDefinitionsMap) {
|
|
|
79
92
|
* @param value The value or comparison object for the property.
|
|
80
93
|
* @param column The Drizzle column object.
|
|
81
94
|
* @returns A Drizzle SQL condition.
|
|
82
|
-
* @throws {NotSupportedError} If an unsupported operator like $exists, $text, $geoShape, or $geoDistance is used.
|
|
83
|
-
* @throws {Error} If the value structure is not a recognized comparison operator.
|
|
84
95
|
*/
|
|
85
|
-
function getCondition(property, value, column) {
|
|
96
|
+
function getCondition(property, value, column, table, columnDefinitionsMap) {
|
|
86
97
|
const isPrimitiveValue = isPrimitive(value);
|
|
87
98
|
if (isPrimitiveValue || hasOwnProperty(value, '$eq')) {
|
|
88
99
|
const queryValue = isPrimitiveValue ? value : value.$eq;
|
|
@@ -92,7 +103,7 @@ function getCondition(property, value, column) {
|
|
|
92
103
|
return eq(column, queryValue);
|
|
93
104
|
}
|
|
94
105
|
if (hasOwnProperty(value, '$and')) {
|
|
95
|
-
const innerQueries = value.$and.map((query) => getCondition(property, query, column)); // eslint-disable-line @typescript-eslint/no-unsafe-argument
|
|
106
|
+
const innerQueries = value.$and.map((query) => getCondition(property, query, column, table, columnDefinitionsMap)); // eslint-disable-line @typescript-eslint/no-unsafe-argument
|
|
96
107
|
const andQuery = and(...innerQueries);
|
|
97
108
|
if (isUndefined(andQuery)) {
|
|
98
109
|
throw new Error(`No valid conditions in $and for property "${property}".`);
|
|
@@ -100,7 +111,7 @@ function getCondition(property, value, column) {
|
|
|
100
111
|
return andQuery;
|
|
101
112
|
}
|
|
102
113
|
if (hasOwnProperty(value, '$or')) {
|
|
103
|
-
const innerQueries = value.$or.map((query) => getCondition(property, query, column)); // eslint-disable-line @typescript-eslint/no-unsafe-argument
|
|
114
|
+
const innerQueries = value.$or.map((query) => getCondition(property, query, column, table, columnDefinitionsMap)); // eslint-disable-line @typescript-eslint/no-unsafe-argument
|
|
104
115
|
const orQuery = or(...innerQueries);
|
|
105
116
|
if (isUndefined(orQuery)) {
|
|
106
117
|
throw new Error(`No valid conditions in $or for property "${property}".`);
|
|
@@ -108,7 +119,7 @@ function getCondition(property, value, column) {
|
|
|
108
119
|
return orQuery;
|
|
109
120
|
}
|
|
110
121
|
if (hasOwnProperty(value, '$not')) {
|
|
111
|
-
const innerQuery = getCondition(property, value.$not, column); // eslint-disable-line @typescript-eslint/no-unsafe-argument
|
|
122
|
+
const innerQuery = getCondition(property, value.$not, column, table, columnDefinitionsMap); // eslint-disable-line @typescript-eslint/no-unsafe-argument
|
|
112
123
|
return not(innerQuery);
|
|
113
124
|
}
|
|
114
125
|
if (hasOwnProperty(value, '$neq')) {
|
|
@@ -156,8 +167,17 @@ function getCondition(property, value, column) {
|
|
|
156
167
|
const operator = (regexp.flags?.includes('i') ?? false) ? sql.raw('~*') : sql.raw('~');
|
|
157
168
|
return sql `${column} ${operator} ${regexp.value}`;
|
|
158
169
|
}
|
|
159
|
-
if (hasOwnProperty(value, '$
|
|
160
|
-
|
|
170
|
+
if (hasOwnProperty(value, '$fts')) {
|
|
171
|
+
const queryValue = value.$fts;
|
|
172
|
+
const { query, method = 'vector', language = 'simple', parser = 'plain' } = isString(queryValue)
|
|
173
|
+
? { query: queryValue }
|
|
174
|
+
: queryValue;
|
|
175
|
+
if (method == 'similarity') {
|
|
176
|
+
return isSimilar(column, query);
|
|
177
|
+
}
|
|
178
|
+
const tsquery = getTsQuery(query, language, parser);
|
|
179
|
+
const tsvector = toTsVector(language, column);
|
|
180
|
+
return sql `${tsvector} @@ ${tsquery}`;
|
|
161
181
|
}
|
|
162
182
|
if (hasOwnProperty(value, '$geoShape')) {
|
|
163
183
|
throw new NotSupportedError('$geoShape is not supported.');
|
|
@@ -167,3 +187,37 @@ function getCondition(property, value, column) {
|
|
|
167
187
|
}
|
|
168
188
|
throw new Error(`Unsupported query type "${property}".`);
|
|
169
189
|
}
|
|
190
|
+
export function getTsQuery(text, language, parser) {
|
|
191
|
+
switch (parser) {
|
|
192
|
+
case 'raw':
|
|
193
|
+
return toTsQuery(language, text);
|
|
194
|
+
case 'plain':
|
|
195
|
+
return plainToTsQuery(language, text);
|
|
196
|
+
case 'phrase':
|
|
197
|
+
return phraseToTsQuery(language, text);
|
|
198
|
+
case 'websearch':
|
|
199
|
+
return websearchToTsQuery(language, text);
|
|
200
|
+
default:
|
|
201
|
+
throw new NotSupportedError(`Unsupported text search parser: ${parser}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export function getTsVector(fields, language, table, columnDefinitionsMap, weights) {
|
|
205
|
+
const tsvector = sql.join(fields.map((field) => {
|
|
206
|
+
const columnDef = assertDefinedPass(columnDefinitionsMap.get(field), `Could not map property ${field} to column.`);
|
|
207
|
+
const column = table[columnDef.name];
|
|
208
|
+
const vector = toTsVector(language, column);
|
|
209
|
+
const weight = weights?.[field];
|
|
210
|
+
if (isDefined(weight)) {
|
|
211
|
+
return setweight(vector, weight);
|
|
212
|
+
}
|
|
213
|
+
return vector;
|
|
214
|
+
}), sql ` || `);
|
|
215
|
+
return tsvector;
|
|
216
|
+
}
|
|
217
|
+
export function getColumnConcatenation(fields, table, columnDefinitionsMap) {
|
|
218
|
+
const columns = fields.map((field) => {
|
|
219
|
+
const columnDef = assertDefinedPass(columnDefinitionsMap.get(field), `Could not map property ${field} to column.`);
|
|
220
|
+
return table[columnDef.name];
|
|
221
|
+
});
|
|
222
|
+
return sql `(${sql.join(columns, sql ` || ' ' || `)})`;
|
|
223
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { SQL } from 'drizzle-orm';
|
|
2
|
-
import type { PgColumn, PgInsertValue, PgUpdateSetSource } from 'drizzle-orm/pg-core';
|
|
3
|
-
import { afterResolve, type Resolvable
|
|
4
|
-
import type { DeepPartial, OneOrMany, Paths, Type, UntaggedDeep } from '../../types/index.js';
|
|
2
|
+
import type { PgColumn, PgInsertValue, PgSelectBuilder, PgUpdateSetSource, SelectedFields } from 'drizzle-orm/pg-core';
|
|
3
|
+
import { afterResolve, resolveArgumentType, type Resolvable } from '../../injector/interfaces.js';
|
|
4
|
+
import type { DeepPartial, Function, OneOrMany, Paths, Record, Type, UntaggedDeep } from '../../types/index.js';
|
|
5
5
|
import { Entity, type EntityMetadataAttributes, type EntityType, type EntityWithoutMetadata } from '../entity.js';
|
|
6
|
-
import type { Query } from '../query.js';
|
|
7
|
-
import type { EntityMetadataUpdate, EntityUpdate, LoadManyOptions, LoadOptions, NewEntity, Order, TargetColumnPaths } from '../repository.types.js';
|
|
6
|
+
import type { FullTextSearchQuery, Query } from '../query.js';
|
|
7
|
+
import type { EntityMetadataUpdate, EntityUpdate, LoadManyOptions, LoadOptions, NewEntity, Order, SearchOptions, SearchResult, TargetColumn, TargetColumnPaths } from '../repository.types.js';
|
|
8
8
|
import type { Database } from './database.js';
|
|
9
9
|
import type { PgTransaction } from './transaction.js';
|
|
10
10
|
import { Transactional } from './transactional.js';
|
|
@@ -40,6 +40,16 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
|
|
|
40
40
|
[afterResolve](): void;
|
|
41
41
|
private expirationLoop;
|
|
42
42
|
protected getTransactionalContextData(): EntityRepositoryContext;
|
|
43
|
+
vectorSearch(options: SearchOptions<T>): Promise<SearchResult<T>[]>;
|
|
44
|
+
trigramSearch(options: SearchOptions<T>): Promise<SearchResult<T>[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Performs a full-text search and returns entities ranked by relevance.
|
|
47
|
+
* This method is a convenience wrapper around `loadManyByQuery` with the `$fts` operator.
|
|
48
|
+
* @param query The search query using the `$fts` operator.
|
|
49
|
+
* @param options Search options including ranking, and highlighting configuration.
|
|
50
|
+
* @returns A promise that resolves to an array of search results, including the entity, score, and optional highlight.
|
|
51
|
+
*/
|
|
52
|
+
search(_query: FullTextSearchQuery<T>['$fts'], _options?: SearchOptions<T>): Promise<SearchResult<T>[]>;
|
|
43
53
|
/**
|
|
44
54
|
* Loads a single entity by its ID.
|
|
45
55
|
* Throws `NotFoundError` if the entity is not found.
|
|
@@ -416,7 +426,7 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
|
|
|
416
426
|
identity: undefined;
|
|
417
427
|
generated: undefined;
|
|
418
428
|
}, {}, {}>;
|
|
419
|
-
}, "partial", globalThis.Record<string, "not-null">, false, "
|
|
429
|
+
}, "partial", globalThis.Record<string, "not-null">, false, "limit" | "where", {
|
|
420
430
|
id: string;
|
|
421
431
|
}[], {
|
|
422
432
|
id: PgColumn<{
|
|
@@ -436,7 +446,9 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
|
|
|
436
446
|
identity: undefined;
|
|
437
447
|
generated: undefined;
|
|
438
448
|
}, {}, {}>;
|
|
439
|
-
}>, "
|
|
449
|
+
}>, "limit" | "where">;
|
|
450
|
+
applySelect<TApplyTo extends Record<'select' | 'selectDistinct' | 'selectDistinctOn', Function<any[], PgSelectBuilder<any, any>>>>(applyTo: TApplyTo, distinct?: boolean | TargetColumn<T>[]): PgSelectBuilder<undefined, ReturnType<TApplyTo['selectDistinct']> extends PgSelectBuilder<any, infer TResult> ? TResult : never>;
|
|
451
|
+
applySelect<TApplyTo extends Record<'select' | 'selectDistinct' | 'selectDistinctOn', Function<any[], PgSelectBuilder<any, any>>>, TSelection extends SelectedFields>(applyTo: TApplyTo, selection: TSelection, distinct?: boolean | TargetColumn<T>[]): PgSelectBuilder<TSelection, ReturnType<TApplyTo['selectDistinct']> extends PgSelectBuilder<any, infer TResult> ? TResult : never>;
|
|
440
452
|
protected getAttributesUpdate(attributes: SQL | EntityMetadataAttributes | undefined): SQL<unknown> | undefined;
|
|
441
453
|
protected _mapManyToEntity(columns: InferSelect[], transformContext: TransformContext): Promise<T[]>;
|
|
442
454
|
protected _mapToEntity(columns: InferSelect, transformContext: TransformContext): Promise<T>;
|
package/orm/server/repository.js
CHANGED
|
@@ -5,9 +5,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { and, asc, count, desc, eq, inArray, isNull, isSQLWrapper, lte, or, SQL, sql } from 'drizzle-orm';
|
|
8
|
-
import { match } from 'ts-pattern';
|
|
8
|
+
import { match, P } from 'ts-pattern';
|
|
9
9
|
import { CancellationSignal } from '../../cancellation/token.js';
|
|
10
10
|
import { NotFoundError } from '../../errors/not-found.error.js';
|
|
11
|
+
import { NotImplementedError } from '../../errors/not-implemented.error.js';
|
|
11
12
|
import { Singleton } from '../../injector/decorators.js';
|
|
12
13
|
import { inject, injectArgument } from '../../injector/inject.js';
|
|
13
14
|
import { afterResolve, resolveArgumentType } from '../../injector/interfaces.js';
|
|
@@ -19,15 +20,18 @@ import { importSymmetricKey } from '../../utils/cryptography.js';
|
|
|
19
20
|
import { fromDeepObjectEntries, fromEntries, objectEntries } from '../../utils/object/object.js';
|
|
20
21
|
import { cancelableTimeout } from '../../utils/timing.js';
|
|
21
22
|
import { tryIgnoreAsync } from '../../utils/try-ignore.js';
|
|
22
|
-
import { assertDefined, assertDefinedPass, isArray, isDefined, isString, isUndefined } from '../../utils/type-guards.js';
|
|
23
|
+
import { assertDefined, assertDefinedPass, isArray, isBoolean, isDefined, isFunction, isInstanceOf, isString, isUndefined } from '../../utils/type-guards.js';
|
|
23
24
|
import { typeExtends } from '../../utils/type/index.js';
|
|
24
25
|
import { millisecondsPerSecond } from '../../utils/units.js';
|
|
25
26
|
import { Entity } from '../entity.js';
|
|
26
|
-
import { TRANSACTION_TIMESTAMP } from '../sqls.js';
|
|
27
|
+
import { TRANSACTION_TIMESTAMP, tsHeadline, tsRankCd } from '../sqls.js';
|
|
27
28
|
import { getColumnDefinitions, getColumnDefinitionsMap, getDrizzleTableFromType } from './drizzle/schema-converter.js';
|
|
28
|
-
import { convertQuery } from './query-converter.js';
|
|
29
|
+
import { convertQuery, getColumnConcatenation, getTsQuery, getTsVector } from './query-converter.js';
|
|
29
30
|
import { ENCRYPTION_SECRET } from './tokens.js';
|
|
30
31
|
import { getTransactionalContextData, injectTransactional, injectTransactionalAsync, isInTransactionalContext, Transactional } from './transactional.js';
|
|
32
|
+
const searchScoreColumn = '__tsl_score';
|
|
33
|
+
const searchDistanceColumn = '__tsl_distance';
|
|
34
|
+
const searchHighlightColumn = '__tsl_highlight';
|
|
31
35
|
export const repositoryType = Symbol('repositoryType');
|
|
32
36
|
/**
|
|
33
37
|
* Configuration class for EntityRepository.
|
|
@@ -92,6 +96,128 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
92
96
|
};
|
|
93
97
|
return context;
|
|
94
98
|
}
|
|
99
|
+
async vectorSearch(options) {
|
|
100
|
+
const { vector: { language = 'simple' } = {} } = options;
|
|
101
|
+
const languageSql = isString(language) ? language : sql `${language}`;
|
|
102
|
+
const tsquery = getTsQuery(options.text, languageSql, options.vector?.parser ?? 'raw');
|
|
103
|
+
const tsvector = getTsVector(options.fields, languageSql, this.table, this.#columnDefinitionsMap, options.vector?.weights);
|
|
104
|
+
const rawScore = (options.score != false) ? tsRankCd(tsvector, tsquery, isBoolean(options.rank) ? undefined : options.rank) : undefined;
|
|
105
|
+
const score = (isFunction(options.score) ? options.score(rawScore) : rawScore)?.as(searchScoreColumn);
|
|
106
|
+
const vectorClause = sql `${tsvector} @@ ${tsquery}`;
|
|
107
|
+
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.getColumn(column)]));
|
|
108
|
+
if (isDefined(score)) {
|
|
109
|
+
selection[searchScoreColumn] = score;
|
|
110
|
+
}
|
|
111
|
+
if (isDefined(options.highlight)) {
|
|
112
|
+
const { source, ...headlineOptions } = (isString(options.highlight) || isInstanceOf(options.highlight, SQL))
|
|
113
|
+
? { source: options.highlight }
|
|
114
|
+
: options.highlight;
|
|
115
|
+
const document = match(source)
|
|
116
|
+
.with(P.instanceOf(SQL), (s) => s)
|
|
117
|
+
.otherwise((paths) => {
|
|
118
|
+
const columns = this.getColumns(paths);
|
|
119
|
+
return sql.join(columns, sql ` || ' ' || `);
|
|
120
|
+
});
|
|
121
|
+
selection[searchHighlightColumn] = tsHeadline(languageSql, document, tsquery, headlineOptions).as(searchHighlightColumn);
|
|
122
|
+
}
|
|
123
|
+
const whereClause = isDefined(options.filter)
|
|
124
|
+
? and(this.convertQuery(options.filter), vectorClause)
|
|
125
|
+
: vectorClause;
|
|
126
|
+
let dbQuery = this
|
|
127
|
+
.applySelect(this.session, selection, options.distinct)
|
|
128
|
+
.from(this.#table)
|
|
129
|
+
.where(whereClause)
|
|
130
|
+
.$dynamic();
|
|
131
|
+
if (isDefined(options.offset)) {
|
|
132
|
+
dbQuery = dbQuery.offset(options.offset);
|
|
133
|
+
}
|
|
134
|
+
if (isDefined(options.limit)) {
|
|
135
|
+
dbQuery = dbQuery.limit(options.limit);
|
|
136
|
+
}
|
|
137
|
+
const orderByExpressions = [];
|
|
138
|
+
if (isDefined(options.order)) {
|
|
139
|
+
const order = isFunction(options.order)
|
|
140
|
+
? options.order({
|
|
141
|
+
get score() {
|
|
142
|
+
return assertDefinedPass(score, 'Score is disabled.');
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
: options.order;
|
|
146
|
+
orderByExpressions.push(...this.convertOrderBy(order));
|
|
147
|
+
}
|
|
148
|
+
else if (isDefined(score)) {
|
|
149
|
+
orderByExpressions.push(desc(score));
|
|
150
|
+
}
|
|
151
|
+
dbQuery = dbQuery.orderBy(...orderByExpressions);
|
|
152
|
+
const transformContext = await this.getTransformContext();
|
|
153
|
+
const rows = await dbQuery;
|
|
154
|
+
return await toArrayAsync(mapAsync(rows, async ({ [searchScoreColumn]: score, [searchHighlightColumn]: highlight, ...row }) => ({
|
|
155
|
+
entity: await this._mapToEntity(row, transformContext),
|
|
156
|
+
score: score,
|
|
157
|
+
highlight: highlight,
|
|
158
|
+
})));
|
|
159
|
+
}
|
|
160
|
+
async trigramSearch(options) {
|
|
161
|
+
const distanceOperator = match(options.trigram?.type ?? 'phrase')
|
|
162
|
+
.with('phrase', () => '<->')
|
|
163
|
+
.with('word', () => '<<->')
|
|
164
|
+
.with('strict-word', () => '<<<->')
|
|
165
|
+
.exhaustive();
|
|
166
|
+
const distanceThresholdOperator = match(options.trigram?.type ?? 'phrase')
|
|
167
|
+
.with('phrase', () => '%')
|
|
168
|
+
.with('word', () => '<%')
|
|
169
|
+
.with('strict-word', () => '<<%')
|
|
170
|
+
.exhaustive();
|
|
171
|
+
// TODO: set similarity_threshold, word_similarity_threshold, strict_word_similarity_threshold
|
|
172
|
+
const searchExpression = getColumnConcatenation(options.fields, this.table, this.#columnDefinitionsMap);
|
|
173
|
+
const distance = sql `(${options.text} ${sql.raw(distanceOperator)} ${searchExpression})`.as(searchDistanceColumn);
|
|
174
|
+
const trigramClause = sql `(${options.text} ${sql.raw(distanceThresholdOperator)} ${searchExpression})`;
|
|
175
|
+
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.getColumn(column)]));
|
|
176
|
+
selection[searchDistanceColumn] = distance;
|
|
177
|
+
const whereClause = isDefined(options.filter)
|
|
178
|
+
? and(this.convertQuery(options.filter), trigramClause)
|
|
179
|
+
: trigramClause;
|
|
180
|
+
let dbQuery = this
|
|
181
|
+
.applySelect(this.session, selection, options.distinct)
|
|
182
|
+
.from(this.#table)
|
|
183
|
+
.where(whereClause)
|
|
184
|
+
.$dynamic();
|
|
185
|
+
if (isDefined(options.offset)) {
|
|
186
|
+
dbQuery = dbQuery.offset(options.offset);
|
|
187
|
+
}
|
|
188
|
+
if (isDefined(options.limit)) {
|
|
189
|
+
dbQuery = dbQuery.limit(options.limit);
|
|
190
|
+
}
|
|
191
|
+
const orderByExpressions = [];
|
|
192
|
+
if (isDefined(options.order)) {
|
|
193
|
+
const order = isFunction(options.order)
|
|
194
|
+
? options.order({ score: sql `1 - ${distance}` })
|
|
195
|
+
: options.order;
|
|
196
|
+
orderByExpressions.push(...this.convertOrderBy(order));
|
|
197
|
+
}
|
|
198
|
+
else if (options.rank != false) {
|
|
199
|
+
orderByExpressions.push(distance);
|
|
200
|
+
}
|
|
201
|
+
dbQuery = dbQuery.orderBy(...orderByExpressions);
|
|
202
|
+
console.log(dbQuery.toSQL());
|
|
203
|
+
const transformContext = await this.getTransformContext();
|
|
204
|
+
const rows = await dbQuery;
|
|
205
|
+
return await toArrayAsync(mapAsync(rows, async ({ [searchDistanceColumn]: distance, [searchHighlightColumn]: highlight, ...row }) => ({
|
|
206
|
+
entity: await this._mapToEntity(row, transformContext),
|
|
207
|
+
score: (1 - distance),
|
|
208
|
+
highlight: highlight,
|
|
209
|
+
})));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Performs a full-text search and returns entities ranked by relevance.
|
|
213
|
+
* This method is a convenience wrapper around `loadManyByQuery` with the `$fts` operator.
|
|
214
|
+
* @param query The search query using the `$fts` operator.
|
|
215
|
+
* @param options Search options including ranking, and highlighting configuration.
|
|
216
|
+
* @returns A promise that resolves to an array of search results, including the entity, score, and optional highlight.
|
|
217
|
+
*/
|
|
218
|
+
async search(_query, _options) {
|
|
219
|
+
throw new NotImplementedError('EntityRepository.search is not implemented yet.');
|
|
220
|
+
}
|
|
95
221
|
/**
|
|
96
222
|
* Loads a single entity by its ID.
|
|
97
223
|
* Throws `NotFoundError` if the entity is not found.
|
|
@@ -186,13 +312,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
186
312
|
*/
|
|
187
313
|
async loadManyByQuery(query, options) {
|
|
188
314
|
const sqlQuery = this.convertQuery(query);
|
|
189
|
-
let dbQuery =
|
|
190
|
-
.with(false, () => this.session.select())
|
|
191
|
-
.with(true, () => this.session.selectDistinct())
|
|
192
|
-
.otherwise((targets) => {
|
|
193
|
-
const ons = targets.map((target) => isString(target) ? this.getColumn(target) : target);
|
|
194
|
-
return this.session.selectDistinctOn(ons);
|
|
195
|
-
})
|
|
315
|
+
let dbQuery = this.applySelect(this.session, options?.distinct)
|
|
196
316
|
.from(this.#table)
|
|
197
317
|
.where(sqlQuery)
|
|
198
318
|
.$dynamic();
|
|
@@ -855,6 +975,19 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
855
975
|
.where(sqlQuery)
|
|
856
976
|
.limit(1);
|
|
857
977
|
}
|
|
978
|
+
applySelect(applyTo, selectionOrDistinct, distinctOrNothing) {
|
|
979
|
+
const firstParameterIsDistinct = isBoolean(selectionOrDistinct) || isArray(selectionOrDistinct);
|
|
980
|
+
const selection = firstParameterIsDistinct ? undefined : selectionOrDistinct;
|
|
981
|
+
const distinct = firstParameterIsDistinct ? selectionOrDistinct : distinctOrNothing;
|
|
982
|
+
const selectBuilder = match(distinct ?? false)
|
|
983
|
+
.with(false, () => applyTo.select(selection))
|
|
984
|
+
.with(true, () => applyTo.selectDistinct(selection))
|
|
985
|
+
.otherwise((targets) => {
|
|
986
|
+
const ons = targets.map((target) => isString(target) ? this.getColumn(target) : target);
|
|
987
|
+
return applyTo.selectDistinctOn(ons, selection);
|
|
988
|
+
});
|
|
989
|
+
return selectBuilder;
|
|
990
|
+
}
|
|
858
991
|
getAttributesUpdate(attributes) {
|
|
859
992
|
if (isUndefined(attributes)) {
|
|
860
993
|
return undefined;
|