@tstdl/base 0.93.155 → 0.93.156
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/orm/sqls/sqls.d.ts +2 -2
- package/orm/sqls/sqls.js +43 -28
- package/orm/tests/build-jsonb.test.d.ts +1 -0
- package/orm/tests/build-jsonb.test.js +39 -0
- package/orm/tests/query-converter-complex.test.js +24 -19
- package/orm/tests/sql-helpers.test.js +1 -1
- package/package.json +1 -1
- package/test4.js +1 -1
- package/testing/integration-setup.js +1 -1
package/orm/sqls/sqls.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ import type { Uuid } from '../types.js';
|
|
|
13
13
|
/** Represents valid units for PostgreSQL interval values. */
|
|
14
14
|
export type IntervalUnit = 'millennium' | 'millenniums' | 'millennia' | 'century' | 'centuries' | 'decade' | 'decades' | 'year' | 'years' | 'day' | 'days' | 'hour' | 'hours' | 'minute' | 'minutes' | 'second' | 'seconds' | 'millisecond' | 'milliseconds' | 'microsecond' | 'microseconds';
|
|
15
15
|
export type ExclusiveColumnCondition = Column | boolean | SQL;
|
|
16
|
-
export declare const simpleJsonKeyPattern: RegExp;
|
|
17
16
|
export type TsHeadlineOptions = {
|
|
18
17
|
/**
|
|
19
18
|
* The longest headline to output.
|
|
@@ -64,6 +63,7 @@ export declare const RANDOM_UUID_V4: SQL<Uuid>;
|
|
|
64
63
|
export declare const RANDOM_UUID_V7: SQL<Uuid>;
|
|
65
64
|
export declare const SQL_TRUE: SQL<boolean>;
|
|
66
65
|
export declare const SQL_FALSE: SQL<boolean>;
|
|
66
|
+
export declare const SQL_NULL: SQL<null>;
|
|
67
67
|
export declare function enumValue<T extends EnumerationObject>(enumeration: T, dbEnum: PgEnumFromEnumeration<T> | string | null, value: EnumerationValue<T>): SQL<string>;
|
|
68
68
|
/**
|
|
69
69
|
* Generates a SQL `CASE` expression to enforce strict, mutually exclusive column usage based on a discriminator value.
|
|
@@ -114,7 +114,7 @@ export declare function exclusiveNotNull(...columns: Column[]): SQL;
|
|
|
114
114
|
* that defines the default condition to apply when a `Column` is provided in `conditionMapping`.
|
|
115
115
|
* By default, it generates an `IS NOT NULL` check.
|
|
116
116
|
*/
|
|
117
|
-
export declare function enumerationCaseWhen<T extends EnumerationObject>(enumeration: T, discriminator: Column, conditionMapping: Record<EnumerationValue<T>, Column | [Column, ...Column[]] | boolean | SQL>, defaultColumnCondition?: (
|
|
117
|
+
export declare function enumerationCaseWhen<T extends EnumerationObject>(enumeration: T, discriminator: Column, conditionMapping: Record<EnumerationValue<T>, Column | [Column, ...Column[]] | boolean | SQL>, defaultColumnCondition?: (columns: Column | [Column, ...Column[]]) => SQL<unknown>): SQL;
|
|
118
118
|
export declare function array<T>(values: readonly (SQL<T> | SQLChunk | T)[]): SQL<T[]>;
|
|
119
119
|
export declare function autoAlias<T>(column: AnyColumn<{
|
|
120
120
|
data: T;
|
package/orm/sqls/sqls.js
CHANGED
|
@@ -25,7 +25,6 @@ function isJsonb(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
|
-
export const simpleJsonKeyPattern = /^[a-zA-Z0-9_-]+$/u;
|
|
29
28
|
/** Drizzle SQL helper for getting the current transaction's timestamp. Returns a Date object. */
|
|
30
29
|
export const TRANSACTION_TIMESTAMP = sql `transaction_timestamp()`;
|
|
31
30
|
/** Drizzle SQL helper for generating a random UUID (v4). Returns a Uuid string. */
|
|
@@ -34,6 +33,7 @@ export const RANDOM_UUID_V4 = sql `gen_random_uuid()`;
|
|
|
34
33
|
export const RANDOM_UUID_V7 = sql `uuidv7()`;
|
|
35
34
|
export const SQL_TRUE = sql `TRUE`;
|
|
36
35
|
export const SQL_FALSE = sql `FALSE`;
|
|
36
|
+
export const SQL_NULL = sql `NULL`;
|
|
37
37
|
export function enumValue(enumeration, dbEnum, value) {
|
|
38
38
|
if (isNull(dbEnum)) {
|
|
39
39
|
const enumName = getEnumName(enumeration);
|
|
@@ -69,25 +69,45 @@ export function enumValue(enumeration, dbEnum, value) {
|
|
|
69
69
|
* @returns A SQL object representing the complete `CASE discriminator WHEN ... THEN ... ELSE FALSE` statement.
|
|
70
70
|
*/
|
|
71
71
|
export function exclusiveColumn(enumeration, discriminator, conditionMapping) {
|
|
72
|
+
// 1. Gather all unique columns across the entire mapping
|
|
72
73
|
const allColumns = objectValues(conditionMapping)
|
|
73
74
|
.filter(isNotNull)
|
|
74
|
-
.flatMap((value) => toArray(value).filter((
|
|
75
|
+
.flatMap((value) => toArray(value).filter((item) => isInstanceOf(item, Column)));
|
|
75
76
|
const participatingColumns = distinct(allColumns);
|
|
76
|
-
const
|
|
77
|
+
const kaseWhen = caseWhen(discriminator);
|
|
78
|
+
// 2. Iterate and build conditions safely
|
|
79
|
+
for (const [key, value] of objectEntries(conditionMapping)) {
|
|
77
80
|
if (isNull(value)) {
|
|
78
|
-
|
|
81
|
+
kaseWhen.when(enumValue(enumeration, null, key), SQL_FALSE);
|
|
82
|
+
continue;
|
|
79
83
|
}
|
|
80
|
-
const
|
|
84
|
+
const conditions = [];
|
|
85
|
+
const items = toArray(value);
|
|
86
|
+
// Identify required vs null columns
|
|
87
|
+
const requiredColumns = items.filter((item) => isInstanceOf(item, Column));
|
|
81
88
|
const nullColumns = participatingColumns.filter((column) => !requiredColumns.includes(column));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Push column constraints
|
|
90
|
+
for (const col of requiredColumns) {
|
|
91
|
+
conditions.push(sqlIsNotNull(col));
|
|
92
|
+
}
|
|
93
|
+
for (const col of nullColumns) {
|
|
94
|
+
conditions.push(sqlIsNull(col));
|
|
95
|
+
}
|
|
96
|
+
// Handle custom conditions (SQL or booleans)
|
|
97
|
+
for (const item of items) {
|
|
98
|
+
if (!isInstanceOf(item, Column)) {
|
|
99
|
+
if (isBoolean(item)) {
|
|
100
|
+
if (!item) {
|
|
101
|
+
conditions.push(SQL_FALSE);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
conditions.push(item);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const finalCondition = and(...conditions) ?? SQL_TRUE;
|
|
110
|
+
kaseWhen.when(enumValue(enumeration, null, key), finalCondition);
|
|
91
111
|
}
|
|
92
112
|
return kaseWhen.else(SQL_FALSE);
|
|
93
113
|
}
|
|
@@ -116,20 +136,17 @@ export function exclusiveNotNull(...columns) {
|
|
|
116
136
|
* that defines the default condition to apply when a `Column` is provided in `conditionMapping`.
|
|
117
137
|
* By default, it generates an `IS NOT NULL` check.
|
|
118
138
|
*/
|
|
119
|
-
export function enumerationCaseWhen(enumeration, discriminator, conditionMapping, defaultColumnCondition = (
|
|
120
|
-
const
|
|
139
|
+
export function enumerationCaseWhen(enumeration, discriminator, conditionMapping, defaultColumnCondition = (columns) => isArray(columns) ? and(...columns.map((col) => sqlIsNotNull(col))) : sqlIsNotNull(columns)) {
|
|
140
|
+
const kaseWhen = caseWhen(discriminator);
|
|
121
141
|
for (const [key, value] of objectEntries(conditionMapping)) {
|
|
122
142
|
const condition = match(value)
|
|
123
|
-
.with(P.boolean, (bool) => bool ? SQL_TRUE : SQL_FALSE)
|
|
124
|
-
|
|
143
|
+
.with(P.boolean, (bool) => (bool ? SQL_TRUE : SQL_FALSE))
|
|
144
|
+
// Check for both a single Column OR an Array of Columns
|
|
145
|
+
.when((val) => isInstanceOf(val, Column) || (isArray(val) && val.every((v) => isInstanceOf(v, Column))), (cols) => defaultColumnCondition(cols))
|
|
125
146
|
.otherwise((rawSql) => rawSql);
|
|
126
|
-
|
|
147
|
+
kaseWhen.when(enumValue(enumeration, null, key), condition);
|
|
127
148
|
}
|
|
128
|
-
return
|
|
129
|
-
CASE ${discriminator}
|
|
130
|
-
${sql.join(whens, sql `\n`)}
|
|
131
|
-
ELSE FALSE
|
|
132
|
-
END`;
|
|
149
|
+
return kaseWhen.else(SQL_FALSE);
|
|
133
150
|
}
|
|
134
151
|
export function array(values) {
|
|
135
152
|
const chunks = values.map((value) => isSQLWrapper(value) ? value : sql `${value}`);
|
|
@@ -411,9 +428,7 @@ export function jsonbBuildObject(properties) {
|
|
|
411
428
|
const chunks = [];
|
|
412
429
|
for (const [key, value] of entries) {
|
|
413
430
|
if (isDefined(value)) {
|
|
414
|
-
|
|
415
|
-
const sqlKey = isSimpleKey ? sql.raw(`'${key}'`) : sql `${key}`;
|
|
416
|
-
chunks.push(sqlKey, buildJsonb(value));
|
|
431
|
+
chunks.push(sql `${key}::text`, buildJsonb(value));
|
|
417
432
|
}
|
|
418
433
|
}
|
|
419
434
|
if (chunks.length == 0) {
|
|
@@ -442,5 +457,5 @@ export function buildJsonb(value) {
|
|
|
442
457
|
if (isObject(value)) {
|
|
443
458
|
return jsonbBuildObject(value);
|
|
444
459
|
}
|
|
445
|
-
return markAsJsonb(sql
|
|
460
|
+
return markAsJsonb(sql `to_jsonb(${value})`);
|
|
446
461
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { buildJsonb } from '../sqls/sqls.js';
|
|
4
|
+
describe('buildJsonb', () => {
|
|
5
|
+
const dialect = new PgDialect();
|
|
6
|
+
test('should build jsonb from simple object', () => {
|
|
7
|
+
const query = buildJsonb({ a: 1, b: 'foo' });
|
|
8
|
+
const { sql, params } = dialect.sqlToQuery(query);
|
|
9
|
+
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2), $3::text, to_jsonb($4))');
|
|
10
|
+
expect(params).toEqual(['a', 1, 'b', 'foo']);
|
|
11
|
+
});
|
|
12
|
+
test('should build jsonb from object with non-simple keys', () => {
|
|
13
|
+
const query = buildJsonb({ 'Betriebs-Nr.': '18182952' });
|
|
14
|
+
const { sql, params } = dialect.sqlToQuery(query);
|
|
15
|
+
// This is what failed before: it lacked the ::text cast
|
|
16
|
+
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2))');
|
|
17
|
+
expect(params).toEqual(['Betriebs-Nr.', '18182952']);
|
|
18
|
+
});
|
|
19
|
+
test('should build jsonb from nested structures', () => {
|
|
20
|
+
const query = buildJsonb({
|
|
21
|
+
additionalData: { 'Betriebs-Nr.': '18182952' },
|
|
22
|
+
tags: ['a', 'b']
|
|
23
|
+
});
|
|
24
|
+
const { sql, params } = dialect.sqlToQuery(query);
|
|
25
|
+
expect(sql).toBe('jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3)), $4::text, jsonb_build_array(to_jsonb($5), to_jsonb($6)))');
|
|
26
|
+
expect(params).toEqual(['additionalData', 'Betriebs-Nr.', '18182952', 'tags', 'a', 'b']);
|
|
27
|
+
});
|
|
28
|
+
test('should handle numbers correctly', () => {
|
|
29
|
+
const query = buildJsonb({ score: 0.5 });
|
|
30
|
+
const { sql, params } = dialect.sqlToQuery(query);
|
|
31
|
+
expect(sql).toBe('jsonb_build_object($1::text, to_jsonb($2))');
|
|
32
|
+
expect(params).toEqual(['score', 0.5]);
|
|
33
|
+
});
|
|
34
|
+
test('should handle null and empty structures', () => {
|
|
35
|
+
expect(dialect.sqlToQuery(buildJsonb(null)).sql).toBe('\'null\'::jsonb');
|
|
36
|
+
expect(dialect.sqlToQuery(buildJsonb({})).sql).toBe('\'{}\'::jsonb');
|
|
37
|
+
expect(dialect.sqlToQuery(buildJsonb([])).sql).toBe('\'[]\'::jsonb');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -7,13 +7,12 @@ 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 { describe, expect, test } from 'vitest';
|
|
11
|
-
import { sql } from 'drizzle-orm';
|
|
12
10
|
import { PgDialect } from 'drizzle-orm/pg-core';
|
|
13
|
-
import {
|
|
11
|
+
import { Integer, StringProperty } from '../../schema/index.js';
|
|
12
|
+
import { describe, expect, test } from 'vitest';
|
|
13
|
+
import { Table } from '../decorators.js';
|
|
14
14
|
import { Entity } from '../entity.js';
|
|
15
|
-
import {
|
|
16
|
-
import { getDrizzleTableFromType, getColumnDefinitionsMap } from '../server/drizzle/schema-converter.js';
|
|
15
|
+
import { getColumnDefinitionsMap, getDrizzleTableFromType } from '../server/drizzle/schema-converter.js';
|
|
17
16
|
import { convertQuery } from '../server/query-converter.js';
|
|
18
17
|
describe('ORM Query Converter Complex', () => {
|
|
19
18
|
const dialect = new PgDialect();
|
|
@@ -49,10 +48,11 @@ describe('ORM Query Converter Complex', () => {
|
|
|
49
48
|
},
|
|
50
49
|
};
|
|
51
50
|
const condition = convertQuery(q, table, colMap);
|
|
52
|
-
const
|
|
53
|
-
expect(
|
|
54
|
-
expect(
|
|
55
|
-
expect(
|
|
51
|
+
const { sql, params } = dialect.sqlToQuery(condition);
|
|
52
|
+
expect(sql).toContain('setweight(to_tsvector($1, "test"."complex_items"."name"), \'A\')');
|
|
53
|
+
expect(sql).toContain('setweight(to_tsvector($2, "test"."complex_items"."description"), \'B\')');
|
|
54
|
+
expect(sql).toContain('websearch_to_tsquery($3, $4)');
|
|
55
|
+
expect(params).toEqual(['english', 'english', 'english', 'search term']);
|
|
56
56
|
});
|
|
57
57
|
test('should handle ParadeDB $parade match with tokenizer', () => {
|
|
58
58
|
const q = {
|
|
@@ -66,9 +66,10 @@ describe('ORM Query Converter Complex', () => {
|
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
68
|
const condition = convertQuery(q, table, colMap);
|
|
69
|
-
const
|
|
69
|
+
const { sql, params } = dialect.sqlToQuery(condition);
|
|
70
70
|
// Tokenizer is supported via JSON object syntax
|
|
71
|
-
expect(
|
|
71
|
+
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3), $4::text, jsonb_build_object($5::text, to_jsonb($6), $7::text, to_jsonb($8), $9::text, to_jsonb($10))))::pdb.query');
|
|
72
|
+
expect(params).toEqual(['match', 'value', 'test', 'tokenizer', 'type', 'ngram', 'min_gram', 3, 'max_gram', 3]);
|
|
72
73
|
});
|
|
73
74
|
test('should handle ParadeDB $parade range', () => {
|
|
74
75
|
const q = {
|
|
@@ -82,9 +83,10 @@ describe('ORM Query Converter Complex', () => {
|
|
|
82
83
|
},
|
|
83
84
|
};
|
|
84
85
|
const condition = convertQuery(q, table, colMap);
|
|
85
|
-
const
|
|
86
|
+
const { sql, params } = dialect.sqlToQuery(condition);
|
|
86
87
|
// This should fall back to convertParadeComparisonQuery with recursive jsonb build
|
|
87
|
-
expect(
|
|
88
|
+
expect(sql).toContain('"test"."complex_items"."value" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_object($3::text, to_jsonb($4)), $5::text, jsonb_build_object($6::text, to_jsonb($7))))::pdb.query');
|
|
89
|
+
expect(params).toEqual(['range', 'lower_bound', 'included', 10, 'upper_bound', 'excluded', 20]);
|
|
88
90
|
});
|
|
89
91
|
test('should handle ParadeDB $parade phrasePrefix', () => {
|
|
90
92
|
const q = {
|
|
@@ -95,8 +97,9 @@ describe('ORM Query Converter Complex', () => {
|
|
|
95
97
|
},
|
|
96
98
|
};
|
|
97
99
|
const condition = convertQuery(q, table, colMap);
|
|
98
|
-
const
|
|
99
|
-
expect(
|
|
100
|
+
const { sql, params } = dialect.sqlToQuery(condition);
|
|
101
|
+
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_array(to_jsonb($3), to_jsonb($4)), $5::text, to_jsonb($6)))::pdb.query');
|
|
102
|
+
expect(params).toEqual(['phrase_prefix', 'phrases', 'hello', 'wor', 'max_expansions', 10]);
|
|
100
103
|
});
|
|
101
104
|
test('should handle ParadeDB $parade regexPhrase', () => {
|
|
102
105
|
const q = {
|
|
@@ -107,8 +110,9 @@ describe('ORM Query Converter Complex', () => {
|
|
|
107
110
|
},
|
|
108
111
|
};
|
|
109
112
|
const condition = convertQuery(q, table, colMap);
|
|
110
|
-
const
|
|
111
|
-
expect(
|
|
113
|
+
const { sql, params } = dialect.sqlToQuery(condition);
|
|
114
|
+
expect(sql).toContain('"test"."complex_items"."name" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, jsonb_build_array(to_jsonb($3), to_jsonb($4)), $5::text, to_jsonb($6)))::pdb.query');
|
|
115
|
+
expect(params).toEqual(['regex_phrase', 'regexes', 'he.*', 'wo.*', 'slop', 1]);
|
|
112
116
|
});
|
|
113
117
|
test('should handle ParadeDB top-level moreLikeThis', () => {
|
|
114
118
|
const q = {
|
|
@@ -120,7 +124,8 @@ describe('ORM Query Converter Complex', () => {
|
|
|
120
124
|
},
|
|
121
125
|
};
|
|
122
126
|
const condition = convertQuery(q, table, colMap);
|
|
123
|
-
const
|
|
124
|
-
expect(
|
|
127
|
+
const { sql, params } = dialect.sqlToQuery(condition);
|
|
128
|
+
expect(sql).toContain('"test"."complex_items"."id" @@@ jsonb_build_object($1::text, jsonb_build_object($2::text, to_jsonb($3), $4::text, to_jsonb(ARRAY[$5, $6])))::pdb.query');
|
|
129
|
+
expect(params).toEqual(['more_like_this', 'key_value', '123', 'fields', 'name', 'description']);
|
|
125
130
|
});
|
|
126
131
|
});
|
|
@@ -42,7 +42,7 @@ describe('ORM SQL Helpers', () => {
|
|
|
42
42
|
test('interval should generate postgres interval syntax', () => {
|
|
43
43
|
const i = interval(5, 'days');
|
|
44
44
|
const sqlStr = dialect.sqlToQuery(i).sql;
|
|
45
|
-
expect(sqlStr).toContain("||' days')::interval");
|
|
45
|
+
expect(sqlStr).toContain("|| ' days')::interval");
|
|
46
46
|
});
|
|
47
47
|
test('coalesce should join multiple columns', () => {
|
|
48
48
|
const c = coalesce(testTable.colA, testTable.colB, sql `'default'`);
|
package/package.json
CHANGED
package/test4.js
CHANGED
|
@@ -23,7 +23,7 @@ const config = {
|
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
s3: {
|
|
26
|
-
endpoint: string('S3_ENDPOINT', 'http://localhost:
|
|
26
|
+
endpoint: string('S3_ENDPOINT', 'http://localhost:19552'),
|
|
27
27
|
accessKey: string('S3_ACCESS_KEY', 'tstdl-dev'),
|
|
28
28
|
secretKey: string('S3_SECRET_KEY', 'tstdl-dev'),
|
|
29
29
|
bucket: string('S3_BUCKET', undefined),
|
|
@@ -125,7 +125,7 @@ export async function setupIntegrationTest(options = {}) {
|
|
|
125
125
|
if (options.modules?.objectStorage) {
|
|
126
126
|
const bucketPerModule = options.s3?.bucketPerModule ?? configParser.boolean('S3_BUCKET_PER_MODULE', true);
|
|
127
127
|
configureS3ObjectStorage({
|
|
128
|
-
endpoint: options.s3?.endpoint ?? configParser.string('S3_ENDPOINT', 'http://127.0.0.1:
|
|
128
|
+
endpoint: options.s3?.endpoint ?? configParser.string('S3_ENDPOINT', 'http://127.0.0.1:19552'),
|
|
129
129
|
accessKey: options.s3?.accessKey ?? configParser.string('S3_ACCESS_KEY', 'tstdl-dev'),
|
|
130
130
|
secretKey: options.s3?.secretKey ?? configParser.string('S3_SECRET_KEY', 'tstdl-dev'),
|
|
131
131
|
bucket: bucketPerModule ? undefined : (options.s3?.bucket ?? configParser.string('S3_BUCKET', 'test-bucket')),
|