@tstdl/base 0.93.20 → 0.93.22

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.
Files changed (68) hide show
  1. package/application/application.js +1 -1
  2. package/audit/auditor.js +1 -1
  3. package/authentication/server/authentication.service.js +1 -1
  4. package/authentication/server/module.d.ts +1 -1
  5. package/authentication/server/module.js +1 -6
  6. package/document-management/api/document-management.api.d.ts +0 -4
  7. package/document-management/server/services/singleton.js +1 -1
  8. package/document-management/service-models/document.service-model.d.ts +0 -2
  9. package/injector/injector.d.ts +1 -1
  10. package/injector/injector.js +25 -13
  11. package/injector/types.d.ts +1 -1
  12. package/key-value-store/postgres/key-value-store.service.js +1 -1
  13. package/lock/postgres/provider.js +1 -1
  14. package/logger/manager.js +3 -3
  15. package/mail/mail.service.js +1 -1
  16. package/orm/data-types/bytea.d.ts +4 -14
  17. package/orm/data-types/bytea.js +2 -2
  18. package/orm/data-types/common.d.ts +18 -0
  19. package/orm/data-types/common.js +11 -0
  20. package/orm/data-types/index.d.ts +1 -0
  21. package/orm/data-types/index.js +1 -0
  22. package/orm/data-types/numeric-date.d.ts +4 -15
  23. package/orm/data-types/numeric-date.js +2 -2
  24. package/orm/data-types/timestamp.d.ts +4 -15
  25. package/orm/data-types/timestamp.js +2 -2
  26. package/orm/data-types/tsvector.d.ts +3 -13
  27. package/orm/data-types/tsvector.js +2 -2
  28. package/orm/decorators.d.ts +16 -54
  29. package/orm/decorators.js +24 -37
  30. package/orm/entity.d.ts +6 -9
  31. package/orm/entity.js +1 -2
  32. package/orm/query.d.ts +199 -61
  33. package/orm/query.js +2 -2
  34. package/orm/repository.types.d.ts +38 -9
  35. package/orm/server/drizzle/schema-converter.js +40 -118
  36. package/orm/server/query-converter.d.ts +21 -7
  37. package/orm/server/query-converter.js +194 -38
  38. package/orm/server/repository.d.ts +39 -22
  39. package/orm/server/repository.js +141 -71
  40. package/orm/server/types.d.ts +10 -2
  41. package/orm/sqls.d.ts +14 -16
  42. package/orm/sqls.js +34 -17
  43. package/package.json +2 -2
  44. package/queue/postgres/queue.js +1 -1
  45. package/test/drizzle/0000_nervous_iron_monger.sql +9 -0
  46. package/test/drizzle/meta/0000_snapshot.json +27 -7
  47. package/test/drizzle/meta/_journal.json +2 -44
  48. package/test/test.model.js +2 -6
  49. package/test1.js +18 -5
  50. package/test6.js +21 -35
  51. package/types/types.d.ts +8 -5
  52. package/utils/equals.js +2 -2
  53. package/utils/format-error.js +2 -2
  54. package/utils/helpers.js +3 -2
  55. package/utils/object/object.d.ts +4 -4
  56. package/test/drizzle/0000_sudden_sphinx.sql +0 -9
  57. package/test/drizzle/0001_organic_rhodey.sql +0 -2
  58. package/test/drizzle/0002_nice_squadron_supreme.sql +0 -1
  59. package/test/drizzle/0003_serious_mockingbird.sql +0 -1
  60. package/test/drizzle/0004_complete_pixie.sql +0 -1
  61. package/test/drizzle/0005_bumpy_sabra.sql +0 -1
  62. package/test/drizzle/0006_overrated_post.sql +0 -6
  63. package/test/drizzle/meta/0001_snapshot.json +0 -79
  64. package/test/drizzle/meta/0002_snapshot.json +0 -63
  65. package/test/drizzle/meta/0003_snapshot.json +0 -73
  66. package/test/drizzle/meta/0004_snapshot.json +0 -89
  67. package/test/drizzle/meta/0005_snapshot.json +0 -104
  68. package/test/drizzle/meta/0006_snapshot.json +0 -104
