@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.
@@ -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?: (column: [Column, ...Column[]]) => SQL<unknown> | undefined): SQL;
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((value) => isInstanceOf(value, Column)));
75
+ .flatMap((value) => toArray(value).filter((item) => isInstanceOf(item, Column)));
75
76
  const participatingColumns = distinct(allColumns);
76
- const mapping = objectEntries(conditionMapping).map(([key, value]) => {
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
- return [key, SQL_FALSE];
81
+ kaseWhen.when(enumValue(enumeration, null, key), SQL_FALSE);
82
+ continue;
79
83
  }
80
- const requiredColumns = toArray(value).filter((value) => isInstanceOf(value, Column));
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
- const customConditions = toArray(value)
83
- .filter((val) => !isInstanceOf(val, Column) && (val !== true))
84
- .map((condition) => isBoolean(condition) ? (condition ? SQL_TRUE : SQL_FALSE) : condition);
85
- const condition = and(...requiredColumns.map((col) => sqlIsNotNull(col)), ...nullColumns.map((col) => sqlIsNull(col)), ...customConditions);
86
- return [key, condition];
87
- });
88
- const kaseWhen = caseWhen(discriminator);
89
- for (const [key, condition] of mapping) {
90
- kaseWhen.when(enumValue(enumeration, null, key), condition);
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 = (column) => isArray(column) ? and(...column.map((col) => sqlIsNotNull(col))) : sqlIsNotNull(column)) {
120
- const whens = [];
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
- .when((value) => isInstanceOf(value, Column), (col) => defaultColumnCondition(col))
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
- whens.push(sql ` WHEN ${enumValue(enumeration, null, key)} THEN ${condition}`);
147
+ kaseWhen.when(enumValue(enumeration, null, key), condition);
127
148
  }
128
- return sql `
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
- const isSimpleKey = simpleJsonKeyPattern.test(key);
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 `${JSON.stringify(value)}::jsonb`);
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 { StringProperty, Integer } from '../../schema/index.js';
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 { Column, Table } from '../decorators.js';
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 sqlStr = dialect.sqlToQuery(condition).sql;
53
- expect(sqlStr).toContain('setweight(to_tsvector($1, "test"."complex_items"."name"), \'A\')');
54
- expect(sqlStr).toContain('setweight(to_tsvector($2, "test"."complex_items"."description"), \'B\')');
55
- expect(sqlStr).toContain('websearch_to_tsquery($3, $4)');
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 sqlStr = dialect.sqlToQuery(condition).sql;
69
+ const { sql, params } = dialect.sqlToQuery(condition);
70
70
  // Tokenizer is supported via JSON object syntax
71
- expect(sqlStr).toContain('"test"."complex_items"."name" @@@ jsonb_build_object(\'match\', jsonb_build_object(\'value\', $1::jsonb, \'tokenizer\', jsonb_build_object(\'type\', $2::jsonb, \'min_gram\', $3::jsonb, \'max_gram\', $4::jsonb)))::pdb.query');
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 sqlStr = dialect.sqlToQuery(condition).sql;
86
+ const { sql, params } = dialect.sqlToQuery(condition);
86
87
  // This should fall back to convertParadeComparisonQuery with recursive jsonb build
87
- expect(sqlStr).toContain('"test"."complex_items"."value" @@@ jsonb_build_object(\'range\', jsonb_build_object(\'lower_bound\', jsonb_build_object(\'included\', $1::jsonb), \'upper_bound\', jsonb_build_object(\'excluded\', $2::jsonb)))::pdb.query');
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 sqlStr = dialect.sqlToQuery(condition).sql;
99
- expect(sqlStr).toContain('"test"."complex_items"."name" @@@ jsonb_build_object(\'phrase_prefix\', jsonb_build_object(\'phrases\', jsonb_build_array($1::jsonb, $2::jsonb), \'max_expansions\', $3::jsonb))::pdb.query');
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 sqlStr = dialect.sqlToQuery(condition).sql;
111
- expect(sqlStr).toContain('"test"."complex_items"."name" @@@ jsonb_build_object(\'regex_phrase\', jsonb_build_object(\'regexes\', jsonb_build_array($1::jsonb, $2::jsonb), \'slop\', $3::jsonb))::pdb.query');
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 sqlStr = dialect.sqlToQuery(condition).sql;
124
- expect(sqlStr).toContain('"test"."complex_items"."id" @@@ jsonb_build_object(\'more_like_this\', jsonb_build_object(\'key_value\', $1::jsonb, \'fields\', to_jsonb(ARRAY[$2, $3])))::pdb.query');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.155",
3
+ "version": "0.93.156",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/test4.js CHANGED
@@ -23,7 +23,7 @@ const config = {
23
23
  },
24
24
  },
25
25
  s3: {
26
- endpoint: string('S3_ENDPOINT', 'http://localhost:9000'),
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:9000'),
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')),