@tstdl/base 0.93.21 → 0.93.23
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/application/application.js +3 -3
- package/authentication/server/module.d.ts +1 -1
- package/authentication/server/module.js +1 -6
- package/document-management/api/document-management.api.d.ts +0 -4
- package/document-management/service-models/document.service-model.d.ts +0 -2
- package/injector/injector.js +22 -22
- package/injector/resolve-chain.d.ts +7 -5
- package/injector/resolve-chain.js +16 -14
- package/logger/manager.js +3 -3
- package/orm/data-types/bytea.d.ts +4 -14
- package/orm/data-types/bytea.js +2 -2
- package/orm/data-types/common.d.ts +18 -0
- package/orm/data-types/common.js +11 -0
- package/orm/data-types/index.d.ts +1 -0
- package/orm/data-types/index.js +1 -0
- package/orm/data-types/numeric-date.d.ts +4 -15
- package/orm/data-types/numeric-date.js +2 -2
- package/orm/data-types/timestamp.d.ts +4 -15
- package/orm/data-types/timestamp.js +2 -2
- package/orm/data-types/tsvector.d.ts +3 -13
- package/orm/data-types/tsvector.js +2 -2
- package/orm/decorators.d.ts +16 -54
- package/orm/decorators.js +24 -37
- package/orm/entity.d.ts +6 -9
- package/orm/entity.js +1 -2
- package/orm/query.d.ts +199 -61
- package/orm/query.js +2 -2
- package/orm/repository.types.d.ts +38 -9
- package/orm/server/drizzle/schema-converter.js +40 -118
- package/orm/server/query-converter.d.ts +21 -7
- package/orm/server/query-converter.js +194 -38
- package/orm/server/repository.d.ts +39 -22
- package/orm/server/repository.js +141 -71
- package/orm/server/types.d.ts +10 -2
- package/orm/sqls.d.ts +14 -16
- package/orm/sqls.js +34 -17
- package/package.json +2 -2
- package/test/drizzle/0000_nervous_iron_monger.sql +9 -0
- package/test/drizzle/meta/0000_snapshot.json +27 -7
- package/test/drizzle/meta/_journal.json +2 -44
- package/test/test.model.js +2 -6
- package/test1.js +18 -5
- package/test6.js +23 -35
- package/types/types.d.ts +8 -5
- package/utils/equals.js +2 -2
- package/utils/format-error.js +2 -2
- package/utils/helpers.js +3 -2
- package/utils/object/object.d.ts +4 -4
- package/test/drizzle/0000_sudden_sphinx.sql +0 -9
- package/test/drizzle/0001_organic_rhodey.sql +0 -2
- package/test/drizzle/0002_nice_squadron_supreme.sql +0 -1
- package/test/drizzle/0003_serious_mockingbird.sql +0 -1
- package/test/drizzle/0004_complete_pixie.sql +0 -1
- package/test/drizzle/0005_bumpy_sabra.sql +0 -1
- package/test/drizzle/0006_overrated_post.sql +0 -6
- package/test/drizzle/meta/0001_snapshot.json +0 -79
- package/test/drizzle/meta/0002_snapshot.json +0 -63
- package/test/drizzle/meta/0003_snapshot.json +0 -73
- package/test/drizzle/meta/0004_snapshot.json +0 -89
- package/test/drizzle/meta/0005_snapshot.json +0 -104
- package/test/drizzle/meta/0006_snapshot.json +0 -104
package/orm/server/repository.js
CHANGED
|
@@ -8,7 +8,6 @@ import { and, asc, count, desc, eq, inArray, isNull, isSQLWrapper, lte, or, SQL,
|
|
|
8
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';
|
|
12
11
|
import { Singleton } from '../../injector/decorators.js';
|
|
13
12
|
import { inject, injectArgument } from '../../injector/inject.js';
|
|
14
13
|
import { afterResolve, resolveArgumentType } from '../../injector/interfaces.js';
|
|
@@ -17,21 +16,23 @@ import { distinct, toArray } from '../../utils/array/array.js';
|
|
|
17
16
|
import { mapAsync } from '../../utils/async-iterable-helpers/map.js';
|
|
18
17
|
import { toArrayAsync } from '../../utils/async-iterable-helpers/to-array.js';
|
|
19
18
|
import { importSymmetricKey } from '../../utils/cryptography.js';
|
|
20
|
-
import { fromDeepObjectEntries, fromEntries, objectEntries } from '../../utils/object/object.js';
|
|
19
|
+
import { filterUndefinedObjectProperties, fromDeepObjectEntries, fromEntries, objectEntries } from '../../utils/object/object.js';
|
|
20
|
+
import { toSnakeCase } from '../../utils/string/snake-case.js';
|
|
21
21
|
import { cancelableTimeout } from '../../utils/timing.js';
|
|
22
22
|
import { tryIgnoreAsync } from '../../utils/try-ignore.js';
|
|
23
23
|
import { assertDefined, assertDefinedPass, isArray, isBoolean, isDefined, isFunction, isInstanceOf, isString, isUndefined } from '../../utils/type-guards.js';
|
|
24
24
|
import { typeExtends } from '../../utils/type/index.js';
|
|
25
25
|
import { millisecondsPerSecond } from '../../utils/units.js';
|
|
26
26
|
import { Entity } from '../entity.js';
|
|
27
|
-
import { TRANSACTION_TIMESTAMP, tsHeadline, tsRankCd } from '../sqls.js';
|
|
27
|
+
import { distance, isSimilar, isStrictWordSimilar, isWordSimilar, TRANSACTION_TIMESTAMP, tsHeadline, tsRankCd } from '../sqls.js';
|
|
28
28
|
import { getColumnDefinitions, getColumnDefinitionsMap, getDrizzleTableFromType } from './drizzle/schema-converter.js';
|
|
29
|
-
import { convertQuery,
|
|
29
|
+
import { convertQuery, getTsQuery, getTsVector, resolveTargetColumn } from './query-converter.js';
|
|
30
30
|
import { ENCRYPTION_SECRET } from './tokens.js';
|
|
31
31
|
import { getTransactionalContextData, injectTransactional, injectTransactionalAsync, isInTransactionalContext, Transactional } from './transactional.js';
|
|
32
32
|
const searchScoreColumn = '__tsl_score';
|
|
33
33
|
const searchDistanceColumn = '__tsl_distance';
|
|
34
34
|
const searchHighlightColumn = '__tsl_highlight';
|
|
35
|
+
const bm25ScoreColumn = '__parade_bm25_score';
|
|
35
36
|
export const repositoryType = Symbol('repositoryType');
|
|
36
37
|
/**
|
|
37
38
|
* Configuration class for EntityRepository.
|
|
@@ -72,8 +73,8 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
72
73
|
if ((softExpirationColumns.length + hardExpirationColumns.length) == 0) {
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
|
-
const softDeletionQuery = or(...softExpirationColumns.map((column) => lte(sql `${this.
|
|
76
|
-
const hardDeletionQuery = or(...hardExpirationColumns.map((column) => lte(sql `${this.
|
|
76
|
+
const softDeletionQuery = or(...softExpirationColumns.map((column) => lte(sql `${this.resolveTargetColumn(column)} + INTERVAL '${sql.raw(String(column.reflectionData.expirationField.after))} ms'`, TRANSACTION_TIMESTAMP)));
|
|
77
|
+
const hardDeletionQuery = or(...hardExpirationColumns.map((column) => lte(sql `${this.resolveTargetColumn(column)} + INTERVAL '${sql.raw(String(column.reflectionData.expirationField.after))} ms'`, TRANSACTION_TIMESTAMP)));
|
|
77
78
|
assertDefined(this.#cancellationSignal);
|
|
78
79
|
while (this.#cancellationSignal.isUnset) {
|
|
79
80
|
if (isDefined(softDeletionQuery)) {
|
|
@@ -96,22 +97,29 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
96
97
|
};
|
|
97
98
|
return context;
|
|
98
99
|
}
|
|
99
|
-
async
|
|
100
|
-
const {
|
|
100
|
+
async tsVectorSearch(options) {
|
|
101
|
+
const { query: { $tsvector: tsvectorOptions }, score: scoreOption = true, rank, highlight, filter } = options;
|
|
102
|
+
const { fields, query, language = 'simple', parser = 'raw' } = tsvectorOptions;
|
|
103
|
+
const fieldsArray = isArray(fields) ? fields : [fields];
|
|
104
|
+
const convertedFields = fieldsArray.map((target) => {
|
|
105
|
+
const [field, weight] = isArray(target) ? target : [target, undefined];
|
|
106
|
+
const sqlField = resolveTargetColumn(field, this.#table, this.#columnDefinitionsMap);
|
|
107
|
+
return isDefined(weight) ? [sqlField, weight] : sqlField;
|
|
108
|
+
});
|
|
101
109
|
const languageSql = isString(language) ? language : sql `${language}`;
|
|
102
|
-
const tsquery = getTsQuery(
|
|
103
|
-
const tsvector = getTsVector(
|
|
104
|
-
const rawScore = (
|
|
105
|
-
const score = (isFunction(
|
|
110
|
+
const tsquery = getTsQuery(query, languageSql, parser);
|
|
111
|
+
const tsvector = getTsVector(convertedFields, languageSql);
|
|
112
|
+
const rawScore = (scoreOption != false) ? tsRankCd(tsvector, tsquery, isBoolean(rank) ? undefined : rank) : undefined;
|
|
113
|
+
const score = (isFunction(scoreOption) ? scoreOption(rawScore) : rawScore)?.as(searchScoreColumn);
|
|
106
114
|
const vectorClause = sql `${tsvector} @@ ${tsquery}`;
|
|
107
|
-
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.
|
|
115
|
+
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.resolveTargetColumn(column)]));
|
|
108
116
|
if (isDefined(score)) {
|
|
109
117
|
selection[searchScoreColumn] = score;
|
|
110
118
|
}
|
|
111
|
-
if (isDefined(
|
|
112
|
-
const { source, ...headlineOptions } = (isString(
|
|
113
|
-
? { source:
|
|
114
|
-
:
|
|
119
|
+
if (isDefined(highlight)) {
|
|
120
|
+
const { source, ...headlineOptions } = (isString(highlight) || isInstanceOf(highlight, SQL))
|
|
121
|
+
? { source: highlight }
|
|
122
|
+
: highlight;
|
|
115
123
|
const document = match(source)
|
|
116
124
|
.with(P.instanceOf(SQL), (s) => s)
|
|
117
125
|
.otherwise((paths) => {
|
|
@@ -120,8 +128,8 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
120
128
|
});
|
|
121
129
|
selection[searchHighlightColumn] = tsHeadline(languageSql, document, tsquery, headlineOptions).as(searchHighlightColumn);
|
|
122
130
|
}
|
|
123
|
-
const whereClause = isDefined(
|
|
124
|
-
? and(this.convertQuery(
|
|
131
|
+
const whereClause = isDefined(filter)
|
|
132
|
+
? and(this.convertQuery(filter), vectorClause)
|
|
125
133
|
: vectorClause;
|
|
126
134
|
let dbQuery = this
|
|
127
135
|
.applySelect(this.session, selection, options.distinct)
|
|
@@ -149,33 +157,26 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
149
157
|
orderByExpressions.push(desc(score));
|
|
150
158
|
}
|
|
151
159
|
dbQuery = dbQuery.orderBy(...orderByExpressions);
|
|
152
|
-
|
|
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
|
-
})));
|
|
160
|
+
return await this._mapSearchResults(dbQuery, searchScoreColumn, searchHighlightColumn);
|
|
159
161
|
}
|
|
160
162
|
async trigramSearch(options) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.exhaustive();
|
|
166
|
-
const distanceThresholdOperator = match(options.trigram?.type ?? 'phrase')
|
|
167
|
-
.with('phrase', () => '%')
|
|
168
|
-
.with('word', () => '<%')
|
|
169
|
-
.with('strict-word', () => '<<%')
|
|
170
|
-
.exhaustive();
|
|
163
|
+
const { query: { $trigram: trigramOptions }, rank, filter } = options;
|
|
164
|
+
const { fields, query, type = 'phrase', threshold } = trigramOptions;
|
|
165
|
+
const convertedFields = toArray(fields).map((target) => resolveTargetColumn(target, this.#table, this.#columnDefinitionsMap));
|
|
166
|
+
const searchExpression = sql `(${sql.join(convertedFields, sql ` || ' ' || `)})`;
|
|
171
167
|
// TODO: set similarity_threshold, word_similarity_threshold, strict_word_similarity_threshold
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
const trigramClause = isDefined(threshold)
|
|
169
|
+
? match(type)
|
|
170
|
+
.with('phrase', () => isSimilar(query, searchExpression))
|
|
171
|
+
.with('word', () => isWordSimilar(query, searchExpression))
|
|
172
|
+
.with('strict-word', () => isStrictWordSimilar(query, searchExpression))
|
|
173
|
+
.exhaustive()
|
|
174
|
+
: isSimilar(query, searchExpression);
|
|
175
|
+
const distanceColumn = distance(query, searchExpression).as(searchDistanceColumn);
|
|
176
|
+
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.resolveTargetColumn(column)]));
|
|
177
|
+
selection[searchDistanceColumn] = distanceColumn;
|
|
178
|
+
const whereClause = isDefined(filter)
|
|
179
|
+
? and(this.convertQuery(filter), trigramClause)
|
|
179
180
|
: trigramClause;
|
|
180
181
|
let dbQuery = this
|
|
181
182
|
.applySelect(this.session, selection, options.distinct)
|
|
@@ -189,34 +190,80 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
189
190
|
dbQuery = dbQuery.limit(options.limit);
|
|
190
191
|
}
|
|
191
192
|
const orderByExpressions = [];
|
|
193
|
+
const score = sql `1 - ${distanceColumn}`;
|
|
194
|
+
if (isDefined(options.order)) {
|
|
195
|
+
const order = isFunction(options.order) ? options.order({ score }) : options.order;
|
|
196
|
+
orderByExpressions.push(...this.convertOrderBy(order));
|
|
197
|
+
}
|
|
198
|
+
else if (rank != false) {
|
|
199
|
+
orderByExpressions.push(distanceColumn); // order by distance ascending
|
|
200
|
+
}
|
|
201
|
+
dbQuery = dbQuery.orderBy(...orderByExpressions);
|
|
202
|
+
return await this._mapSearchResults(dbQuery, searchDistanceColumn, searchHighlightColumn, (rawDistance) => 1 - rawDistance);
|
|
203
|
+
}
|
|
204
|
+
async paradeDbSearch(options) {
|
|
205
|
+
const { query: { $parade: paradeQuery }, score: scoreOption = true, highlight, filter } = options;
|
|
206
|
+
const keyField = this.#table.id;
|
|
207
|
+
const paradeNativeQuery = convertQuery({ $parade: paradeQuery }, this.#table, this.#columnDefinitionsMap);
|
|
208
|
+
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.resolveTargetColumn(column)]));
|
|
209
|
+
let score;
|
|
210
|
+
if (scoreOption) {
|
|
211
|
+
const rawScore = sql `pdb.score(${keyField})`;
|
|
212
|
+
score = (isFunction(scoreOption) ? scoreOption(rawScore) : rawScore).as(bm25ScoreColumn);
|
|
213
|
+
selection[bm25ScoreColumn] = score;
|
|
214
|
+
}
|
|
215
|
+
if (isDefined(highlight)) {
|
|
216
|
+
const { source, ...highlightOptions } = (isString(highlight) || isInstanceOf(highlight, SQL))
|
|
217
|
+
? { source: highlight }
|
|
218
|
+
: highlight;
|
|
219
|
+
const sourceSql = isInstanceOf(source, SQL) ? source : sql `${this.resolveTargetColumn(source)}`;
|
|
220
|
+
const highlightParts = objectEntries(filterUndefinedObjectProperties(highlightOptions)).map(([key, value]) => sql `${sql.raw(toSnakeCase(key))} => ${sql `${value}`}`);
|
|
221
|
+
const snippet = sql `pdb.snippet(${sql.join([sourceSql, ...highlightParts], sql.raw(', '))})`;
|
|
222
|
+
selection[searchHighlightColumn] = snippet.as(searchHighlightColumn);
|
|
223
|
+
}
|
|
224
|
+
const whereClause = isDefined(filter)
|
|
225
|
+
? and(this.convertQuery(filter), paradeNativeQuery)
|
|
226
|
+
: paradeNativeQuery;
|
|
227
|
+
let dbQuery = this
|
|
228
|
+
.applySelect(this.session, selection, options.distinct)
|
|
229
|
+
.from(this.#table)
|
|
230
|
+
.where(whereClause)
|
|
231
|
+
.$dynamic();
|
|
232
|
+
if (isDefined(options.offset)) {
|
|
233
|
+
dbQuery = dbQuery.offset(options.offset);
|
|
234
|
+
}
|
|
235
|
+
if (isDefined(options.limit)) {
|
|
236
|
+
dbQuery = dbQuery.limit(options.limit);
|
|
237
|
+
}
|
|
238
|
+
const orderByExpressions = [];
|
|
192
239
|
if (isDefined(options.order)) {
|
|
193
240
|
const order = isFunction(options.order)
|
|
194
|
-
? options.order({
|
|
241
|
+
? options.order({
|
|
242
|
+
get score() {
|
|
243
|
+
return assertDefinedPass(score, 'Score is disabled.');
|
|
244
|
+
},
|
|
245
|
+
})
|
|
195
246
|
: options.order;
|
|
196
247
|
orderByExpressions.push(...this.convertOrderBy(order));
|
|
197
248
|
}
|
|
198
|
-
else if (
|
|
199
|
-
orderByExpressions.push(
|
|
249
|
+
else if (isDefined(score)) {
|
|
250
|
+
orderByExpressions.push(desc(score));
|
|
200
251
|
}
|
|
201
252
|
dbQuery = dbQuery.orderBy(...orderByExpressions);
|
|
202
|
-
|
|
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
|
-
})));
|
|
253
|
+
return await this._mapSearchResults(dbQuery, bm25ScoreColumn, searchHighlightColumn);
|
|
210
254
|
}
|
|
211
255
|
/**
|
|
212
256
|
* Performs a full-text search and returns entities ranked by relevance.
|
|
213
|
-
* This method
|
|
214
|
-
* @param
|
|
215
|
-
* @param options Search options including ranking, and highlighting configuration.
|
|
257
|
+
* This method dispatches to the appropriate search implementation based on the `method` option.
|
|
258
|
+
* @param options Search options including method, query text, ranking, and highlighting configuration.
|
|
216
259
|
* @returns A promise that resolves to an array of search results, including the entity, score, and optional highlight.
|
|
217
260
|
*/
|
|
218
|
-
async search(
|
|
219
|
-
|
|
261
|
+
async search(options) {
|
|
262
|
+
return await match(options.query)
|
|
263
|
+
.with({ $tsvector: P.any }, async () => await this.tsVectorSearch(options))
|
|
264
|
+
.with({ $trigram: P.any }, async () => await this.trigramSearch(options))
|
|
265
|
+
.with({ $parade: P.any }, async () => await this.paradeDbSearch(options))
|
|
266
|
+
.exhaustive();
|
|
220
267
|
}
|
|
221
268
|
/**
|
|
222
269
|
* Loads a single entity by its ID.
|
|
@@ -846,17 +893,27 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
846
893
|
.returning();
|
|
847
894
|
return await this.mapManyToEntity(rows);
|
|
848
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Resolves a target column from an object path or column definition to a Drizzle SQL wrapper.
|
|
898
|
+
* @param target The object path or column definition.
|
|
899
|
+
*/
|
|
900
|
+
resolveTargetColumn(target) {
|
|
901
|
+
return resolveTargetColumn(target, this.#table, this.#columnDefinitionsMap);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Resolves multiple target columns from object paths or column definitions to Drizzle SQL wrappers.
|
|
905
|
+
* @param targets The object paths or column definitions.
|
|
906
|
+
*/
|
|
907
|
+
resolveTargetColumns(targets) {
|
|
908
|
+
return targets.map((target) => this.resolveTargetColumn(target));
|
|
909
|
+
}
|
|
849
910
|
/**
|
|
850
911
|
* Retrieves the Drizzle PgColumn for a given object path or column definition.
|
|
851
912
|
* @param pathOrColumn The object path or column definition.
|
|
852
913
|
* @returns The corresponding PgColumn.
|
|
853
914
|
*/
|
|
854
915
|
getColumn(pathOrColumn) {
|
|
855
|
-
|
|
856
|
-
const columnName = assertDefinedPass(this.#columnDefinitionsMap.get(pathOrColumn), `Could not map ${pathOrColumn} to column.`).name;
|
|
857
|
-
return this.#table[columnName];
|
|
858
|
-
}
|
|
859
|
-
return this.#table[pathOrColumn.name];
|
|
916
|
+
return this.resolveTargetColumn(pathOrColumn);
|
|
860
917
|
}
|
|
861
918
|
getColumns(pathOrColumns) {
|
|
862
919
|
return pathOrColumns.map((column) => this.getColumn(column));
|
|
@@ -869,19 +926,20 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
869
926
|
convertOrderBy(order) {
|
|
870
927
|
if (isArray(order)) {
|
|
871
928
|
return order.map((item) => {
|
|
872
|
-
const
|
|
873
|
-
const
|
|
874
|
-
const column = isSQLWrapper(target) ? target : this.getColumn(target);
|
|
875
|
-
const direction = itemIsArray ? item[1] : 'asc';
|
|
929
|
+
const [target, direction = 'asc'] = isArray(item) ? item : [item];
|
|
930
|
+
const column = this.resolveTargetColumn(target);
|
|
876
931
|
return direction == 'asc' ? asc(column) : desc(column);
|
|
877
932
|
});
|
|
878
933
|
}
|
|
934
|
+
if (isSQLWrapper(order) || (order instanceof SQL) || (order instanceof SQL.Aliased)) {
|
|
935
|
+
return [asc(order)];
|
|
936
|
+
}
|
|
879
937
|
if (isString(order)) {
|
|
880
|
-
const column = this.
|
|
938
|
+
const column = this.resolveTargetColumn(order);
|
|
881
939
|
return [asc(column)];
|
|
882
940
|
}
|
|
883
941
|
return objectEntries(order).map(([path, direction]) => {
|
|
884
|
-
const column = this.
|
|
942
|
+
const column = this.resolveTargetColumn(path);
|
|
885
943
|
return direction == 'asc' ? asc(column) : desc(column);
|
|
886
944
|
});
|
|
887
945
|
}
|
|
@@ -983,7 +1041,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
983
1041
|
.with(false, () => applyTo.select(selection))
|
|
984
1042
|
.with(true, () => applyTo.selectDistinct(selection))
|
|
985
1043
|
.otherwise((targets) => {
|
|
986
|
-
const ons = targets.map((target) =>
|
|
1044
|
+
const ons = targets.map((target) => resolveTargetColumn(target, this.#table, this.#columnDefinitionsMap));
|
|
987
1045
|
return applyTo.selectDistinctOn(ons, selection);
|
|
988
1046
|
});
|
|
989
1047
|
return selectBuilder;
|
|
@@ -1072,6 +1130,18 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
1072
1130
|
}
|
|
1073
1131
|
return await this.#transformContext;
|
|
1074
1132
|
}
|
|
1133
|
+
async _mapSearchResults(rowsPromise, scoreColumnName, highlightColumnName, scoreTransformer = (s) => s) {
|
|
1134
|
+
const rows = await rowsPromise;
|
|
1135
|
+
const transformContext = await this.getTransformContext();
|
|
1136
|
+
return await toArrayAsync(mapAsync(rows, async (row) => {
|
|
1137
|
+
const { [scoreColumnName]: rawScore, [highlightColumnName]: highlight, ...entityRow } = row;
|
|
1138
|
+
return {
|
|
1139
|
+
entity: await this._mapToEntity(entityRow, transformContext),
|
|
1140
|
+
score: scoreTransformer(rawScore),
|
|
1141
|
+
highlight: highlight,
|
|
1142
|
+
};
|
|
1143
|
+
}));
|
|
1144
|
+
}
|
|
1075
1145
|
};
|
|
1076
1146
|
EntityRepository = __decorate([
|
|
1077
1147
|
Singleton()
|
package/orm/server/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BuildColumns, NotNull } from 'drizzle-orm';
|
|
2
|
-
import type { PgColumnBuilder, PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
2
|
+
import type { ExtraConfigColumn, PgColumn, PgColumnBuilder, PgTableWithColumns } from 'drizzle-orm/pg-core';
|
|
3
3
|
import type { CamelCase, ConditionalPick, SnakeCase } from 'type-fest';
|
|
4
4
|
import type { JsonPath } from '../../json-path/json-path.js';
|
|
5
5
|
import type { Record } from '../../schema/index.js';
|
|
@@ -27,11 +27,19 @@ type Column<Name extends string, T> = null extends T ? ColumnBuilder<Exclude<T,
|
|
|
27
27
|
export type ColumnPrefix<T> = T extends Tagged<unknown, EmbeddedConfigTag, {
|
|
28
28
|
prefix: infer Prefix;
|
|
29
29
|
}> ? Prefix extends string ? Prefix : '' : '';
|
|
30
|
+
export type ExtraConfigColumnsFromType<T extends EntityType = EntityType> = {
|
|
31
|
+
[P in keyof PgTableColumnsFromType<T>]: PgTableColumnsFromType<T>[P] extends PgColumn<infer U> ? ExtraConfigColumn<U> : never;
|
|
32
|
+
};
|
|
33
|
+
export type PgTableColumnsFromType<T extends EntityType, TableName extends string = string> = BuildColumns<TableName, {
|
|
34
|
+
[P in Exclude<keyof InstanceType<T>, keyof EmbeddedProperties<InstanceType<T>>>]: Column<CamelCase<Extract<P, string>>, InstanceType<T>[P]>;
|
|
35
|
+
} & UnionToIntersection<{
|
|
36
|
+
[P in keyof EmbeddedProperties<InstanceType<T>>]: EmbeddedColumns<InstanceType<T>[P], ColumnPrefix<InstanceType<T>[P]>>;
|
|
37
|
+
}[keyof EmbeddedProperties<InstanceType<T>>]>, 'pg'>;
|
|
30
38
|
export type PgTableFromType<T extends EntityType = EntityType, S extends string = string, TableName extends string = T extends Required<EntityType> ? SnakeCase<T['entityName']> : string> = PgTableWithColumns<{
|
|
31
39
|
name: TableName;
|
|
32
40
|
schema: S;
|
|
33
41
|
columns: BuildColumns<TableName, {
|
|
34
|
-
[P in Exclude<
|
|
42
|
+
[P in Exclude<keyof InstanceType<T>, keyof EmbeddedProperties<InstanceType<T>>>]: Column<CamelCase<Extract<P, string>>, InstanceType<T>[P]>;
|
|
35
43
|
} & UnionToIntersection<{
|
|
36
44
|
[P in keyof EmbeddedProperties<InstanceType<T>>]: EmbeddedColumns<InstanceType<T>[P], ColumnPrefix<InstanceType<T>[P]>>;
|
|
37
45
|
}[keyof EmbeddedProperties<InstanceType<T>>]>, 'pg'>;
|
package/orm/sqls.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AnyColumn, type Column, type SQL, type SQLChunk } from 'drizzle-orm';
|
|
2
2
|
import type { GetSelectTableSelection, SelectResultField, TableLike } from 'drizzle-orm/query-builders/select.types';
|
|
3
|
+
import type { TsVectorWeight } from './query.js';
|
|
3
4
|
import type { Uuid } from './types.js';
|
|
4
5
|
/** Drizzle SQL helper for getting the current transaction's timestamp. Returns a Date object. */
|
|
5
6
|
export declare const TRANSACTION_TIMESTAMP: SQL<Date>;
|
|
@@ -160,7 +161,7 @@ export declare function websearchToTsQuery(language: string | SQL, text: string
|
|
|
160
161
|
* @param weight The weight to assign.
|
|
161
162
|
* @returns A Drizzle SQL object representing the weighted tsvector.
|
|
162
163
|
*/
|
|
163
|
-
export declare function setweight(tsvector: SQL, weight:
|
|
164
|
+
export declare function setweight(tsvector: SQL, weight: TsVectorWeight): SQL;
|
|
164
165
|
/**
|
|
165
166
|
* Creates a PostgreSQL `ts_rank_cd` function call for relevance ranking.
|
|
166
167
|
* @param tsvector The document's tsvector.
|
|
@@ -196,26 +197,23 @@ export declare function tsHeadline(language: string | SQL, document: string | SQ
|
|
|
196
197
|
export declare function similarity(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
197
198
|
/**
|
|
198
199
|
* Creates a PostgreSQL `word_similarity` function call (from pg_trgm extension).
|
|
199
|
-
* Calculates the
|
|
200
|
+
* Calculates the greatest similarity between the set of trigrams in the first string and any continuous extent of an ordered set of trigrams in the second string.
|
|
200
201
|
* @param left The first text column or expression.
|
|
201
202
|
* @param right The second text value or expression to compare against.
|
|
202
203
|
* @returns A Drizzle SQL object representing the similarity score (0 to 1).
|
|
203
204
|
*/
|
|
204
205
|
export declare function wordSimilarity(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
205
206
|
/**
|
|
206
|
-
* Creates a PostgreSQL
|
|
207
|
-
*
|
|
208
|
-
* @param left The text column or expression.
|
|
209
|
-
* @param right The text value or expression to compare against.
|
|
210
|
-
* @returns A Drizzle SQL object representing
|
|
207
|
+
* Creates a PostgreSQL `strict_word_similarity` function call (from pg_trgm extension).
|
|
208
|
+
* Same as `word_similarity`, but forces extent boundaries to match word boundaries.
|
|
209
|
+
* @param left The first text column or expression.
|
|
210
|
+
* @param right The second text value or expression to compare against.
|
|
211
|
+
* @returns A Drizzle SQL object representing the similarity score (0 to 1).
|
|
211
212
|
*/
|
|
213
|
+
export declare function strictWordSimilarity(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
212
214
|
export declare function isSimilar(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<boolean>;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
* @param right The text value or expression to compare against.
|
|
219
|
-
* @returns A Drizzle SQL object representing the similarity distance.
|
|
220
|
-
*/
|
|
221
|
-
export declare function similarityDistance(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
215
|
+
export declare function isWordSimilar(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<boolean>;
|
|
216
|
+
export declare function isStrictWordSimilar(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<boolean>;
|
|
217
|
+
export declare function distance(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
218
|
+
export declare function wordDistance(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
219
|
+
export declare function strictWordDistance(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
package/orm/sqls.js
CHANGED
|
@@ -213,7 +213,7 @@ export function similarity(left, right) {
|
|
|
213
213
|
}
|
|
214
214
|
/**
|
|
215
215
|
* Creates a PostgreSQL `word_similarity` function call (from pg_trgm extension).
|
|
216
|
-
* Calculates the
|
|
216
|
+
* Calculates the greatest similarity between the set of trigrams in the first string and any continuous extent of an ordered set of trigrams in the second string.
|
|
217
217
|
* @param left The first text column or expression.
|
|
218
218
|
* @param right The second text value or expression to compare against.
|
|
219
219
|
* @returns A Drizzle SQL object representing the similarity score (0 to 1).
|
|
@@ -224,27 +224,44 @@ export function wordSimilarity(left, right) {
|
|
|
224
224
|
return sql `word_similarity(${leftSql}, ${rightSql})`;
|
|
225
225
|
}
|
|
226
226
|
/**
|
|
227
|
-
* Creates a PostgreSQL
|
|
228
|
-
*
|
|
229
|
-
* @param left The text column or expression.
|
|
230
|
-
* @param right The text value or expression to compare against.
|
|
231
|
-
* @returns A Drizzle SQL object representing
|
|
227
|
+
* Creates a PostgreSQL `strict_word_similarity` function call (from pg_trgm extension).
|
|
228
|
+
* Same as `word_similarity`, but forces extent boundaries to match word boundaries.
|
|
229
|
+
* @param left The first text column or expression.
|
|
230
|
+
* @param right The second text value or expression to compare against.
|
|
231
|
+
* @returns A Drizzle SQL object representing the similarity score (0 to 1).
|
|
232
232
|
*/
|
|
233
|
+
export function strictWordSimilarity(left, right) {
|
|
234
|
+
const leftSql = isString(left) ? sql `${left}` : left;
|
|
235
|
+
const rightSql = isString(right) ? sql `${right}` : right;
|
|
236
|
+
return sql `strict_word_similarity(${leftSql}, ${rightSql})`;
|
|
237
|
+
}
|
|
233
238
|
export function isSimilar(left, right) {
|
|
234
239
|
const leftSql = isString(left) ? sql `${left}` : left;
|
|
235
240
|
const rightSql = isString(right) ? sql `${right}` : right;
|
|
236
|
-
return sql `(${leftSql}
|
|
241
|
+
return sql `(${leftSql} % ${rightSql})`;
|
|
237
242
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
export function isWordSimilar(left, right) {
|
|
244
|
+
const leftSql = isString(left) ? sql `${left}` : left;
|
|
245
|
+
const rightSql = isString(right) ? sql `${right}` : right;
|
|
246
|
+
return sql `(${leftSql} <% ${rightSql})`;
|
|
247
|
+
}
|
|
248
|
+
export function isStrictWordSimilar(left, right) {
|
|
249
|
+
const leftSql = isString(left) ? sql `${left}` : left;
|
|
250
|
+
const rightSql = isString(right) ? sql `${right}` : right;
|
|
251
|
+
return sql `(${leftSql} <<% ${rightSql})`;
|
|
252
|
+
}
|
|
253
|
+
export function distance(left, right) {
|
|
254
|
+
const leftSql = isString(left) ? sql `${left}` : left;
|
|
255
|
+
const rightSql = isString(right) ? sql `${right}` : right;
|
|
256
|
+
return sql `(${leftSql} <-> ${rightSql})`;
|
|
257
|
+
}
|
|
258
|
+
export function wordDistance(left, right) {
|
|
259
|
+
const leftSql = isString(left) ? sql `${left}` : left;
|
|
260
|
+
const rightSql = isString(right) ? sql `${right}` : right;
|
|
261
|
+
return sql `(${leftSql} <<-> ${rightSql})`;
|
|
262
|
+
}
|
|
263
|
+
export function strictWordDistance(left, right) {
|
|
247
264
|
const leftSql = isString(left) ? sql `${left}` : left;
|
|
248
265
|
const rightSql = isString(right) ? sql `${right}` : right;
|
|
249
|
-
return sql `(${leftSql}
|
|
266
|
+
return sql `(${leftSql} <<<-> ${rightSql})`;
|
|
250
267
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.23",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
},
|
|
137
137
|
"peerDependencies": {
|
|
138
138
|
"@google-cloud/storage": "^7.17",
|
|
139
|
-
"@google/genai": "^1.
|
|
139
|
+
"@google/genai": "^1.28",
|
|
140
140
|
"@tstdl/angular": "^0.93",
|
|
141
141
|
"@zxcvbn-ts/core": "^3.0",
|
|
142
142
|
"@zxcvbn-ts/language-common": "^3.0",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE "test"."test" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
3
|
+
"title" text NOT NULL,
|
|
4
|
+
"content" text NOT NULL,
|
|
5
|
+
"tags" text NOT NULL,
|
|
6
|
+
"language" text NOT NULL
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE INDEX "test_id_title_content_tags_idx" ON "test"."test" USING bm25 ("id","title","content","tags") WITH (key_field='id');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "e703ecc5-9f40-4ecb-848d-c1f3663d484c",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -41,20 +41,40 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"indexes": {
|
|
44
|
-
"
|
|
45
|
-
"name": "
|
|
44
|
+
"test_id_title_content_tags_idx": {
|
|
45
|
+
"name": "test_id_title_content_tags_idx",
|
|
46
46
|
"columns": [
|
|
47
47
|
{
|
|
48
|
-
"expression": "
|
|
48
|
+
"expression": "id",
|
|
49
|
+
"isExpression": false,
|
|
50
|
+
"asc": true,
|
|
51
|
+
"nulls": "last"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"expression": "title",
|
|
55
|
+
"isExpression": false,
|
|
56
|
+
"asc": true,
|
|
57
|
+
"nulls": "last"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"expression": "content",
|
|
61
|
+
"isExpression": false,
|
|
62
|
+
"asc": true,
|
|
63
|
+
"nulls": "last"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"expression": "tags",
|
|
67
|
+
"isExpression": false,
|
|
49
68
|
"asc": true,
|
|
50
|
-
"isExpression": true,
|
|
51
69
|
"nulls": "last"
|
|
52
70
|
}
|
|
53
71
|
],
|
|
54
72
|
"isUnique": false,
|
|
55
73
|
"concurrently": false,
|
|
56
|
-
"method": "
|
|
57
|
-
"with": {
|
|
74
|
+
"method": "bm25",
|
|
75
|
+
"with": {
|
|
76
|
+
"key_field": "'id'"
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
},
|
|
60
80
|
"foreignKeys": {},
|
|
@@ -5,50 +5,8 @@
|
|
|
5
5
|
{
|
|
6
6
|
"idx": 0,
|
|
7
7
|
"version": "7",
|
|
8
|
-
"when":
|
|
9
|
-
"tag": "
|
|
10
|
-
"breakpoints": true
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"idx": 1,
|
|
14
|
-
"version": "7",
|
|
15
|
-
"when": 1760974454017,
|
|
16
|
-
"tag": "0001_organic_rhodey",
|
|
17
|
-
"breakpoints": true
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"idx": 2,
|
|
21
|
-
"version": "7",
|
|
22
|
-
"when": 1761138612314,
|
|
23
|
-
"tag": "0002_nice_squadron_supreme",
|
|
24
|
-
"breakpoints": true
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"idx": 3,
|
|
28
|
-
"version": "7",
|
|
29
|
-
"when": 1761142229231,
|
|
30
|
-
"tag": "0003_serious_mockingbird",
|
|
31
|
-
"breakpoints": true
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"idx": 4,
|
|
35
|
-
"version": "7",
|
|
36
|
-
"when": 1761143426736,
|
|
37
|
-
"tag": "0004_complete_pixie",
|
|
38
|
-
"breakpoints": true
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
"idx": 5,
|
|
42
|
-
"version": "7",
|
|
43
|
-
"when": 1761153443342,
|
|
44
|
-
"tag": "0005_bumpy_sabra",
|
|
45
|
-
"breakpoints": true
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"idx": 6,
|
|
49
|
-
"version": "7",
|
|
50
|
-
"when": 1761153500086,
|
|
51
|
-
"tag": "0006_overrated_post",
|
|
8
|
+
"when": 1761843226770,
|
|
9
|
+
"tag": "0000_nervous_iron_monger",
|
|
52
10
|
"breakpoints": true
|
|
53
11
|
}
|
|
54
12
|
]
|