@@ -1,10 +1,10 @@
1
- import { SQL } from 'drizzle-orm';
1
+ import { SQL, type SQLWrapper } from 'drizzle-orm';
2
2
  import type { PgColumn, PgInsertValue, PgSelectBuilder, PgUpdateSetSource, SelectedFields } from 'drizzle-orm/pg-core';
3
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
- import { Entity, type EntityMetadataAttributes, type EntityType, type EntityWithoutMetadata } from '../entity.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';
4
+ import type { DeepPartial, Function, OneOrMany, Record, Type } from '../../types/index.js';
5
+ import { Entity, type BaseEntity, type EntityMetadataAttributes, type EntityType } from '../entity.js';
6
+ import type { ParadeDbSearchQuery, Query, TrigramSearchQuery, TsVectorSearchQuery } from '../query.js';
7
+ import type { EntityMetadataUpdate, EntityUpdate, LoadManyOptions, LoadOptions, NewEntity, Order, SearchOptions, SearchResult, TargetColumn, TargetColumnPath } 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';
@@ -26,8 +26,8 @@ type EntityRepositoryContext = {
26
26
  encryptionSecret: Uint8Array<ArrayBuffer> | undefined;
27
27
  transformContext: TransformContext | Promise<TransformContext> | undefined;
28
28
  };
