@tstdl/base 0.93.10 → 0.93.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/injector/decorators.d.ts +7 -0
- package/injector/decorators.js +10 -6
- package/injector/injector.js +73 -30
- package/orm/query.d.ts +33 -17
- package/orm/repository.types.d.ts +20 -7
- package/orm/server/query-converter.d.ts +1 -1
- package/orm/server/query-converter.js +7 -7
- package/orm/server/repository.d.ts +8 -4
- package/orm/server/repository.js +122 -91
- package/orm/sqls.d.ts +15 -5
- package/orm/sqls.js +20 -2
- package/package.json +3 -3
- package/test/test.model.js +1 -1
- package/test1.js +6 -3
- package/test2.d.ts +1 -0
- package/test2.js +32 -0
package/injector/decorators.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export type InjectableOptions<T, A, C extends Record = Record> = RegistrationOpt
|
|
|
10
10
|
alias?: OneOrMany<InjectionToken>;
|
|
11
11
|
/** Custom provider. Useful for example if initialization is required */
|
|
12
12
|
provider?: Provider<T, A, C>;
|
|
13
|
+
/**
|
|
14
|
+
* Which InjectableOptions to inherit from the parent (class which is extended from).
|
|
15
|
+
* If true (default), all options are inherited.
|
|
16
|
+
* If false, no options are inherited.
|
|
17
|
+
* If array of keys, only the provided options are inherited.
|
|
18
|
+
*/
|
|
19
|
+
inheritOptions?: boolean | (keyof InjectableOptions<T, A, C>)[];
|
|
13
20
|
};
|
|
14
21
|
export type InjectableOptionsWithoutLifecycle<T, A, C extends Record = Record> = Simplify<TypedOmit<InjectableOptions<T, A, C>, 'lifecycle'>>;
|
|
15
22
|
/**
|
package/injector/decorators.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
2
2
|
import { createClassDecorator, createDecorator, reflectionRegistry } from '../reflection/index.js';
|
|
3
3
|
import { toArray } from '../utils/array/array.js';
|
|
4
|
-
import {
|
|
4
|
+
import { filterObject } from '../utils/object/object.js';
|
|
5
|
+
import { isArray, isDefined, isFunction, isNotNull } from '../utils/type-guards.js';
|
|
5
6
|
import { Injector } from './injector.js';
|
|
6
7
|
import { injectMetadataSymbol, injectableMetadataSymbol, injectableOptionsSymbol } from './symbols.js';
|
|
7
8
|
/**
|
|
@@ -33,20 +34,23 @@ export function Injectable(options = {}) {
|
|
|
33
34
|
const { alias: aliases, provider, ...registrationOptions } = options;
|
|
34
35
|
const token = data.constructor;
|
|
35
36
|
let mergedRegistationOptions = registrationOptions;
|
|
36
|
-
if (isNotNull(metadata.parent)) {
|
|
37
|
+
if (isNotNull(metadata.parent) && (options.inheritOptions != false)) {
|
|
37
38
|
const parentOptions = reflectionRegistry.getMetadata(metadata.parent)?.data.tryGet(injectableOptionsSymbol);
|
|
38
39
|
if (isDefined(parentOptions)) {
|
|
39
40
|
const { alias: _, provider: __, ...parentRegistrationOptions } = parentOptions;
|
|
41
|
+
const optionsToInherit = isArray(options.inheritOptions)
|
|
42
|
+
? filterObject(parentRegistrationOptions, (_value, key) => options.inheritOptions.includes(key))
|
|
43
|
+
: parentRegistrationOptions;
|
|
40
44
|
mergedRegistationOptions = {
|
|
41
|
-
...
|
|
45
|
+
...optionsToInherit,
|
|
42
46
|
...registrationOptions,
|
|
43
|
-
providers: [...(
|
|
47
|
+
providers: [...(optionsToInherit.providers ?? []), ...(registrationOptions.providers ?? [])],
|
|
44
48
|
afterResolve: (instance, argument, context) => {
|
|
45
|
-
|
|
49
|
+
optionsToInherit.afterResolve?.(instance, argument, context);
|
|
46
50
|
registrationOptions.afterResolve?.(instance, argument, context);
|
|
47
51
|
},
|
|
48
52
|
metadata: {
|
|
49
|
-
...
|
|
53
|
+
...optionsToInherit.metadata,
|
|
50
54
|
...registrationOptions.metadata,
|
|
51
55
|
},
|
|
52
56
|
};
|
package/injector/injector.js
CHANGED
|
@@ -331,8 +331,16 @@ export class Injector {
|
|
|
331
331
|
const resolutionScoped = registration.options.lifecycle == 'resolution';
|
|
332
332
|
const injectorScoped = registration.options.lifecycle == 'injector';
|
|
333
333
|
const singletonScoped = registration.options.lifecycle == 'singleton';
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
let resolveArgument = argument ?? registration.options.defaultArgument;
|
|
335
|
+
if (isUndefined(resolveArgument) && isFunction(registration.options.defaultArgumentProvider)) {
|
|
336
|
+
try {
|
|
337
|
+
resolveArgument = registration.options.defaultArgumentProvider(injector.getResolveContext(resolutionTag, context, chain));
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
throw new ResolveError('Error in defaultArgumentProvider.', chain, error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const argumentIdentity = resolveArgumentIdentity(registration, resolveArgument, chain);
|
|
336
344
|
if (resolutionScoped && context.resolutionScopedResolutions.hasFlat(token, argumentIdentity)) {
|
|
337
345
|
return context.resolutionScopedResolutions.getFlat(token, argumentIdentity).value;
|
|
338
346
|
}
|
|
@@ -436,7 +444,13 @@ export class Injector {
|
|
|
436
444
|
if (isDefined(injectMetadata.injectArgumentMapper) && (!this.hasRegistration(injectToken) || isDefined(resolveArgument) || isUndefined(injectToken))) {
|
|
437
445
|
return injectMetadata.injectArgumentMapper(resolveArgument);
|
|
438
446
|
}
|
|
439
|
-
|
|
447
|
+
let parameterResolveArgument;
|
|
448
|
+
try {
|
|
449
|
+
parameterResolveArgument = injectMetadata.forwardArgumentMapper?.(resolveArgument) ?? injectMetadata.resolveArgumentProvider?.(this.getResolveContext(resolutionTag, context, getChain(injectToken)));
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
throw new ResolveError('Error in parameter argument provider (forwardArgumentMapper or resolveArgumentProvider).', getChain(injectToken), error);
|
|
453
|
+
}
|
|
440
454
|
const { forwardRef } = injectMetadata;
|
|
441
455
|
if (isDefined(forwardRef) && isDefined(injectMetadata.mapper)) {
|
|
442
456
|
const forwardToken = isFunction(forwardRef) ? forwardRef() : isBoolean(forwardRef) ? injectToken : forwardRef;
|
|
@@ -444,7 +458,15 @@ export class Injector {
|
|
|
444
458
|
}
|
|
445
459
|
const resolveFn = (injectMetadata.resolveAll == true) ? '_resolveAll' : '_resolve';
|
|
446
460
|
const resolved = this[resolveFn](injectToken, parameterResolveArgument, { optional: injectMetadata.optional, forwardRef, forwardRefTypeHint: injectMetadata.forwardRefTypeHint }, context, getChain(injectToken));
|
|
447
|
-
|
|
461
|
+
if (isDefined(injectMetadata.mapper)) {
|
|
462
|
+
try {
|
|
463
|
+
return injectMetadata.mapper(resolved);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
throw new ResolveError('Error in inject mapper.', getChain(injectToken), error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return resolved;
|
|
448
470
|
}
|
|
449
471
|
resolveInjection(token, argument, options, context, injectIndex, chain) {
|
|
450
472
|
return this._resolve(token, argument, options, context, chain.addInject(token, injectIndex));
|
|
@@ -546,21 +568,29 @@ function postProcess(context) {
|
|
|
546
568
|
}
|
|
547
569
|
derefForwardRefs(context);
|
|
548
570
|
for (const resolution of context.resolutions) {
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
571
|
+
try {
|
|
572
|
+
for (const afterResolveHandler of resolution.afterResolveRegistrations) {
|
|
573
|
+
const returnValue = afterResolveHandler(resolution.argument, resolution.afterResolveContext);
|
|
574
|
+
throwOnPromise(returnValue, 'registerAfterResolve()', resolution.chain);
|
|
575
|
+
}
|
|
576
|
+
if (!isTokenProvider(resolution.registration.provider) && isFunction(resolution.value?.[afterResolve])) {
|
|
577
|
+
const returnValue = resolution.value[afterResolve](resolution.argument, resolution.afterResolveContext);
|
|
578
|
+
throwOnPromise(returnValue, '[afterResolve]', resolution.chain);
|
|
579
|
+
}
|
|
580
|
+
if (isProviderWithInitializer(resolution.registration.provider)) {
|
|
581
|
+
const returnValue = resolution.registration.provider.afterResolve?.(resolution.value, resolution.argument, resolution.afterResolveContext);
|
|
582
|
+
throwOnPromise(returnValue, 'provider afterResolve handler', resolution.chain);
|
|
583
|
+
}
|
|
584
|
+
if (isDefined(resolution.registration.options.afterResolve)) {
|
|
585
|
+
const returnValue = resolution.registration.options.afterResolve(resolution.value, resolution.argument, resolution.afterResolveContext);
|
|
586
|
+
throwOnPromise(returnValue, 'registration afterResolve handler', resolution.chain);
|
|
587
|
+
}
|
|
560
588
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
589
|
+
catch (error) {
|
|
590
|
+
if (error instanceof ResolveError) {
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
throw new ResolveError('Error in afterResolve hook.', resolution.chain, error);
|
|
564
594
|
}
|
|
565
595
|
}
|
|
566
596
|
}
|
|
@@ -570,23 +600,36 @@ async function postProcessAsync(context) {
|
|
|
570
600
|
}
|
|
571
601
|
derefForwardRefs(context);
|
|
572
602
|
for (const resolution of context.resolutions) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
603
|
+
try {
|
|
604
|
+
for (const afterResolveHandler of resolution.afterResolveRegistrations) {
|
|
605
|
+
await afterResolveHandler(resolution.argument, resolution.afterResolveContext);
|
|
606
|
+
}
|
|
607
|
+
if (!isTokenProvider(resolution.registration.provider) && isFunction(resolution.value?.[afterResolve])) {
|
|
608
|
+
await resolution.value[afterResolve](resolution.argument, resolution.afterResolveContext);
|
|
609
|
+
}
|
|
610
|
+
if (isProviderWithInitializer(resolution.registration.provider)) {
|
|
611
|
+
await resolution.registration.provider.afterResolve?.(resolution.value, resolution.argument, resolution.afterResolveContext);
|
|
612
|
+
}
|
|
613
|
+
if (isDefined(resolution.registration.options.afterResolve)) {
|
|
614
|
+
await resolution.registration.options.afterResolve(resolution.value, resolution.argument, resolution.afterResolveContext);
|
|
615
|
+
}
|
|
581
616
|
}
|
|
582
|
-
|
|
583
|
-
|
|
617
|
+
catch (error) {
|
|
618
|
+
if (error instanceof ResolveError) {
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
throw new ResolveError('Error in async afterResolve hook.', resolution.chain, error);
|
|
584
622
|
}
|
|
585
623
|
}
|
|
586
624
|
}
|
|
587
|
-
function resolveArgumentIdentity(registration, resolveArgument) {
|
|
625
|
+
function resolveArgumentIdentity(registration, resolveArgument, chain) {
|
|
588
626
|
if (isDefined(registration.options.argumentIdentityProvider) && ((registration.options.lifecycle == 'resolution') || (registration.options.lifecycle == 'singleton'))) {
|
|
589
|
-
|
|
627
|
+
try {
|
|
628
|
+
return registration.options.argumentIdentityProvider(resolveArgument);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
throw new ResolveError('Error in argumentIdentityProvider.', chain, error);
|
|
632
|
+
}
|
|
590
633
|
}
|
|
591
634
|
return resolveArgument;
|
|
592
635
|
}
|
package/orm/query.d.ts
CHANGED
|
@@ -166,26 +166,42 @@ export type ComparisonGeoDistanceQuery = {
|
|
|
166
166
|
export type FullTextSearchQuery<T = any> = {
|
|
167
167
|
$fts: {
|
|
168
168
|
fields: readonly (Extract<keyof T, string>)[];
|
|
169
|
-
|
|
169
|
+
text: string | SQL<string>;
|
|
170
170
|
/**
|
|
171
171
|
* The search method to use.
|
|
172
172
|
* - 'vector': (Default) Standard full-text search using tsvector and tsquery.
|
|
173
|
-
* - '
|
|
174
|
-
*/
|
|
175
|
-
method?: 'vector' | 'similarity';
|
|
176
|
-
/**
|
|
177
|
-
* The parser to use for the query. Only applicable for 'vector' method.
|
|
178
|
-
*/
|
|
179
|
-
parser?: FtsParser;
|
|
180
|
-
/**
|
|
181
|
-
* The text search configuration (e.g., 'english', 'simple'). Can also be a SQL object for dynamic configuration. Only applicable for 'vector' method.
|
|
182
|
-
*/
|
|
183
|
-
language?: string | SQL<string>;
|
|
184
|
-
/**
|
|
185
|
-
* Assigns weights to fields for ranking.
|
|
186
|
-
* Keys are field names from `fields`, values are 'A', 'B', 'C', or 'D'.
|
|
187
|
-
* Fields without a specified weight will use the default. Only applicable for 'vector' method.
|
|
173
|
+
* - 'trigram': Trigram-based similarity search using the pg_trgm extension.
|
|
188
174
|
*/
|
|
189
|
-
|
|
175
|
+
method?: 'vector' | 'trigram';
|
|
176
|
+
vector?: {
|
|
177
|
+
/**
|
|
178
|
+
* The parser to use for the query. Only applicable for 'vector' method.
|
|
179
|
+
*/
|
|
180
|
+
parser?: FtsParser;
|
|
181
|
+
/**
|
|
182
|
+
* The text search configuration (e.g., 'english', 'simple'). Can also be a SQL object for dynamic configuration. Only applicable for 'vector' method.
|
|
183
|
+
*/
|
|
184
|
+
language?: string | SQL<string>;
|
|
185
|
+
/**
|
|
186
|
+
* Assigns weights to fields for ranking.
|
|
187
|
+
* Keys are field names from `fields`, values are 'A', 'B', 'C', or 'D'.
|
|
188
|
+
* Fields without a specified weight will use the default. Only applicable for 'vector' method.
|
|
189
|
+
*/
|
|
190
|
+
weights?: Partial<Record<Extract<keyof T, string>, 'A' | 'B' | 'C' | 'D'>>;
|
|
191
|
+
};
|
|
192
|
+
trigram?: {
|
|
193
|
+
/**
|
|
194
|
+
* Type of similarity to use for 'trigram' search.
|
|
195
|
+
* - 'normal': Standard trigram similarity (default).
|
|
196
|
+
* - 'word': Word-based similarity.
|
|
197
|
+
* - 'strict-word': Strict word-based similarity.
|
|
198
|
+
* @default 'normal'
|
|
199
|
+
*/
|
|
200
|
+
type?: 'phrase' | 'word' | 'strict-word';
|
|
201
|
+
/**
|
|
202
|
+
* Threshold for similarity matching (0 to 1). Only applicable for 'trigram' method.
|
|
203
|
+
*/
|
|
204
|
+
threshold?: number | SQL<number>;
|
|
205
|
+
};
|
|
190
206
|
};
|
|
191
207
|
};
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Defines types used by ORM repositories for operations like loading, updating, and creating entities.
|
|
4
4
|
* Includes types for ordering, loading options, and entity data structures for create/update operations.
|
|
5
5
|
*/
|
|
6
|
-
import type { Paths, Record, TypedOmit } from '../types/index.js';
|
|
6
|
+
import type { Paths, Record, SimplifyObject, TypedOmit } from '../types/index.js';
|
|
7
7
|
import type { UntaggedDeep } from '../types/tagged.js';
|
|
8
8
|
import type { SQL, SQLWrapper } from 'drizzle-orm';
|
|
9
9
|
import type { PartialDeep } from 'type-fest';
|
|
10
10
|
import type { Entity, EntityMetadata, EntityWithoutMetadata } from './entity.js';
|
|
11
|
-
import type { Query } from './query.js';
|
|
11
|
+
import type { FullTextSearchQuery, Query } from './query.js';
|
|
12
12
|
import type { TsHeadlineOptions } from './sqls.js';
|
|
13
13
|
type WithSql<T> = {
|
|
14
14
|
[P in keyof T]: T[P] extends Record ? WithSql<T[P]> : (T[P] | SQL);
|
|
@@ -84,31 +84,44 @@ export type HighlightOptions<T extends EntityWithoutMetadata> = {
|
|
|
84
84
|
* Options for the `search` method.
|
|
85
85
|
* @template T - The entity type.
|
|
86
86
|
*/
|
|
87
|
-
export type SearchOptions<T extends EntityWithoutMetadata> =
|
|
87
|
+
export type SearchOptions<T extends EntityWithoutMetadata> = SimplifyObject<FullTextSearchQuery<T>['$fts'] & TypedOmit<LoadManyOptions<T>, 'order'> & {
|
|
88
88
|
/**
|
|
89
89
|
* An additional filter to apply to the search query.
|
|
90
90
|
*/
|
|
91
91
|
filter?: Query<T>;
|
|
92
|
+
/**
|
|
93
|
+
* How to order the search results.
|
|
94
|
+
*/
|
|
95
|
+
order?: Order<T> | ((columns: {
|
|
96
|
+
score: SQL | SQL.Aliased<number>;
|
|
97
|
+
}) => Order<T>);
|
|
98
|
+
/**
|
|
99
|
+
* Whether to include a relevance score with each result. Only applicable for vector searches.
|
|
100
|
+
* - If `true`, the default score is included.
|
|
101
|
+
* - If a function is provided, it customizes the score calculation using the original score.
|
|
102
|
+
* - If no order is specified, results are ordered by score descending, when score is enabled.
|
|
103
|
+
* @default true
|
|
104
|
+
*/
|
|
105
|
+
score?: boolean | ((originalScore: SQL<number>) => SQL<number>);
|
|
92
106
|
/**
|
|
93
107
|
* Enable and configure ranking of search results.
|
|
94
108
|
* - If `true` (default), results are ordered by score descending using default ranking options.
|
|
95
109
|
* - If an `RankOptions` object is provided, ranking is customized.
|
|
96
|
-
* - If an `SQL` object is provided, it's used as a custom scoring expression.
|
|
97
110
|
* @default true
|
|
98
111
|
*/
|
|
99
|
-
rank?: boolean | RankOptions
|
|
112
|
+
rank?: boolean | RankOptions;
|
|
100
113
|
/**
|
|
101
114
|
* Enable and configure highlighting of search results.
|
|
102
115
|
*/
|
|
103
116
|
highlight?: TargetColumnPaths<T> | SQL<string> | HighlightOptions<T>;
|
|
104
|
-
}
|
|
117
|
+
}>;
|
|
105
118
|
/**
|
|
106
119
|
* Represents a single result from a full-text search operation.
|
|
107
120
|
* @template T - The entity type.
|
|
108
121
|
*/
|
|
109
122
|
export type SearchResult<T extends EntityWithoutMetadata> = {
|
|
110
123
|
entity: T;
|
|
111
|
-
score
|
|
124
|
+
score?: number;
|
|
112
125
|
highlight?: string;
|
|
113
126
|
};
|
|
114
127
|
/**
|
|
@@ -17,4 +17,4 @@ import type { ColumnDefinition, PgTableFromType } from './types.js';
|
|
|
17
17
|
export declare function convertQuery(query: Query, table: PgTableFromType, columnDefinitionsMap: Map<string, ColumnDefinition>): SQL;
|
|
18
18
|
export declare function getTsQuery(text: string | SQL, language: string | SQL, parser: FtsParser): SQL;
|
|
19
19
|
export declare function getTsVector(fields: readonly string[], language: string | SQL, table: PgTableFromType, columnDefinitionsMap: Map<string, ColumnDefinition>, weights?: Partial<Record<string, 'A' | 'B' | 'C' | 'D'>>): SQL;
|
|
20
|
-
export declare function
|
|
20
|
+
export declare function getColumnConcatenation(fields: readonly string[], table: PgTableFromType, columnDefinitionsMap: Map<string, ColumnDefinition>): SQL;
|
|
@@ -61,13 +61,13 @@ export function convertQuery(query, table, columnDefinitionsMap) {
|
|
|
61
61
|
const method = ftsQuery.method ?? 'vector';
|
|
62
62
|
const sqlValue = match(method)
|
|
63
63
|
.with('vector', () => {
|
|
64
|
-
const tsquery = getTsQuery(ftsQuery.
|
|
65
|
-
const tsvector = getTsVector(ftsQuery.fields, ftsQuery.language ?? 'simple', table, columnDefinitionsMap, ftsQuery.weights);
|
|
64
|
+
const tsquery = getTsQuery(ftsQuery.text, ftsQuery.vector?.language ?? 'simple', ftsQuery.vector?.parser ?? 'raw');
|
|
65
|
+
const tsvector = getTsVector(ftsQuery.fields, ftsQuery.vector?.language ?? 'simple', table, columnDefinitionsMap, ftsQuery.vector?.weights);
|
|
66
66
|
return sql `${tsvector} @@ ${tsquery}`;
|
|
67
67
|
})
|
|
68
|
-
.with('
|
|
69
|
-
const searchExpression =
|
|
70
|
-
return isSimilar(searchExpression, ftsQuery.
|
|
68
|
+
.with('trigram', () => {
|
|
69
|
+
const searchExpression = getColumnConcatenation(ftsQuery.fields, table, columnDefinitionsMap);
|
|
70
|
+
return isSimilar(searchExpression, ftsQuery.text);
|
|
71
71
|
})
|
|
72
72
|
.exhaustive();
|
|
73
73
|
conditions.push(sqlValue);
|
|
@@ -214,10 +214,10 @@ export function getTsVector(fields, language, table, columnDefinitionsMap, weigh
|
|
|
214
214
|
}), sql ` || `);
|
|
215
215
|
return tsvector;
|
|
216
216
|
}
|
|
217
|
-
export function
|
|
217
|
+
export function getColumnConcatenation(fields, table, columnDefinitionsMap) {
|
|
218
218
|
const columns = fields.map((field) => {
|
|
219
219
|
const columnDef = assertDefinedPass(columnDefinitionsMap.get(field), `Could not map property ${field} to column.`);
|
|
220
220
|
return table[columnDef.name];
|
|
221
221
|
});
|
|
222
|
-
return sql.join(columns, sql ` || ' ' || `)
|
|
222
|
+
return sql `(${sql.join(columns, sql ` || ' ' || `)})`;
|
|
223
223
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { SQL } from 'drizzle-orm';
|
|
2
|
-
import type { PgColumn, PgInsertValue, PgUpdateSetSource } from 'drizzle-orm/pg-core';
|
|
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, OneOrMany, Paths, Type, UntaggedDeep } from '../../types/index.js';
|
|
4
|
+
import type { DeepPartial, Function, OneOrMany, Paths, Record, Type, UntaggedDeep } from '../../types/index.js';
|
|
5
5
|
import { Entity, type EntityMetadataAttributes, type EntityType, type EntityWithoutMetadata } from '../entity.js';
|
|
6
6
|
import type { FullTextSearchQuery, Query } from '../query.js';
|
|
7
|
-
import type { EntityMetadataUpdate, EntityUpdate, LoadManyOptions, LoadOptions, NewEntity, Order, SearchOptions, SearchResult, TargetColumnPaths } from '../repository.types.js';
|
|
7
|
+
import type { EntityMetadataUpdate, EntityUpdate, LoadManyOptions, LoadOptions, NewEntity, Order, SearchOptions, SearchResult, TargetColumn, TargetColumnPaths } from '../repository.types.js';
|
|
8
8
|
import type { Database } from './database.js';
|
|
9
9
|
import type { PgTransaction } from './transaction.js';
|
|
10
10
|
import { Transactional } from './transactional.js';
|
|
@@ -40,6 +40,8 @@ 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
45
|
/**
|
|
44
46
|
* Performs a full-text search and returns entities ranked by relevance.
|
|
45
47
|
* This method is a convenience wrapper around `loadManyByQuery` with the `$fts` operator.
|
|
@@ -47,7 +49,7 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
|
|
|
47
49
|
* @param options Search options including ranking, and highlighting configuration.
|
|
48
50
|
* @returns A promise that resolves to an array of search results, including the entity, score, and optional highlight.
|
|
49
51
|
*/
|
|
50
|
-
search(
|
|
52
|
+
search(_query: FullTextSearchQuery<T>['$fts'], _options?: SearchOptions<T>): Promise<SearchResult<T>[]>;
|
|
51
53
|
/**
|
|
52
54
|
* Loads a single entity by its ID.
|
|
53
55
|
* Throws `NotFoundError` if the entity is not found.
|
|
@@ -445,6 +447,8 @@ export declare class EntityRepository<T extends Entity | EntityWithoutMetadata =
|
|
|
445
447
|
generated: undefined;
|
|
446
448
|
}, {}, {}>;
|
|
447
449
|
}>, "limit" | "where">;
|
|
450
|
+
applySelect<TApplyTo extends Record<'select' | 'selectDistinct' | 'selectDistinctOn', Function<any[], PgSelectBuilder<any, any>>>>(applyTo: TApplyTo, distinct?: boolean | TargetColumn<T>[]): PgSelectBuilder<undefined, ReturnType<TApplyTo['selectDistinct']> extends PgSelectBuilder<any, infer TResult> ? TResult : never>;
|
|
451
|
+
applySelect<TApplyTo extends Record<'select' | 'selectDistinct' | 'selectDistinctOn', Function<any[], PgSelectBuilder<any, any>>>, TSelection extends SelectedFields>(applyTo: TApplyTo, selection: TSelection, distinct?: boolean | TargetColumn<T>[]): PgSelectBuilder<TSelection, ReturnType<TApplyTo['selectDistinct']> extends PgSelectBuilder<any, infer TResult> ? TResult : never>;
|
|
448
452
|
protected getAttributesUpdate(attributes: SQL | EntityMetadataAttributes | undefined): SQL<unknown> | undefined;
|
|
449
453
|
protected _mapManyToEntity(columns: InferSelect[], transformContext: TransformContext): Promise<T[]>;
|
|
450
454
|
protected _mapToEntity(columns: InferSelect, transformContext: TransformContext): Promise<T>;
|
package/orm/server/repository.js
CHANGED
|
@@ -8,7 +8,7 @@ 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 {
|
|
11
|
+
import { NotImplementedError } from '../../errors/not-implemented.error.js';
|
|
12
12
|
import { Singleton } from '../../injector/decorators.js';
|
|
13
13
|
import { inject, injectArgument } from '../../injector/inject.js';
|
|
14
14
|
import { afterResolve, resolveArgumentType } from '../../injector/interfaces.js';
|
|
@@ -20,16 +20,17 @@ import { importSymmetricKey } from '../../utils/cryptography.js';
|
|
|
20
20
|
import { fromDeepObjectEntries, fromEntries, objectEntries } from '../../utils/object/object.js';
|
|
21
21
|
import { cancelableTimeout } from '../../utils/timing.js';
|
|
22
22
|
import { tryIgnoreAsync } from '../../utils/try-ignore.js';
|
|
23
|
-
import { assertDefined, assertDefinedPass, isArray, isBoolean, isDefined, isInstanceOf, isString, isUndefined } from '../../utils/type-guards.js';
|
|
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 {
|
|
27
|
+
import { TRANSACTION_TIMESTAMP, tsHeadline, tsRankCd } from '../sqls.js';
|
|
28
28
|
import { getColumnDefinitions, getColumnDefinitionsMap, getDrizzleTableFromType } from './drizzle/schema-converter.js';
|
|
29
|
-
import { convertQuery,
|
|
29
|
+
import { convertQuery, getColumnConcatenation, getTsQuery, getTsVector } 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
|
+
const searchDistanceColumn = '__tsl_distance';
|
|
33
34
|
const searchHighlightColumn = '__tsl_highlight';
|
|
34
35
|
export const repositoryType = Symbol('repositoryType');
|
|
35
36
|
/**
|
|
@@ -95,97 +96,59 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
95
96
|
};
|
|
96
97
|
return context;
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const { method = 'vector' } = query;
|
|
107
|
-
let whereClause;
|
|
108
|
-
let rankExpression;
|
|
109
|
-
let highlightExpression;
|
|
110
|
-
const rankEnabled = options?.rank ?? true;
|
|
111
|
-
match(method)
|
|
112
|
-
.with('similarity', () => {
|
|
113
|
-
if (isDefined(query.weights) || isDefined(query.parser) || isDefined(options?.highlight)) {
|
|
114
|
-
throw new NotSupportedError('`weights`, `parser`, and `highlight` are not applicable to similarity search.');
|
|
115
|
-
}
|
|
116
|
-
const searchExpression = getSimilaritySearchExpression(query.fields, this.table, this.#columnDefinitionsMap);
|
|
117
|
-
whereClause = isSimilar(searchExpression, query.query);
|
|
118
|
-
if (rankEnabled) {
|
|
119
|
-
rankExpression = isInstanceOf(options?.rank, SQL)
|
|
120
|
-
? options.rank
|
|
121
|
-
: similarity(searchExpression, query.query);
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
.with('vector', () => {
|
|
125
|
-
const { language = 'simple' } = query;
|
|
126
|
-
const languageSql = isString(language) ? language : sql `${language}`;
|
|
127
|
-
const tsquery = getTsQuery(query.query, languageSql, query.parser ?? 'raw');
|
|
128
|
-
const tsvector = getTsVector(query.fields, languageSql, this.table, this.#columnDefinitionsMap, query.weights);
|
|
129
|
-
whereClause = sql `${tsvector} @@ ${tsquery}`;
|
|
130
|
-
if (rankEnabled) {
|
|
131
|
-
if (isInstanceOf(options?.rank, SQL)) {
|
|
132
|
-
rankExpression = options.rank;
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
const rankOptions = isBoolean(options?.rank) ? undefined : options?.rank;
|
|
136
|
-
rankExpression = tsRankCd(tsvector, tsquery, rankOptions);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (isDefined(options?.highlight)) {
|
|
140
|
-
const { source, ...headlineOptions } = (isString(options.highlight) || isInstanceOf(options.highlight, SQL))
|
|
141
|
-
? { source: options.highlight }
|
|
142
|
-
: options.highlight;
|
|
143
|
-
const document = match(source)
|
|
144
|
-
.with(P.instanceOf(SQL), (s) => s)
|
|
145
|
-
.otherwise((paths) => {
|
|
146
|
-
const columns = this.getColumns(paths);
|
|
147
|
-
return sql.join(columns, sql ` || ' ' || `);
|
|
148
|
-
});
|
|
149
|
-
highlightExpression = tsHeadline(languageSql, document, tsquery, headlineOptions);
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
.exhaustive();
|
|
153
|
-
if (isDefined(options?.filter)) {
|
|
154
|
-
const filter = this.convertQuery(options.filter);
|
|
155
|
-
whereClause = and(whereClause, filter);
|
|
156
|
-
}
|
|
99
|
+
async vectorSearch(options) {
|
|
100
|
+
const { vector: { language = 'simple' } = {} } = options;
|
|
101
|
+
const languageSql = isString(language) ? language : sql `${language}`;
|
|
102
|
+
const tsquery = getTsQuery(options.text, languageSql, options.vector?.parser ?? 'raw');
|
|
103
|
+
const tsvector = getTsVector(options.fields, languageSql, this.table, this.#columnDefinitionsMap, options.vector?.weights);
|
|
104
|
+
const rawScore = (options.score != false) ? tsRankCd(tsvector, tsquery, isBoolean(options.rank) ? undefined : options.rank) : undefined;
|
|
105
|
+
const score = (isFunction(options.score) ? options.score(rawScore) : rawScore)?.as(searchScoreColumn);
|
|
106
|
+
const vectorClause = sql `${tsvector} @@ ${tsquery}`;
|
|
157
107
|
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.getColumn(column)]));
|
|
158
|
-
if (isDefined(
|
|
159
|
-
selection[searchScoreColumn] =
|
|
108
|
+
if (isDefined(score)) {
|
|
109
|
+
selection[searchScoreColumn] = score;
|
|
160
110
|
}
|
|
161
|
-
if (isDefined(
|
|
162
|
-
|
|
111
|
+
if (isDefined(options.highlight)) {
|
|
112
|
+
const { source, ...headlineOptions } = (isString(options.highlight) || isInstanceOf(options.highlight, SQL))
|
|
113
|
+
? { source: options.highlight }
|
|
114
|
+
: options.highlight;
|
|
115
|
+
const document = match(source)
|
|
116
|
+
.with(P.instanceOf(SQL), (s) => s)
|
|
117
|
+
.otherwise((paths) => {
|
|
118
|
+
const columns = this.getColumns(paths);
|
|
119
|
+
return sql.join(columns, sql ` || ' ' || `);
|
|
120
|
+
});
|
|
121
|
+
selection[searchHighlightColumn] = tsHeadline(languageSql, document, tsquery, headlineOptions).as(searchHighlightColumn);
|
|
163
122
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return this.session.selectDistinctOn(ons, selection);
|
|
170
|
-
})
|
|
123
|
+
const whereClause = isDefined(options.filter)
|
|
124
|
+
? and(this.convertQuery(options.filter), vectorClause)
|
|
125
|
+
: vectorClause;
|
|
126
|
+
let dbQuery = this
|
|
127
|
+
.applySelect(this.session, selection, options.distinct)
|
|
171
128
|
.from(this.#table)
|
|
172
129
|
.where(whereClause)
|
|
173
130
|
.$dynamic();
|
|
174
|
-
if (isDefined(options
|
|
131
|
+
if (isDefined(options.offset)) {
|
|
175
132
|
dbQuery = dbQuery.offset(options.offset);
|
|
176
133
|
}
|
|
177
|
-
if (isDefined(options
|
|
134
|
+
if (isDefined(options.limit)) {
|
|
178
135
|
dbQuery = dbQuery.limit(options.limit);
|
|
179
136
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
137
|
+
const orderByExpressions = [];
|
|
138
|
+
if (isDefined(options.order)) {
|
|
139
|
+
const order = isFunction(options.order)
|
|
140
|
+
? options.order({
|
|
141
|
+
get score() {
|
|
142
|
+
return assertDefinedPass(score, 'Score is disabled.');
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
: options.order;
|
|
146
|
+
orderByExpressions.push(...this.convertOrderBy(order));
|
|
188
147
|
}
|
|
148
|
+
else if (isDefined(score)) {
|
|
149
|
+
orderByExpressions.push(desc(score));
|
|
150
|
+
}
|
|
151
|
+
dbQuery = dbQuery.orderBy(...orderByExpressions);
|
|
189
152
|
const transformContext = await this.getTransformContext();
|
|
190
153
|
const rows = await dbQuery;
|
|
191
154
|
return await toArrayAsync(mapAsync(rows, async ({ [searchScoreColumn]: score, [searchHighlightColumn]: highlight, ...row }) => ({
|
|
@@ -194,6 +157,67 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
194
157
|
highlight: highlight,
|
|
195
158
|
})));
|
|
196
159
|
}
|
|
160
|
+
async trigramSearch(options) {
|
|
161
|
+
const distanceOperator = match(options.trigram?.type ?? 'phrase')
|
|
162
|
+
.with('phrase', () => '<->')
|
|
163
|
+
.with('word', () => '<<->')
|
|
164
|
+
.with('strict-word', () => '<<<->')
|
|
165
|
+
.exhaustive();
|
|
166
|
+
const distanceThresholdOperator = match(options.trigram?.type ?? 'phrase')
|
|
167
|
+
.with('phrase', () => '%')
|
|
168
|
+
.with('word', () => '<%')
|
|
169
|
+
.with('strict-word', () => '<<%')
|
|
170
|
+
.exhaustive();
|
|
171
|
+
// TODO: set similarity_threshold, word_similarity_threshold, strict_word_similarity_threshold
|
|
172
|
+
const searchExpression = getColumnConcatenation(options.fields, this.table, this.#columnDefinitionsMap);
|
|
173
|
+
const distance = sql `(${options.text} ${sql.raw(distanceOperator)} ${searchExpression})`.as(searchDistanceColumn);
|
|
174
|
+
const trigramClause = sql `(${options.text} ${sql.raw(distanceThresholdOperator)} ${searchExpression})`;
|
|
175
|
+
const selection = fromEntries(this.#columnDefinitions.map((column) => [column.name, this.getColumn(column)]));
|
|
176
|
+
selection[searchDistanceColumn] = distance;
|
|
177
|
+
const whereClause = isDefined(options.filter)
|
|
178
|
+
? and(this.convertQuery(options.filter), trigramClause)
|
|
179
|
+
: trigramClause;
|
|
180
|
+
let dbQuery = this
|
|
181
|
+
.applySelect(this.session, selection, options.distinct)
|
|
182
|
+
.from(this.#table)
|
|
183
|
+
.where(whereClause)
|
|
184
|
+
.$dynamic();
|
|
185
|
+
if (isDefined(options.offset)) {
|
|
186
|
+
dbQuery = dbQuery.offset(options.offset);
|
|
187
|
+
}
|
|
188
|
+
if (isDefined(options.limit)) {
|
|
189
|
+
dbQuery = dbQuery.limit(options.limit);
|
|
190
|
+
}
|
|
191
|
+
const orderByExpressions = [];
|
|
192
|
+
if (isDefined(options.order)) {
|
|
193
|
+
const order = isFunction(options.order)
|
|
194
|
+
? options.order({ score: sql `1 - ${distance}` })
|
|
195
|
+
: options.order;
|
|
196
|
+
orderByExpressions.push(...this.convertOrderBy(order));
|
|
197
|
+
}
|
|
198
|
+
else if (options.rank != false) {
|
|
199
|
+
orderByExpressions.push(distance);
|
|
200
|
+
}
|
|
201
|
+
dbQuery = dbQuery.orderBy(...orderByExpressions);
|
|
202
|
+
console.log(dbQuery.toSQL());
|
|
203
|
+
const transformContext = await this.getTransformContext();
|
|
204
|
+
const rows = await dbQuery;
|
|
205
|
+
return await toArrayAsync(mapAsync(rows, async ({ [searchDistanceColumn]: distance, [searchHighlightColumn]: highlight, ...row }) => ({
|
|
206
|
+
entity: await this._mapToEntity(row, transformContext),
|
|
207
|
+
score: (1 - distance),
|
|
208
|
+
highlight: highlight,
|
|
209
|
+
})));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Performs a full-text search and returns entities ranked by relevance.
|
|
213
|
+
* This method is a convenience wrapper around `loadManyByQuery` with the `$fts` operator.
|
|
214
|
+
* @param query The search query using the `$fts` operator.
|
|
215
|
+
* @param options Search options including ranking, and highlighting configuration.
|
|
216
|
+
* @returns A promise that resolves to an array of search results, including the entity, score, and optional highlight.
|
|
217
|
+
*/
|
|
218
|
+
async search(_query, _options) {
|
|
219
|
+
throw new NotImplementedError('EntityRepository.search is not implemented yet.');
|
|
220
|
+
}
|
|
197
221
|
/**
|
|
198
222
|
* Loads a single entity by its ID.
|
|
199
223
|
* Throws `NotFoundError` if the entity is not found.
|
|
@@ -288,13 +312,7 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
288
312
|
*/
|
|
289
313
|
async loadManyByQuery(query, options) {
|
|
290
314
|
const sqlQuery = this.convertQuery(query);
|
|
291
|
-
let dbQuery =
|
|
292
|
-
.with(false, () => this.session.select())
|
|
293
|
-
.with(true, () => this.session.selectDistinct())
|
|
294
|
-
.otherwise((targets) => {
|
|
295
|
-
const ons = targets.map((target) => isString(target) ? this.getColumn(target) : target);
|
|
296
|
-
return this.session.selectDistinctOn(ons);
|
|
297
|
-
})
|
|
315
|
+
let dbQuery = this.applySelect(this.session, options?.distinct)
|
|
298
316
|
.from(this.#table)
|
|
299
317
|
.where(sqlQuery)
|
|
300
318
|
.$dynamic();
|
|
@@ -957,6 +975,19 @@ let EntityRepository = class EntityRepository extends Transactional {
|
|
|
957
975
|
.where(sqlQuery)
|
|
958
976
|
.limit(1);
|
|
959
977
|
}
|
|
978
|
+
applySelect(applyTo, selectionOrDistinct, distinctOrNothing) {
|
|
979
|
+
const firstParameterIsDistinct = isBoolean(selectionOrDistinct) || isArray(selectionOrDistinct);
|
|
980
|
+
const selection = firstParameterIsDistinct ? undefined : selectionOrDistinct;
|
|
981
|
+
const distinct = firstParameterIsDistinct ? selectionOrDistinct : distinctOrNothing;
|
|
982
|
+
const selectBuilder = match(distinct ?? false)
|
|
983
|
+
.with(false, () => applyTo.select(selection))
|
|
984
|
+
.with(true, () => applyTo.selectDistinct(selection))
|
|
985
|
+
.otherwise((targets) => {
|
|
986
|
+
const ons = targets.map((target) => isString(target) ? this.getColumn(target) : target);
|
|
987
|
+
return applyTo.selectDistinctOn(ons, selection);
|
|
988
|
+
});
|
|
989
|
+
return selectBuilder;
|
|
990
|
+
}
|
|
960
991
|
getAttributesUpdate(attributes) {
|
|
961
992
|
if (isUndefined(attributes)) {
|
|
962
993
|
return undefined;
|
package/orm/sqls.d.ts
CHANGED
|
@@ -116,7 +116,9 @@ export declare function greatest<T extends (Column | SQL | number)[]>(...values:
|
|
|
116
116
|
[P in keyof T]: T[P] extends number ? Exclude<T[P], number> | SQL<number> : T[P];
|
|
117
117
|
}[number]>>;
|
|
118
118
|
export declare function greatest<T>(...values: T[]): SQL<SelectResultField<T>>;
|
|
119
|
-
export declare function
|
|
119
|
+
export declare function unnest<T>(array: SQL<readonly T[]>): SQL<T>;
|
|
120
|
+
export declare function toTsVector(language: string | SQL, text: string | Column | SQLChunk): SQL<string>;
|
|
121
|
+
export declare function tsvectorToArray(tsvector: SQL): SQL<string[]>;
|
|
120
122
|
/**
|
|
121
123
|
* Creates a PostgreSQL `to_tsquery` function call.
|
|
122
124
|
* This function parses text into a tsquery, respecting operators like & (AND), | (OR), and ! (NOT).
|
|
@@ -183,7 +185,7 @@ export declare function tsRankCd(tsvector: SQL, tsquery: SQL, options?: {
|
|
|
183
185
|
* Cross-Site Scripting (XSS) vulnerabilities. Ensure the `document` content is
|
|
184
186
|
* properly sanitized before rendering it in a browser if it comes from an untrusted source.
|
|
185
187
|
*/
|
|
186
|
-
export declare function tsHeadline(language: string | SQL, document: string | SQL | AnyColumn, tsquery: SQL, options?: TsHeadlineOptions): SQL<string>;
|
|
188
|
+
export declare function tsHeadline(language: string | SQL, document: string | SQL | SQL.Aliased | AnyColumn, tsquery: SQL, options?: TsHeadlineOptions): SQL<string>;
|
|
187
189
|
/**
|
|
188
190
|
* Creates a PostgreSQL `similarity` function call (from pg_trgm extension).
|
|
189
191
|
* Calculates the similarity between two strings based on trigram matching.
|
|
@@ -191,7 +193,15 @@ export declare function tsHeadline(language: string | SQL, document: string | SQ
|
|
|
191
193
|
* @param right The second text value or expression to compare against.
|
|
192
194
|
* @returns A Drizzle SQL object representing the similarity score (0 to 1).
|
|
193
195
|
*/
|
|
194
|
-
export declare function similarity(left: string | SQL | AnyColumn, right: string | SQL | AnyColumn): SQL<number>;
|
|
196
|
+
export declare function similarity(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
197
|
+
/**
|
|
198
|
+
* Creates a PostgreSQL `word_similarity` function call (from pg_trgm extension).
|
|
199
|
+
* Calculates the word similarity between two strings based on trigram matching.
|
|
200
|
+
* @param left The first text column or expression.
|
|
201
|
+
* @param right The second text value or expression to compare against.
|
|
202
|
+
* @returns A Drizzle SQL object representing the similarity score (0 to 1).
|
|
203
|
+
*/
|
|
204
|
+
export declare function wordSimilarity(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
|
195
205
|
/**
|
|
196
206
|
* Creates a PostgreSQL `%` operator call (from pg_trgm extension) for similarity check.
|
|
197
207
|
* Returns true if the similarity between the two arguments is greater than the current similarity threshold.
|
|
@@ -199,7 +209,7 @@ export declare function similarity(left: string | SQL | AnyColumn, right: string
|
|
|
199
209
|
* @param right The text value or expression to compare against.
|
|
200
210
|
* @returns A Drizzle SQL object representing a boolean similarity check.
|
|
201
211
|
*/
|
|
202
|
-
export declare function isSimilar(left: string | SQL | AnyColumn, right: string | SQL | AnyColumn): SQL<boolean>;
|
|
212
|
+
export declare function isSimilar(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<boolean>;
|
|
203
213
|
/**
|
|
204
214
|
* Creates a PostgreSQL `<->` operator call (from pg_trgm extension) for similarity distance.
|
|
205
215
|
* Returns the "distance" between the arguments, that is one minus the similarity() value.
|
|
@@ -208,4 +218,4 @@ export declare function isSimilar(left: string | SQL | AnyColumn, right: string
|
|
|
208
218
|
* @param right The text value or expression to compare against.
|
|
209
219
|
* @returns A Drizzle SQL object representing the similarity distance.
|
|
210
220
|
*/
|
|
211
|
-
export declare function similarityDistance(left: string | SQL | AnyColumn, right: string | SQL | AnyColumn): SQL<number>;
|
|
221
|
+
export declare function similarityDistance(left: string | SQL | SQL.Aliased | AnyColumn, right: string | SQL | SQL.Aliased | AnyColumn): SQL<number>;
|
package/orm/sqls.js
CHANGED
|
@@ -95,11 +95,17 @@ export function greatest(...values) {
|
|
|
95
95
|
const sqlValues = values.map((value) => isNumber(value) ? sql.raw(String(value)) : value);
|
|
96
96
|
return sql `greatest(${sql.join(sqlValues, sql.raw(', '))})`;
|
|
97
97
|
}
|
|
98
|
+
export function unnest(array) {
|
|
99
|
+
return sql `unnest(${array})`;
|
|
100
|
+
}
|
|
98
101
|
function getLanguageSql(language) {
|
|
99
102
|
return isString(language) ? sql `'${sql.raw(language)}'` : sql `${language}`;
|
|
100
103
|
}
|
|
101
|
-
export function toTsVector(language,
|
|
102
|
-
return sql `to_tsvector(${getLanguageSql(language)}, ${
|
|
104
|
+
export function toTsVector(language, text) {
|
|
105
|
+
return sql `to_tsvector(${getLanguageSql(language)}, ${text})`;
|
|
106
|
+
}
|
|
107
|
+
export function tsvectorToArray(tsvector) {
|
|
108
|
+
return sql `tsvector_to_array(${tsvector})`;
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
105
111
|
* Creates a PostgreSQL `to_tsquery` function call.
|
|
@@ -205,6 +211,18 @@ export function similarity(left, right) {
|
|
|
205
211
|
const rightSql = isString(right) ? sql `${right}` : right;
|
|
206
212
|
return sql `similarity(${leftSql}, ${rightSql})`;
|
|
207
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Creates a PostgreSQL `word_similarity` function call (from pg_trgm extension).
|
|
216
|
+
* Calculates the word similarity between two strings based on trigram matching.
|
|
217
|
+
* @param left The first text column or expression.
|
|
218
|
+
* @param right The second text value or expression to compare against.
|
|
219
|
+
* @returns A Drizzle SQL object representing the similarity score (0 to 1).
|
|
220
|
+
*/
|
|
221
|
+
export function wordSimilarity(left, right) {
|
|
222
|
+
const leftSql = isString(left) ? sql `${left}` : left;
|
|
223
|
+
const rightSql = isString(right) ? sql `${right}` : right;
|
|
224
|
+
return sql `word_similarity(${leftSql}, ${rightSql})`;
|
|
225
|
+
}
|
|
208
226
|
/**
|
|
209
227
|
* Creates a PostgreSQL `%` operator call (from pg_trgm extension) for similarity check.
|
|
210
228
|
* Returns true if the similarity between the two arguments is greater than the current similarity threshold.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.11",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
}
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
|
-
"@stylistic/eslint-plugin": "5.
|
|
165
|
+
"@stylistic/eslint-plugin": "5.5",
|
|
166
166
|
"@types/koa__router": "12.0",
|
|
167
167
|
"@types/luxon": "3.7",
|
|
168
168
|
"@types/mjml": "4.7",
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"@types/pg": "8.15",
|
|
172
172
|
"concurrently": "9.2",
|
|
173
173
|
"drizzle-kit": "0.31",
|
|
174
|
-
"eslint": "9.
|
|
174
|
+
"eslint": "9.38",
|
|
175
175
|
"globals": "16.4",
|
|
176
176
|
"tsc-alias": "1.8",
|
|
177
177
|
"typedoc-github-wiki-theme": "2.1",
|
package/test/test.model.js
CHANGED
|
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
-
import { EntityWithoutMetadata, GinIndex, Table } from '../orm/index.js';
|
|
10
|
+
import { EntityWithoutMetadata, GinIndex, Index, Table } from '../orm/index.js';
|
|
11
11
|
import { StringProperty } from '../schema/index.js';
|
|
12
12
|
let Test = class Test extends EntityWithoutMetadata {
|
|
13
13
|
title;
|
package/test1.js
CHANGED
|
@@ -41,10 +41,13 @@ async function bootstrap() {
|
|
|
41
41
|
}
|
|
42
42
|
async function main(_cancellationSignal) {
|
|
43
43
|
const repository = injectRepository(Test);
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if (await repository.count() == 0) {
|
|
45
|
+
await repository.insertMany(testData);
|
|
46
|
+
}
|
|
47
|
+
const result = await repository.trigramSearch({
|
|
46
48
|
fields: ['title', 'content', 'tags'],
|
|
47
|
-
|
|
49
|
+
text: 'Optimizing PostgreSQL Full-Text Search with GIN Indexes A deep dive into GIN index performance.',
|
|
50
|
+
trigram: { type: 'phrase', threshold: 0.1 },
|
|
48
51
|
});
|
|
49
52
|
console.log(result);
|
|
50
53
|
}
|
package/test2.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/test2.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { inject, Singleton } from './injector/index.js';
|
|
8
|
+
import { Application } from './application/application.js';
|
|
9
|
+
import { provideModule } from './application/providers.js';
|
|
10
|
+
import { PrettyPrintLogFormatter, provideConsoleLogTransport } from './logger/index.js';
|
|
11
|
+
let Foo = class Foo {
|
|
12
|
+
bar = inject(Bar);
|
|
13
|
+
};
|
|
14
|
+
Foo = __decorate([
|
|
15
|
+
Singleton()
|
|
16
|
+
], Foo);
|
|
17
|
+
let Bar = class Bar {
|
|
18
|
+
};
|
|
19
|
+
Bar = __decorate([
|
|
20
|
+
Singleton({
|
|
21
|
+
argumentIdentityProvider() {
|
|
22
|
+
throw new Error('haha');
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
], Bar);
|
|
26
|
+
function main() {
|
|
27
|
+
const foo = inject(Foo);
|
|
28
|
+
}
|
|
29
|
+
Application.run('Test', [
|
|
30
|
+
provideModule(main),
|
|
31
|
+
provideConsoleLogTransport(PrettyPrintLogFormatter)
|
|
32
|
+
]);
|