@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.
Files changed (61) hide show
  1. package/application/application.js +3 -3
  2. package/authentication/server/module.d.ts +1 -1
  3. package/authentication/server/module.js +1 -6
  4. package/document-management/api/document-management.api.d.ts +0 -4
  5. package/document-management/service-models/document.service-model.d.ts +0 -2
  6. package/injector/injector.js +22 -22
  7. package/injector/resolve-chain.d.ts +7 -5
  8. package/injector/resolve-chain.js +16 -14
  9. package/logger/manager.js +3 -3
  10. package/orm/data-types/bytea.d.ts +4 -14
  11. package/orm/data-types/bytea.js +2 -2
  12. package/orm/data-types/common.d.ts +18 -0
  13. package/orm/data-types/common.js +11 -0
  14. package/orm/data-types/index.d.ts +1 -0
  15. package/orm/data-types/index.js +1 -0
  16. package/orm/data-types/numeric-date.d.ts +4 -15
  17. package/orm/data-types/numeric-date.js +2 -2
  18. package/orm/data-types/timestamp.d.ts +4 -15
  19. package/orm/data-types/timestamp.js +2 -2
  20. package/orm/data-types/tsvector.d.ts +3 -13
  21. package/orm/data-types/tsvector.js +2 -2
  22. package/orm/decorators.d.ts +16 -54
  23. package/orm/decorators.js +24 -37
  24. package/orm/entity.d.ts +6 -9
  25. package/orm/entity.js +1 -2
  26. package/orm/query.d.ts +199 -61
  27. package/orm/query.js +2 -2
  28. package/orm/repository.types.d.ts +38 -9
  29. package/orm/server/drizzle/schema-converter.js +40 -118
  30. package/orm/server/query-converter.d.ts +21 -7
  31. package/orm/server/query-converter.js +194 -38
  32. package/orm/server/repository.d.ts +39 -22
  33. package/orm/server/repository.js +141 -71
  34. package/orm/server/types.d.ts +10 -2
  35. package/orm/sqls.d.ts +14 -16
  36. package/orm/sqls.js +34 -17
  37. package/package.json +2 -2
  38. package/test/drizzle/0000_nervous_iron_monger.sql +9 -0
  39. package/test/drizzle/meta/0000_snapshot.json +27 -7
  40. package/test/drizzle/meta/_journal.json +2 -44
  41. package/test/test.model.js +2 -6
  42. package/test1.js +18 -5
  43. package/test6.js +23 -35
  44. package/types/types.d.ts +8 -5
  45. package/utils/equals.js +2 -2
  46. package/utils/format-error.js +2 -2
  47. package/utils/helpers.js +3 -2
  48. package/utils/object/object.d.ts +4 -4
  49. package/test/drizzle/0000_sudden_sphinx.sql +0 -9
  50. package/test/drizzle/0001_organic_rhodey.sql +0 -2
  51. package/test/drizzle/0002_nice_squadron_supreme.sql +0 -1
  52. package/test/drizzle/0003_serious_mockingbird.sql +0 -1
  53. package/test/drizzle/0004_complete_pixie.sql +0 -1
  54. package/test/drizzle/0005_bumpy_sabra.sql +0 -1
  55. package/test/drizzle/0006_overrated_post.sql +0 -6
  56. package/test/drizzle/meta/0001_snapshot.json +0 -79
  57. package/test/drizzle/meta/0002_snapshot.json +0 -63
  58. package/test/drizzle/meta/0003_snapshot.json +0 -73
  59. package/test/drizzle/meta/0004_snapshot.json +0 -89
  60. package/test/drizzle/meta/0005_snapshot.json +0 -104
  61. package/test/drizzle/meta/0006_snapshot.json +0 -104
@@ -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>;
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 word similarity between two strings based on trigram matching.
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 `%` operator call (from pg_trgm extension) for similarity check.
228
- * Returns true if the similarity between the two arguments is greater than the current similarity threshold.
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 a boolean similarity check.
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}) % (${rightSql})`;
241
+ return sql `(${leftSql} % ${rightSql})`;
237
242
  }
238
- /**
239
- * Creates a PostgreSQL `<->` operator call (from pg_trgm extension) for similarity distance.
240
- * Returns the "distance" between the arguments, that is one minus the similarity() value.
241
- * This is useful for ordering by similarity with an index.
242
- * @param left The text column or expression.
243
- * @param right The text value or expression to compare against.
244
- * @returns A Drizzle SQL object representing the similarity distance.
245
- */
246
- export function similarityDistance(left, right) {
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}) <-> (${rightSql})`;
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.21",
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.27",
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": "5c221821-4273-489d-b533-2f49653ab8d1",
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
- "test_title_content_tags_idx": {
45
- "name": "test_title_content_tags_idx",
44
+ "test_id_title_content_tags_idx": {
45
+ "name": "test_id_title_content_tags_idx",
46
46
  "columns": [
47
47
  {
48
- "expression": "(setweight(to_tsvector(\"language\"::regconfig, \"title\"), 'A') || setweight(to_tsvector(\"language\"::regconfig, \"content\"), 'B') || setweight(to_tsvector(\"language\"::regconfig, \"tags\"), 'C'))",
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": "gin",
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": 1760697525756,
9
- "tag": "0000_sudden_sphinx",
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
  ]