29
- type InferSelect<T extends Entity | EntityWithoutMetadata = Entity | EntityWithoutMetadata> = PgTableFromType<EntityType<T>>['$inferSelect'];
30
- export declare class EntityRepository<T extends Entity | EntityWithoutMetadata = EntityWithoutMetadata> extends Transactional<EntityRepositoryContext> implements Resolvable<EntityType<T>> {
29
+ type InferSelect<T extends BaseEntity = BaseEntity> = PgTableFromType<EntityType<T>>['$inferSelect'];
30
+ export declare class EntityRepository<T extends BaseEntity = BaseEntity> extends Transactional<EntityRepositoryContext> implements Resolvable<EntityType<T>> {
31
31
  #private;
32
32
  readonly type: EntityType<T>;
33
33
  readonly typeName: string;
@@ -40,16 +40,22 @@ 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>[]>;
43
+ protected tsVectorSearch(options: SearchOptions<T> & {
44
+ query: TsVectorSearchQuery<T>;
45
+ }): Promise<SearchResult<T>[]>;
46
+ protected trigramSearch(options: SearchOptions<T> & {
47
+ query: TrigramSearchQuery<T>;
48
+ }): Promise<SearchResult<T>[]>;
49
+ protected paradeDbSearch(options: SearchOptions<T> & {
50
+ query: ParadeDbSearchQuery<T>;
51
+ }): Promise<SearchResult<T>[]>;
45
52
  /**
46
53
  * 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.
54
+ * This method dispatches to the appropriate search implementation based on the `method` option.
55
+ * @param options Search options including method, query text, ranking, and highlighting configuration.
50
56
  * @returns A promise that resolves to an array of search results, including the entity, score, and optional highlight.
51
57
  */
52
- search(_query: FullTextSearchQuery<T>['$fts'], _options?: SearchOptions<T>): Promise<SearchResult<T>[]>;
58
+ search(options: SearchOptions<T>): Promise<SearchResult<T>[]>;
53
59
  /**
54
60
  * Loads a single entity by its ID.
55
61
  * Throws `NotFoundError` if the entity is not found.
@@ -175,14 +181,14 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
175
181
  * @param entity The entity to insert.
176
182
  * @returns A promise that resolves to the inserted or existing entity.
177
183
  */
178
- insertIfNotExists(target: OneOrMany<Paths<UntaggedDeep<T>>>, entity: NewEntity<T>): Promise<T | undefined>;
184
+ insertIfNotExists(target: OneOrMany<TargetColumnPath<T>>, entity: NewEntity<T>): Promise<T | undefined>;
179
185
  /**
180
186
  * Inserts many entities if they do not already exist based on the target columns.
181
187
  * @param target The column(s) to use for conflict detection.
182
188
  * @param entities The entities to insert.
183
189
  * @returns A promise that resolves to the inserted or existing entities.
184
190
  */
185
- insertManyIfNotExists(target: OneOrMany<Paths<UntaggedDeep<T>>>, entities: NewEntity<T>[]): Promise<T[]>;
191
+ insertManyIfNotExists(target: OneOrMany<TargetColumnPath<T>>, entities: NewEntity<T>[]): Promise<T[]>;
186
192
  /**
187
193
  * Inserts an entity or updates it if a conflict occurs based on the target columns.
188
194
  * @param target The column(s) to use for conflict detection.
@@ -190,7 +196,7 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
190
196
  * @param update Optional update to apply if a conflict occurs. Defaults to the inserted entity's values.
191
197
  * @returns A promise that resolves to the inserted or updated entity.
192
198
  */
193
- upsert(target: OneOrMany<Paths<UntaggedDeep<T>>>, entity: NewEntity<T>, update?: EntityUpdate<T>): Promise<T>;
199
+ upsert(target: OneOrMany<TargetColumnPath<T>>, entity: NewEntity<T>, update?: EntityUpdate<T>): Promise<T>;
194
200
  /**
195
201
  * Inserts multiple entities or updates them if a conflict occurs based on the target columns.
196
202
  * @param target The column(s) to use for conflict detection.
@@ -198,7 +204,7 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
198
204
  * @param update Optional update to apply if a conflict occurs. Defaults to the inserted entity's values.
199
205
  * @returns A promise that resolves to an array of the inserted or updated entities.
200
206
  */
201
- upsertMany(target: OneOrMany<Paths<UntaggedDeep<T>>>, entities: NewEntity<T>[], update?: EntityUpdate<T>): Promise<T[]>;
207
+ upsertMany(target: OneOrMany<TargetColumnPath<T>>, entities: NewEntity<T>[], update?: EntityUpdate<T>): Promise<T[]>;
202
208
  /**
203
209
  * Updates an entity by its ID.
204
210
  * Throws `NotFoundError` if the entity is not found.
@@ -337,13 +343,23 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
337
343
  * @returns A promise that resolves to an array of the hard deleted entities.
338
344
  */
339
345
  hardDeleteManyByQuery(query: Query<T>): Promise<T[]>;
346
+ /**
347
+ * Resolves a target column from an object path or column definition to a Drizzle SQL wrapper.
348
+ * @param target The object path or column definition.
349
+ */
350
+ resolveTargetColumn(target: TargetColumn<T> | ColumnDefinition): SQLWrapper;
351
+ /**
352
+ * Resolves multiple target columns from object paths or column definitions to Drizzle SQL wrappers.
353
+ * @param targets The object paths or column definitions.
354
+ */
355
+ resolveTargetColumns(targets: readonly (TargetColumn<T> | ColumnDefinition)[]): SQLWrapper[];
340
356
  /**
341
357
  * Retrieves the Drizzle PgColumn for a given object path or column definition.
342
358
  * @param pathOrColumn The object path or column definition.
343
359
  * @returns The corresponding PgColumn.
344
360
  */
345
- getColumn(pathOrColumn: TargetColumnPaths<T> | ColumnDefinition): PgColumn;
346
- getColumns(pathOrColumns: (TargetColumnPaths<T> | ColumnDefinition)[]): PgColumn[];
361
+ getColumn(pathOrColumn: TargetColumnPath<T> | ColumnDefinition): PgColumn;
362
+ getColumns(pathOrColumns: readonly (TargetColumnPath<T> | ColumnDefinition)[]): PgColumn[];
347
363
  /**
348
364
  * Converts an Order object to an array of Drizzle SQL order expressions.
349
365
  * @param order The order object.
@@ -459,6 +475,7 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
459
475
  protected _mapUpdate(update: EntityUpdate<T>, transformContext: TransformContext): Promise<PgUpdateSetSource<PgTableFromType>>;
460
476
  protected _getMetadataUpdate(update?: EntityUpdate<T>): PgUpdateSetSource<PgTableFromType<EntityType<Entity>>> | undefined;
461
477
  protected getTransformContext(): Promise<TransformContext>;
478
+ protected _mapSearchResults(rowsPromise: Promise<Record[]>, scoreColumnName: string, highlightColumnName: string, scoreTransformer?: (rawScore: any) => number | undefined): Promise<SearchResult<T>[]>;
462
479
  }
463
480
  /**
464
481
  * Injects an EntityRepository instance for the specified entity type.
@@ -466,19 +483,19 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
466
483
  * @param type The entity type.
467
484
  * @returns An EntityRepository instance for the specified type.
468
485
  */
469
- export declare function injectRepository<T extends Entity | EntityWithoutMetadata>(type: EntityType<T>, session?: Database | PgTransaction | null): EntityRepository<T>;
486
+ export declare function injectRepository<T extends BaseEntity>(type: EntityType<T>, session?: Database | PgTransaction | null): EntityRepository<T>;
470
487
  /**
471
488
  * Injects an EntityRepository instance for the specified entity type.
472
489
  * @template T The entity type.
473
490
  * @param type The entity type.
474
491
  * @returns An EntityRepository instance for the specified type.
475
492
  */
476
- export declare function injectRepositoryAsync<T extends Entity | EntityWithoutMetadata>(type: EntityType<T>, session?: Database | PgTransaction | null): Promise<EntityRepository<T>>;
493
+ export declare function injectRepositoryAsync<T extends BaseEntity>(type: EntityType<T>, session?: Database | PgTransaction | null): Promise<EntityRepository<T>>;
477
494
  /**
478
495
  * Gets or creates a singleton EntityRepository class for the specified entity type.
479
496
  * @template T The entity type.
480
497
  * @param type The entity type.
481
498
  * @returns A singleton EntityRepository class for the specified type.
482
499
  */
483
- export declare function getRepository<T extends Entity | EntityWithoutMetadata>(type: EntityType<T>): Type<EntityRepository<T>>;
500
+ export declare function getRepository<T extends BaseEntity>(type: EntityType<T>): Type<EntityRepository<T>>;
484
501
  export {};
@@ -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, getColumnConcatenation, getTsQuery, getTsVector } from './query-converter.js';
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.getColumn(column)} + INTERVAL '${sql.raw(String(column.reflectionData.expirationField.after))} ms'`, TRANSACTION_TIMESTAMP)));
76
- const hardDeletionQuery = or(...hardExpirationColumns.map((column) => lte(sql `${this.getColumn(column)} + INTERVAL '${sql.raw(String(column.reflectionData.expirationField.after))} ms'`, TRANSACTION_TIMESTAMP)));
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 vectorSearch(options) {
100
- const { vector: { language = 'simple' } = {} } = options;
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(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);
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.getColumn(column)]));
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(options.highlight)) {
112
- const { source, ...headlineOptions } = (isString(options.highlight) || isInstanceOf(options.highlight, SQL))
113
- ? { source: options.highlight }
114
- : options.highlight;
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(options.filter)
124
- ? and(this.convertQuery(options.filter), vectorClause)
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
- 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
- })));
160
+ return await this._mapSearchResults(dbQuery, searchScoreColumn, searchHighlightColumn);
159
161
  }
160
162
  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();
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 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)
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({ score: sql `1 - ${distance}` })
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 (options.rank != false) {
199
- orderByExpressions.push(distance);
249
+ else if (isDefined(score)) {
250
+ orderByExpressions.push(desc(score));
200
251
  }
201
252
  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
- })));
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 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.
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(_query, _options) {
219
- throw new NotImplementedError('EntityRepository.search is not implemented yet.');
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
- if (isString(pathOrColumn)) {
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 itemIsArray = isArray(item);
873
- const target = itemIsArray ? item[0] : item;
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.getColumn(order);
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.getColumn(path);
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) => isString(target) ? this.getColumn(target) : 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()
@@ -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<Exclude<keyof InstanceType<T>, '__entityMeta__'>, keyof EmbeddedProperties<InstanceType<T>>>]: Column<CamelCase<Extract<P, string>>, InstanceType<T>[P]>;
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: 'A' | 'B' | 'C' | 'D'): SQL;
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 word similarity between two strings based on trigram matching.
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 `%` operator call (from pg_trgm extension) for similarity check.
207
- * Returns true if the similarity between the two arguments is greater than the current similarity threshold.
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 a boolean similarity check.
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
- * Creates a PostgreSQL `<->` operator call (from pg_trgm extension) for similarity distance.
215
- * Returns the "distance" between the arguments, that is one minus the similarity() value.
216
- * This is useful for ordering by similarity with an index.
217
- * @param left The text column or expression.
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>;