@lakeql/query-builder 0.2.0 → 1.0.1
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/README.md +1 -1
- package/dist/index.d.mts +70 -51
- package/dist/index.mjs +5 -1
- package/package.json +16 -12
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lakeql/query-builder
|
|
2
2
|
|
|
3
|
-
SQL query builder
|
|
3
|
+
SQL query builder using Kysely, supporting dynamic filters (AND/OR), sorting, pagination, and field transformations across adapters.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Special thanks goes to https://github.com/igalklebanov
|
|
3
|
+
* for solving our edge case issue with multiple schematas <3
|
|
4
|
+
* https://www.answeroverflow.com/m/1227929774144618598
|
|
5
|
+
* and also with helping to solve the type issues <3
|
|
6
|
+
* https://www.answeroverflow.com/m/1228457827572645908
|
|
7
|
+
*/
|
|
1
8
|
import { GraphQLResolveInfo } from "graphql";
|
|
2
|
-
import {
|
|
3
|
-
|
|
9
|
+
import { DatabaseIntrospector, DialectAdapter, ExpressionBuilder, ExpressionWrapper, Kysely, QueryCompiler, RawBuilder, SelectExpression, SqlBool } from "kysely";
|
|
10
|
+
import { AdapterName, SourceConfig } from "@lakeql/datasource";
|
|
11
|
+
/**
|
|
12
|
+
* Engine-specific SQL generation strategy for Kysely.
|
|
13
|
+
*
|
|
14
|
+
* Each adapter provides a dialect that handles identifier quoting,
|
|
15
|
+
* pagination syntax, date functions, and sql-formatter language selection.
|
|
16
|
+
* The dialect is injected into the query builder — the query builder
|
|
17
|
+
* itself contains no adapter-specific SQL logic.
|
|
18
|
+
*/
|
|
19
|
+
interface QueryDialect {
|
|
20
|
+
/** Open adapter name identifying the backend this dialect targets. */
|
|
21
|
+
readonly adapterName: AdapterName;
|
|
22
|
+
/** Creates the Kysely dialect adapter (e.g. PostgresAdapter, MssqlAdapter). */
|
|
23
|
+
createAdapter: () => DialectAdapter;
|
|
24
|
+
/** Creates the Kysely query compiler for the target backend. */
|
|
25
|
+
createQueryCompiler: () => QueryCompiler;
|
|
26
|
+
/** Creates the Kysely database introspector for the target backend. */
|
|
27
|
+
createIntrospector: (db: Kysely<unknown>) => DatabaseIntrospector;
|
|
28
|
+
/** Wraps a column name in the backend-specific timestamp-to-unix conversion function. */
|
|
29
|
+
toUnixTimestamp: (field: string) => RawBuilder<unknown>;
|
|
30
|
+
/** Returns a raw SQL fragment for limit/offset pagination. */
|
|
31
|
+
pagination: (limit: number, offset?: number) => RawBuilder<unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Backend-specific boolean "true" literal used for the record-present
|
|
34
|
+
* sentinel column in the `records` CTE. Trino uses `TRUE`; T-SQL backends
|
|
35
|
+
* (e.g. Fabric) have no boolean literal and use `1`.
|
|
36
|
+
*/
|
|
37
|
+
trueLiteral: () => RawBuilder<unknown>;
|
|
38
|
+
/** Returns the fully-qualified table name with backend-specific quoting. */
|
|
39
|
+
qualifiedTable: (config: SourceConfig) => string;
|
|
40
|
+
}
|
|
4
41
|
/**
|
|
5
42
|
* Logical operators for combining filter conditions.
|
|
6
43
|
*/
|
|
7
|
-
declare enum WhereOperator {
|
|
44
|
+
export declare enum WhereOperator {
|
|
8
45
|
AND = "and",
|
|
9
46
|
OR = "or"
|
|
10
47
|
}
|
|
@@ -15,7 +52,7 @@ declare enum WhereOperator {
|
|
|
15
52
|
* String operators compare the field value against the provided string.
|
|
16
53
|
* Boolean operators (`is`/`isNot`) check for equality with `true`/`false`.
|
|
17
54
|
*/
|
|
18
|
-
interface FieldOptions {
|
|
55
|
+
export interface FieldOptions {
|
|
19
56
|
eq?: string;
|
|
20
57
|
neq?: string;
|
|
21
58
|
in?: string;
|
|
@@ -32,9 +69,9 @@ interface FieldOptions {
|
|
|
32
69
|
/**
|
|
33
70
|
* Defines the sort order for a query result.
|
|
34
71
|
*
|
|
35
|
-
* @template TableDefinition - The table type used to constrain the `field` to valid column names. See [
|
|
72
|
+
* @template TableDefinition - The table type used to constrain the `field` to valid column names. See [query-builder](/docs/packages/query-builder) for details.
|
|
36
73
|
*/
|
|
37
|
-
interface SortInput<TableDefinition> {
|
|
74
|
+
export interface SortInput<TableDefinition> {
|
|
38
75
|
/**
|
|
39
76
|
* Column name to sort by. Constrained to valid column names from the table type.
|
|
40
77
|
*
|
|
@@ -47,7 +84,7 @@ interface SortInput<TableDefinition> {
|
|
|
47
84
|
/**
|
|
48
85
|
* Controls offset-based pagination.
|
|
49
86
|
*/
|
|
50
|
-
interface PagingInput {
|
|
87
|
+
export interface PagingInput {
|
|
51
88
|
/**
|
|
52
89
|
* Maximum number of rows to return.
|
|
53
90
|
* @default 100
|
|
@@ -60,12 +97,12 @@ interface PagingInput {
|
|
|
60
97
|
* A single field condition — a record where the key is the field name
|
|
61
98
|
* and the value is a [FieldOptions](#field-options) object describing the comparison.
|
|
62
99
|
*/
|
|
63
|
-
type Field = Record<string, FieldOptions>;
|
|
100
|
+
export type Field = Record<string, FieldOptions>;
|
|
64
101
|
/**
|
|
65
102
|
* A recursive filter structure that can contain AND/OR groups,
|
|
66
103
|
* each holding field conditions or further nested groups.
|
|
67
104
|
*/
|
|
68
|
-
type Where = Partial<Record<WhereOperator, (Where | Field)[]>>;
|
|
105
|
+
export type Where = Partial<Record<WhereOperator, (Where | Field)[]>>;
|
|
69
106
|
/**
|
|
70
107
|
* KyselyDatabase is our generic interface which simulates
|
|
71
108
|
* the normal kysely table definition
|
|
@@ -74,23 +111,26 @@ interface KyselyDatabase<TableDefinition> {
|
|
|
74
111
|
tablename: TableDefinition;
|
|
75
112
|
}
|
|
76
113
|
/**
|
|
77
|
-
* Parameters for generating
|
|
114
|
+
* Parameters for generating an adapter-aware SQL query from GraphQL resolve info.
|
|
78
115
|
*
|
|
79
|
-
* @template TableDefinition - The table type definition used by Kysely for type-safe column references. See [
|
|
116
|
+
* @template TableDefinition - The table type definition used by Kysely for type-safe column references. See [query-builder](/docs/packages/query-builder) for details.
|
|
80
117
|
*/
|
|
81
|
-
interface GenerateQueryProps<TableDefinition> {
|
|
82
|
-
/** The
|
|
118
|
+
export interface GenerateQueryProps<TableDefinition> {
|
|
119
|
+
/** The query dialect providing adapter-specific SQL generation. */
|
|
120
|
+
dialect: QueryDialect;
|
|
121
|
+
/** The catalog name (e.g. "hive"). */
|
|
83
122
|
catalog: string;
|
|
84
|
-
/** The
|
|
123
|
+
/** The schema name (e.g. "sales"). */
|
|
85
124
|
schema: string;
|
|
86
125
|
/** The table name to query. */
|
|
87
126
|
table: string;
|
|
88
127
|
/**
|
|
89
|
-
* The
|
|
128
|
+
* The column names to select, derived from GraphQL field selections.
|
|
90
129
|
*
|
|
91
|
-
*
|
|
130
|
+
* These are plain field-name strings (as returned by {@link getSelectFields});
|
|
131
|
+
* they are resolved against the table definition when the query is built.
|
|
92
132
|
*/
|
|
93
|
-
selectFields:
|
|
133
|
+
selectFields: readonly string[];
|
|
94
134
|
/** The user-provided filter query (WHERE clause). */
|
|
95
135
|
userQuery: Where;
|
|
96
136
|
/** Pagination parameters (limit and offset). */
|
|
@@ -99,15 +139,15 @@ interface GenerateQueryProps<TableDefinition> {
|
|
|
99
139
|
sorting: SortInput<TableDefinition>[];
|
|
100
140
|
/** Maps GraphQL field names to database column names. */
|
|
101
141
|
transformFields?: Record<string, string>;
|
|
102
|
-
/** Fields that should be wrapped in
|
|
142
|
+
/** Fields that should be wrapped in adapter-specific timestamp conversion. */
|
|
103
143
|
dateFields?: string[];
|
|
104
144
|
}
|
|
105
145
|
/**
|
|
106
146
|
* Parameters for building a single field comparison expression.
|
|
107
147
|
*
|
|
108
|
-
* @template TableDefinition - The table type definition. See [
|
|
148
|
+
* @template TableDefinition - The table type definition. See [query-builder](/docs/packages/query-builder) for details.
|
|
109
149
|
*/
|
|
110
|
-
interface GetFieldQueryProps<TableDefinition> {
|
|
150
|
+
export interface GetFieldQueryProps<TableDefinition> {
|
|
111
151
|
/**
|
|
112
152
|
* The Kysely expression builder for constructing SQL expressions.
|
|
113
153
|
*
|
|
@@ -128,7 +168,7 @@ interface GetFieldQueryProps<TableDefinition> {
|
|
|
128
168
|
* @param props - The field query parameters.
|
|
129
169
|
* @returns A Kysely [ExpressionWrapper](https://kysely-org.github.io/kysely-apidoc/classes/ExpressionWrapper.html) representing the comparison.
|
|
130
170
|
*/
|
|
131
|
-
declare function getFieldQuery<TableDefinition>({ eb, fieldName, operator, value }: GetFieldQueryProps<TableDefinition>): ExpressionWrapper<KyselyDatabase<TableDefinition>, "tablename", SqlBool>;
|
|
171
|
+
export declare function getFieldQuery<TableDefinition>({ eb, fieldName, operator, value }: GetFieldQueryProps<TableDefinition>): ExpressionWrapper<KyselyDatabase<TableDefinition>, "tablename", SqlBool>;
|
|
132
172
|
/**
|
|
133
173
|
* Normalizes filter objects by ensuring each field is in its own object within and/or arrays.
|
|
134
174
|
*
|
|
@@ -138,7 +178,7 @@ declare function getFieldQuery<TableDefinition>({ eb, fieldName, operator, value
|
|
|
138
178
|
* @param filter - The filter object to normalize.
|
|
139
179
|
* @returns A normalized filter with individual field entries.
|
|
140
180
|
*/
|
|
141
|
-
declare function normalizeFilter(filter: Where): Where;
|
|
181
|
+
export declare function normalizeFilter(filter: Where): Where;
|
|
142
182
|
/**
|
|
143
183
|
* Wraps bare field-level filters into an AND-rooted structure.
|
|
144
184
|
*
|
|
@@ -148,42 +188,22 @@ declare function normalizeFilter(filter: Where): Where;
|
|
|
148
188
|
* @param userQuery - The user-provided filter query.
|
|
149
189
|
* @returns A normalized query with a logical operator at the root.
|
|
150
190
|
*/
|
|
151
|
-
declare function normalizeUserQuery(userQuery: Where): Where;
|
|
191
|
+
export declare function normalizeUserQuery(userQuery: Where): Where;
|
|
152
192
|
/**
|
|
153
|
-
* Generates a compiled SQL query with pagination metadata
|
|
193
|
+
* Generates a compiled SQL query with pagination metadata using the injected dialect.
|
|
154
194
|
*
|
|
155
195
|
* The query uses two CTEs internally:
|
|
156
196
|
* - `total_count` — provides `total_records` (total matching rows before paging)
|
|
157
197
|
* - `records` — the actual rows based on `selectFields`, filtering, sorting, and paging
|
|
158
198
|
*
|
|
159
|
-
* The compiled query is executed by the
|
|
199
|
+
* The compiled query is executed by the DataSource adapter which returns the row data.
|
|
160
200
|
*
|
|
161
|
-
* @returns A [CompiledQuery](https://kysely-org.github.io/kysely-apidoc/interfaces/CompiledQuery.html) ready for execution via the
|
|
201
|
+
* @returns A [CompiledQuery](https://kysely-org.github.io/kysely-apidoc/interfaces/CompiledQuery.html) ready for execution via the DataSource adapter.
|
|
162
202
|
*/
|
|
163
|
-
declare function generateQuery<TableDefinition>({ catalog, schema, table, selectFields, userQuery, sorting, paging, transformFields, dateFields }: GenerateQueryProps<TableDefinition>): CompiledQuery<{
|
|
203
|
+
export declare function generateQuery<TableDefinition>({ dialect, catalog, schema, table, selectFields, userQuery, sorting, paging, transformFields, dateFields }: GenerateQueryProps<TableDefinition>): import("kysely").CompiledQuery<{
|
|
164
204
|
total_records: number;
|
|
165
205
|
}>;
|
|
166
|
-
|
|
167
|
-
* Parameters for formatting a compiled SQL query.
|
|
168
|
-
*/
|
|
169
|
-
interface FormatQueryProps<T> {
|
|
170
|
-
/**
|
|
171
|
-
* The compiled Kysely query to format.
|
|
172
|
-
*
|
|
173
|
-
* @see {@link https://kysely-org.github.io/kysely-apidoc/interfaces/CompiledQuery.html | Kysely API Docs - CompiledQuery}
|
|
174
|
-
*/
|
|
175
|
-
query: CompiledQuery<T>;
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Formats a compiled SQL query for readability.
|
|
179
|
-
*
|
|
180
|
-
* Applies uppercase keywords, PostgreSQL dialect, and interpolates parameters.
|
|
181
|
-
*
|
|
182
|
-
* @param props - The format query parameters.
|
|
183
|
-
* @returns The formatted SQL string with interpolated parameters.
|
|
184
|
-
*/
|
|
185
|
-
declare function formatQuery<T>({ query }: FormatQueryProps<T>): string;
|
|
186
|
-
interface GetSelectFieldProps {
|
|
206
|
+
export interface GetSelectFieldProps {
|
|
187
207
|
/**
|
|
188
208
|
* The GraphQL resolve info from the resolver.
|
|
189
209
|
*
|
|
@@ -201,6 +221,5 @@ interface GetSelectFieldProps {
|
|
|
201
221
|
*
|
|
202
222
|
* @returns An array of field names to use as SELECT columns.
|
|
203
223
|
*/
|
|
204
|
-
declare function getSelectFields
|
|
205
|
-
|
|
206
|
-
export { Field, FieldOptions, FormatQueryProps, GenerateQueryProps, GetFieldQueryProps, GetSelectFieldProps, PagingInput, SortInput, Where, WhereOperator, formatQuery, generateQuery, getFieldQuery, getSelectFields, normalizeFilter, normalizeUserQuery };
|
|
224
|
+
export declare function getSelectFields({ graphqlInfo, withNodes }: GetSelectFieldProps): string[];
|
|
225
|
+
export type { QueryDialect };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
import{GraphQLError as e}from"graphql";import{DummyDriver as t,Kysely as n,
|
|
1
|
+
import{GraphQLError as e}from"graphql";import{DummyDriver as t,Kysely as n,sql as r}from"kysely";const i=e=>new n({dialect:{...e,createDriver:()=>new t}});let a=/* @__PURE__ */ function(e){return e.AND=`and`,e.OR=`or`,e}({});const o={eq:`=`,gt:`>`,gte:`>=`,in:`in`,is:`=`,isnot:`!=`,lt:`<`,lte:`<=`,neq:`!=`,notin:`not in`};function s({eb:e,fieldName:t,operator:n,value:i}){let a=t,s=n.toLowerCase();switch(s){case`eq`:case`neq`:case`lt`:case`lte`:case`gt`:case`gte`:case`is`:case`isnot`:return e(r.id(a),o[s]??`=`,i);case`in`:case`notin`:return e(r.id(a),o[s]??`=`,i);case`like`:return e(r.id(a),`like`,`%${i}%`);case`notlike`:return e.not(e(r.id(a),`like`,`%${i}%`));case`startswith`:return e(r.id(a),`like`,`${i}%`);case`notstartswith`:return e.not(e(r.id(a),`like`,`${i}%`));case`endswith`:return e(r.id(a),`like`,`%${i}`);case`notendswith`:return e.not(e(r.id(a),`like`,`%${i}`));default:return e(r.id(a),`=`,i)}}function c({eb:e,query:t,transformFields:n}){let r=[];for(let[i,a]of Object.entries(t))switch(i.toLowerCase()){case`and`:for(let t of a){let i=c({eb:e,query:t,transformFields:n});i!==void 0&&r.push(i)}return e.and(r);case`or`:for(let t of a){let i=c({eb:e,query:t,transformFields:n});i&&r.push(i)}return e.or(r);default:{let[t,r]=Object.entries(a).flat();return s({eb:e,fieldName:n?.[i]??i,operator:t,value:r})}}}function l(e){if(!e)return e;let t={...e};return t.and&&Array.isArray(t.and)&&(t.and=t.and.flatMap(e=>{let t=e,n=Object.keys(t),r=n.filter(e=>e===`and`||e===`or`),i=n.filter(e=>e!==`and`&&e!==`or`);
|
|
2
|
+
/* c8 ignore next 6 */
|
|
3
|
+
if(i.length>0&&r.length>0){let e=i.map(e=>({[e]:t[e]})),n=r.map(e=>({[e]:l({[e]:t[e]})[e]}));return[...e,...n]}if(i.length>1&&r.length===0)return i.map(e=>({[e]:t[e]}));if(r.length>0){let e={...t};for(let n of r)e[n]=l({[n]:t[n]})[n];return e}return e})),t.or&&Array.isArray(t.or)&&(t.or=t.or.flatMap(e=>{let t=e,n=Object.keys(t),r=n.filter(e=>e===`and`||e===`or`),i=n.filter(e=>e!==`and`&&e!==`or`);
|
|
4
|
+
/* c8 ignore next 6 */
|
|
5
|
+
if(i.length>0&&r.length>0){let e=i.map(e=>({[e]:t[e]})),n=r.map(e=>({[e]:l({[e]:t[e]})[e]}));return[...e,...n]}if(i.length>1&&r.length===0)return i.map(e=>({[e]:t[e]}));if(r.length>0){let e={...t};for(let n of r)e[n]=l({[n]:t[n]})[n];return e}return e})),t}function u(e){let t=Object.keys(e);return t.length===1&&(t[0]===`and`||t[0]===`or`)?e:{and:Object.entries(e).map(([e,t])=>({[e]:t}))}}function d({dialect:e,catalog:t,schema:n,table:a,selectFields:o,userQuery:s,sorting:d,paging:f,transformFields:p,dateFields:m=[]}){let h=i(e),g=e.qualifiedTable({dataSource:``,adapter:e.adapterName,catalog:t,schema:n,tableName:a}),{ref:_}=h.dynamic;return h.with(`total_count`,e=>e.selectFrom(r.raw(g)).select(({fn:e})=>e.countAll().as(`total_records`)).$if(Object.keys(s).length>0,e=>e.where(e=>c({eb:e,query:l(u(s)),transformFields:p})??e.and([])))).with(`records`,t=>{let n=t.selectFrom(r.raw(g)).select(o.map(t=>{if(m.includes(t)){if(p?.[t]){let n=p[t];return e.toUnixTimestamp(n).as(n)}return e.toUnixTimestamp(t).as(t)}return p?.[t]?r.id(p[t]).as(p[t]):typeof t==`string`?_(t):t})).select(e.trueLiteral().as(`__lakeql_record_present`)).$if(Object.keys(s).length>0,e=>e.where(e=>c({eb:e,query:l(u(s)),transformFields:p})??e.and([])));for(let e of d){let t=p?.[e.field]||e.field;n=n.orderBy(t,e.direction.toLowerCase())}let i=f?.limit??100,a=f?.offset;return n=!a||a<=0?n.modifyEnd(e.pagination(i)):n.offset(a).modifyEnd(e.pagination(i,a)),n}).selectFrom(`total_count`).selectAll().fullJoin(`records`,e=>e.on(r`1=1`)).compile()}function f({graphqlInfo:t,withNodes:n=!1}){return t.fieldNodes.flatMap(t=>{let r=t.selectionSet?.selections,i=[];if(n){let t=r.find(e=>e.name.value===`nodes`);if(!t)throw new e("No fields to return defined. Please specify at least one field inside `nodes`.");i=t.selectionSet?.selections??[]}else i=t.selectionSet?.selections??[];return i.flatMap(e=>e.name.value)})}export{a as WhereOperator,d as generateQuery,s as getFieldQuery,f as getSelectFields,l as normalizeFilter,u as normalizeUserQuery};
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakeql/query-builder",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kysely-based SQL query builder that translates GraphQL queries into
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Kysely-based SQL query builder that translates GraphQL queries into adapter SQL",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
7
7
|
"kysely",
|
|
8
8
|
"lakeql",
|
|
9
9
|
"query-builder",
|
|
10
10
|
"sql",
|
|
11
|
-
"
|
|
11
|
+
"typescript"
|
|
12
12
|
],
|
|
13
13
|
"homepage": "https://lakeql.dev",
|
|
14
14
|
"license": "Apache-2.0",
|
|
@@ -39,19 +39,22 @@
|
|
|
39
39
|
"import": "./dist/index.mjs"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
42
45
|
"dependencies": {
|
|
43
|
-
"
|
|
44
|
-
"kysely": "0.29.
|
|
45
|
-
"sql-formatter": "15.8.2"
|
|
46
|
+
"@lakeql/datasource": "1.0.1",
|
|
47
|
+
"kysely": "0.29.8"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
|
-
"@
|
|
49
|
-
"@
|
|
50
|
-
"
|
|
51
|
-
"
|
|
50
|
+
"@lakeql/tsconfig": "0.1.0",
|
|
51
|
+
"@lakeql/vitest-config": "0.1.0",
|
|
52
|
+
"@pothos/core": "4.16.0",
|
|
53
|
+
"@types/node": "24.19.2",
|
|
54
|
+
"graphql": "17.1.0",
|
|
55
|
+
"tsdown": "0.23.7",
|
|
52
56
|
"typescript": "6.0.3",
|
|
53
|
-
"vitest": "
|
|
54
|
-
"@lakeql/tsconfig": "0.1.0"
|
|
57
|
+
"vitest": "5.1.0"
|
|
55
58
|
},
|
|
56
59
|
"scripts": {
|
|
57
60
|
"build": "tsdown",
|
|
@@ -59,6 +62,7 @@
|
|
|
59
62
|
"clean:cache": "git clean -xdf .cache .turbo",
|
|
60
63
|
"clean:dist": "git clean -xdf dist",
|
|
61
64
|
"dev": "tsdown --watch",
|
|
65
|
+
"test": "vitest run",
|
|
62
66
|
"typecheck": "tsc --noEmit"
|
|
63
67
|
}
|
|
64
68
|
}
|