@shotleybuilder/svelte-gridlite-kit 0.3.1 → 0.4.0
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/GridLite.svelte +62 -61
- package/dist/components/FilterBar.svelte +9 -6
- package/dist/components/FilterBar.svelte.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/query/builder.d.ts +18 -6
- package/dist/query/builder.js +30 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ Focused, single-feature demo pages in `src/routes/examples/`:
|
|
|
110
110
|
| `/examples/filtering` | FilterBar + programmatic filter buttons |
|
|
111
111
|
| `/examples/grouping` | Hierarchical grouping with aggregations |
|
|
112
112
|
| `/examples/custom-cells` | Currency, date, boolean, rating star formatters |
|
|
113
|
-
| `/examples/raw-query` | JOIN, aggregate, CTE queries via
|
|
113
|
+
| `/examples/raw-query` | JOIN, aggregate, CTE queries via `query` prop with full toolbar |
|
|
114
114
|
|
|
115
115
|
Run `npm run dev` and visit `http://localhost:5173/examples/minimal` to start.
|
|
116
116
|
|
package/dist/GridLite.svelte
CHANGED
|
@@ -164,33 +164,31 @@ async function rebuildQuery() {
|
|
|
164
164
|
}
|
|
165
165
|
let sql;
|
|
166
166
|
let params = [];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
} else if (table) {
|
|
170
|
-
if (isGrouped) {
|
|
171
|
-
await rebuildGroupedQuery();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const usePagination = features.pagination !== false;
|
|
175
|
-
const built = buildQuery({
|
|
176
|
-
table,
|
|
177
|
-
filters,
|
|
178
|
-
filterLogic,
|
|
179
|
-
sorting,
|
|
180
|
-
page: usePagination ? page : void 0,
|
|
181
|
-
pageSize: usePagination ? pageSize : void 0,
|
|
182
|
-
allowedColumns,
|
|
183
|
-
globalSearch: globalFilter || void 0
|
|
184
|
-
});
|
|
185
|
-
sql = built.sql;
|
|
186
|
-
params = built.params;
|
|
187
|
-
if (usePagination) {
|
|
188
|
-
await updateTotalCount();
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
167
|
+
const querySource = query ? { source: query } : table ? { table } : null;
|
|
168
|
+
if (!querySource) {
|
|
191
169
|
error = "Either `table` or `query` prop is required";
|
|
192
170
|
return;
|
|
193
171
|
}
|
|
172
|
+
if (isGrouped) {
|
|
173
|
+
await rebuildGroupedQuery();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const usePagination = features.pagination !== false;
|
|
177
|
+
const built = buildQuery({
|
|
178
|
+
...querySource,
|
|
179
|
+
filters,
|
|
180
|
+
filterLogic,
|
|
181
|
+
sorting,
|
|
182
|
+
page: usePagination ? page : void 0,
|
|
183
|
+
pageSize: usePagination ? pageSize : void 0,
|
|
184
|
+
allowedColumns,
|
|
185
|
+
globalSearch: globalFilter || void 0
|
|
186
|
+
});
|
|
187
|
+
sql = built.sql;
|
|
188
|
+
params = built.params;
|
|
189
|
+
if (usePagination) {
|
|
190
|
+
await updateTotalCount();
|
|
191
|
+
}
|
|
194
192
|
groupData = [];
|
|
195
193
|
expandedGroups = /* @__PURE__ */ new Set();
|
|
196
194
|
totalGroups = 0;
|
|
@@ -216,12 +214,13 @@ function cleanAgg(g) {
|
|
|
216
214
|
};
|
|
217
215
|
}
|
|
218
216
|
async function rebuildGroupedQuery() {
|
|
219
|
-
|
|
217
|
+
const querySource = query ? { source: query } : table ? { table } : null;
|
|
218
|
+
if (!querySource) return;
|
|
220
219
|
try {
|
|
221
220
|
const usePagination = features.pagination !== false;
|
|
222
221
|
const topGroupConfig = cleanAgg(validGrouping[0]);
|
|
223
222
|
const summaryQuery = buildGroupSummaryQuery({
|
|
224
|
-
|
|
223
|
+
...querySource,
|
|
225
224
|
grouping: [topGroupConfig],
|
|
226
225
|
filters,
|
|
227
226
|
filterLogic,
|
|
@@ -237,7 +236,7 @@ async function rebuildGroupedQuery() {
|
|
|
237
236
|
);
|
|
238
237
|
if (usePagination) {
|
|
239
238
|
const countQuery = buildGroupCountQuery({
|
|
240
|
-
|
|
239
|
+
...querySource,
|
|
241
240
|
grouping: [topGroupConfig],
|
|
242
241
|
filters,
|
|
243
242
|
filterLogic,
|
|
@@ -283,7 +282,8 @@ async function rebuildGroupedQuery() {
|
|
|
283
282
|
}
|
|
284
283
|
}
|
|
285
284
|
async function fetchGroupChildren(group) {
|
|
286
|
-
|
|
285
|
+
const querySource = query ? { source: query } : table ? { table } : null;
|
|
286
|
+
if (!querySource) return;
|
|
287
287
|
const key = groupKey(group);
|
|
288
288
|
groupLoading = /* @__PURE__ */ new Set([...groupLoading, key]);
|
|
289
289
|
try {
|
|
@@ -295,7 +295,7 @@ async function fetchGroupChildren(group) {
|
|
|
295
295
|
if (nextDepth < validGrouping.length) {
|
|
296
296
|
const subGroupConfig = cleanAgg(validGrouping[nextDepth]);
|
|
297
297
|
const summaryQuery = buildGroupSummaryQuery({
|
|
298
|
-
|
|
298
|
+
...querySource,
|
|
299
299
|
grouping: [subGroupConfig],
|
|
300
300
|
filters: [
|
|
301
301
|
...filters,
|
|
@@ -334,7 +334,7 @@ async function fetchGroupChildren(group) {
|
|
|
334
334
|
updateGroupInTree(key, { subGroups });
|
|
335
335
|
} else {
|
|
336
336
|
const detailQuery = buildGroupDetailQuery({
|
|
337
|
-
|
|
337
|
+
...querySource,
|
|
338
338
|
groupValues: parentValues,
|
|
339
339
|
filters,
|
|
340
340
|
filterLogic,
|
|
@@ -372,10 +372,11 @@ function updateGroupNode(node, targetKey, updates) {
|
|
|
372
372
|
return node;
|
|
373
373
|
}
|
|
374
374
|
async function updateTotalCount() {
|
|
375
|
-
|
|
375
|
+
const querySource = query ? { source: query } : table ? { table } : null;
|
|
376
|
+
if (!querySource) return;
|
|
376
377
|
try {
|
|
377
378
|
const countQuery = buildCountQuery({
|
|
378
|
-
|
|
379
|
+
...querySource,
|
|
379
380
|
filters,
|
|
380
381
|
filterLogic,
|
|
381
382
|
allowedColumns,
|
|
@@ -804,7 +805,7 @@ onDestroy(() => {
|
|
|
804
805
|
{:else if storeState.error}
|
|
805
806
|
<div class="gridlite-empty">Error: {storeState.error.message}</div>
|
|
806
807
|
{:else}
|
|
807
|
-
{#if
|
|
808
|
+
{#if toolbarLayout !== 'aggrid'}
|
|
808
809
|
<div class="gridlite-toolbar">
|
|
809
810
|
<!-- Custom toolbar content (start) -->
|
|
810
811
|
<slot name="toolbar-start" />
|
|
@@ -847,7 +848,8 @@ onDestroy(() => {
|
|
|
847
848
|
<div class="gridlite-toolbar-filter">
|
|
848
849
|
<FilterBar
|
|
849
850
|
{db}
|
|
850
|
-
{table}
|
|
851
|
+
table={table ?? ''}
|
|
852
|
+
source={query ?? ''}
|
|
851
853
|
{columns}
|
|
852
854
|
columnConfigs={mergedColumnConfigs}
|
|
853
855
|
{allowedColumns}
|
|
@@ -993,7 +995,7 @@ onDestroy(() => {
|
|
|
993
995
|
</div>
|
|
994
996
|
{/if}
|
|
995
997
|
|
|
996
|
-
{#if
|
|
998
|
+
{#if toolbarLayout === 'aggrid'}
|
|
997
999
|
<!-- AG Grid layout: sidebar on right, minimal toolbar on top -->
|
|
998
1000
|
<!-- TODO(#1): aggrid layout is experimental — not production-ready. Needs debugging. -->
|
|
999
1001
|
<div class="gridlite-toolbar gridlite-toolbar-aggrid-top">
|
|
@@ -1159,32 +1161,30 @@ onDestroy(() => {
|
|
|
1159
1161
|
{getColumnLabel(col)}
|
|
1160
1162
|
</span>
|
|
1161
1163
|
{/if}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
>
|
|
1170
|
-
<
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
/>
|
|
1187
|
-
{/if}
|
|
1164
|
+
<button
|
|
1165
|
+
class="gridlite-th-menu-btn"
|
|
1166
|
+
on:click|stopPropagation={() =>
|
|
1167
|
+
(columnMenuOpen = columnMenuOpen === col.name ? null : col.name)}
|
|
1168
|
+
title="Column options"
|
|
1169
|
+
type="button"
|
|
1170
|
+
>
|
|
1171
|
+
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1172
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
1173
|
+
</svg>
|
|
1174
|
+
</button>
|
|
1175
|
+
<ColumnMenu
|
|
1176
|
+
columnName={col.name}
|
|
1177
|
+
isOpen={columnMenuOpen === col.name}
|
|
1178
|
+
{sorting}
|
|
1179
|
+
canSort={features.sorting ?? false}
|
|
1180
|
+
canFilter={features.filtering ?? false}
|
|
1181
|
+
canGroup={features.grouping ?? false}
|
|
1182
|
+
onSort={handleColumnMenuSort}
|
|
1183
|
+
onFilter={handleColumnMenuFilter}
|
|
1184
|
+
onGroup={handleColumnMenuGroup}
|
|
1185
|
+
onHide={handleColumnMenuHide}
|
|
1186
|
+
onClose={() => (columnMenuOpen = null)}
|
|
1187
|
+
/>
|
|
1188
1188
|
</div>
|
|
1189
1189
|
{#if features.columnResizing}
|
|
1190
1190
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
@@ -1401,6 +1401,7 @@ onDestroy(() => {
|
|
|
1401
1401
|
<FilterBar
|
|
1402
1402
|
{db}
|
|
1403
1403
|
table={table ?? ''}
|
|
1404
|
+
source={query ?? ''}
|
|
1404
1405
|
{columns}
|
|
1405
1406
|
columnConfigs={mergedColumnConfigs}
|
|
1406
1407
|
{allowedColumns}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
<script>import { quoteIdentifier } from "../query/builder.js";
|
|
1
|
+
<script>import { quoteIdentifier, resolveFrom } from "../query/builder.js";
|
|
2
2
|
import FilterConditionComponent from "./FilterCondition.svelte";
|
|
3
3
|
export let db;
|
|
4
|
-
export let table;
|
|
4
|
+
export let table = "";
|
|
5
|
+
export let source = "";
|
|
5
6
|
export let columns;
|
|
6
7
|
export let columnConfigs = [];
|
|
7
8
|
export let allowedColumns = [];
|
|
@@ -14,13 +15,14 @@ export let onExpandedChange = void 0;
|
|
|
14
15
|
let columnValuesCache = /* @__PURE__ */ new Map();
|
|
15
16
|
let numericRangeCache = /* @__PURE__ */ new Map();
|
|
16
17
|
async function getColumnValues(columnName) {
|
|
17
|
-
if (!columnName || !table) return [];
|
|
18
|
+
if (!columnName || !table && !source) return [];
|
|
18
19
|
if (columnValuesCache.has(columnName)) {
|
|
19
20
|
return columnValuesCache.get(columnName);
|
|
20
21
|
}
|
|
21
22
|
try {
|
|
23
|
+
const fromClause = resolveFrom(table || void 0, source || void 0);
|
|
22
24
|
const quotedCol = quoteIdentifier(columnName, allowedColumns);
|
|
23
|
-
const sql = `SELECT DISTINCT ${quotedCol}::TEXT AS val FROM ${
|
|
25
|
+
const sql = `SELECT DISTINCT ${quotedCol}::TEXT AS val FROM ${fromClause} WHERE ${quotedCol} IS NOT NULL ORDER BY val LIMIT 200`;
|
|
24
26
|
const result = await db.query(sql);
|
|
25
27
|
const values = result.rows.map((r) => r.val);
|
|
26
28
|
columnValuesCache.set(columnName, values);
|
|
@@ -31,7 +33,7 @@ async function getColumnValues(columnName) {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
async function getNumericRange(columnName) {
|
|
34
|
-
if (!columnName || !table) return null;
|
|
36
|
+
if (!columnName || !table && !source) return null;
|
|
35
37
|
if (numericRangeCache.has(columnName)) {
|
|
36
38
|
return numericRangeCache.get(columnName);
|
|
37
39
|
}
|
|
@@ -43,8 +45,9 @@ async function getNumericRange(columnName) {
|
|
|
43
45
|
return null;
|
|
44
46
|
}
|
|
45
47
|
try {
|
|
48
|
+
const fromClause = resolveFrom(table || void 0, source || void 0);
|
|
46
49
|
const quotedCol = quoteIdentifier(columnName, allowedColumns);
|
|
47
|
-
const sql = `SELECT MIN(${quotedCol})::NUMERIC AS min_val, MAX(${quotedCol})::NUMERIC AS max_val FROM ${
|
|
50
|
+
const sql = `SELECT MIN(${quotedCol})::NUMERIC AS min_val, MAX(${quotedCol})::NUMERIC AS max_val FROM ${fromClause}`;
|
|
48
51
|
const result = await db.query(sql);
|
|
49
52
|
const row = result.rows[0];
|
|
50
53
|
if (row && row.min_val != null && row.max_val != null) {
|
|
@@ -10,7 +10,8 @@ import type { PGliteWithLive } from '../query/live.js';
|
|
|
10
10
|
declare const __propDef: {
|
|
11
11
|
props: {
|
|
12
12
|
/** PGLite instance for running suggestion queries */ db: PGliteWithLive;
|
|
13
|
-
/** Table name for suggestion queries */ table
|
|
13
|
+
/** Table name for suggestion queries (mutually exclusive with `source`) */ table?: string;
|
|
14
|
+
/** Raw SQL subquery source for suggestion queries (mutually exclusive with `table`) */ source?: string;
|
|
14
15
|
/** Introspected column metadata */ columns: ColumnMetadata[];
|
|
15
16
|
/** Column config overrides (labels, dataType, selectOptions) */ columnConfigs?: ColumnConfig[];
|
|
16
17
|
/** Allowed column names for query safety */ allowedColumns?: string[];
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { default as ColumnMenu } from "./components/ColumnMenu.svelte";
|
|
|
9
9
|
export { default as ColumnPicker } from "./components/ColumnPicker.svelte";
|
|
10
10
|
export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
|
|
11
11
|
export type { GridLiteProps, GridConfig, GridFeatures, GridState, ColumnConfig, ColumnDataType, ColumnMetadata, FilterCondition, FilterOperator, FilterLogic, SortConfig, GroupConfig, AggregationConfig, AggregateFunction, ViewPreset, ClassNameMap, RowHeight, ColumnSpacing, ParameterizedQuery, ToolbarLayout, } from "./types.js";
|
|
12
|
-
export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
|
|
12
|
+
export { quoteIdentifier, resolveFrom, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
|
|
13
13
|
export type { QueryOptions, GroupSummaryOptions, GroupDetailOptions, } from "./query/builder.js";
|
|
14
14
|
export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
|
|
15
15
|
export { createLiveQueryStore, createLiveQueryStoreFromQuery, } from "./query/live.js";
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { default as ColumnMenu } from "./components/ColumnMenu.svelte";
|
|
|
12
12
|
export { default as ColumnPicker } from "./components/ColumnPicker.svelte";
|
|
13
13
|
export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
|
|
14
14
|
// Query builder
|
|
15
|
-
export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
|
|
15
|
+
export { quoteIdentifier, resolveFrom, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
|
|
16
16
|
// Schema introspection
|
|
17
17
|
export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
|
|
18
18
|
// Live query store
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* to prevent SQL injection via identifier manipulation.
|
|
10
10
|
*/
|
|
11
11
|
import type { FilterCondition, FilterLogic, SortConfig, GroupConfig, ParameterizedQuery } from "../types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a FROM clause from either a table name or a raw SQL source.
|
|
14
|
+
* When `source` is provided, it's used as a subquery: `(source) AS _gridlite_sub`.
|
|
15
|
+
* When `table` is provided, it's quoted as an identifier: `"table"`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveFrom(table?: string, source?: string): string;
|
|
12
18
|
/**
|
|
13
19
|
* Validate and quote a column name as a SQL identifier.
|
|
14
20
|
* Throws if the name is not a valid identifier.
|
|
@@ -59,8 +65,10 @@ export declare function buildPaginationClause(page: number, pageSize: number): s
|
|
|
59
65
|
*/
|
|
60
66
|
export declare function buildGlobalSearchClause(searchTerm: string, textColumns: string[], paramOffset?: number, allowedColumns?: string[]): ParameterizedQuery;
|
|
61
67
|
export interface GroupSummaryOptions {
|
|
62
|
-
/** Table name */
|
|
63
|
-
table
|
|
68
|
+
/** Table name (mutually exclusive with `source`) */
|
|
69
|
+
table?: string;
|
|
70
|
+
/** Raw SQL subquery source (mutually exclusive with `table`) */
|
|
71
|
+
source?: string;
|
|
64
72
|
/** Group columns (max 3) */
|
|
65
73
|
grouping: GroupConfig[];
|
|
66
74
|
/** Filter conditions to apply before grouping */
|
|
@@ -94,8 +102,10 @@ export declare function buildGroupSummaryQuery(options: GroupSummaryOptions): Pa
|
|
|
94
102
|
*/
|
|
95
103
|
export declare function buildGroupCountQuery(options: GroupSummaryOptions): ParameterizedQuery;
|
|
96
104
|
export interface GroupDetailOptions {
|
|
97
|
-
/** Table name */
|
|
98
|
-
table
|
|
105
|
+
/** Table name (mutually exclusive with `source`) */
|
|
106
|
+
table?: string;
|
|
107
|
+
/** Raw SQL subquery source (mutually exclusive with `table`) */
|
|
108
|
+
source?: string;
|
|
99
109
|
/** Values to match for parent groups: [{ column: "department", value: "Engineering" }] */
|
|
100
110
|
groupValues: {
|
|
101
111
|
column: string;
|
|
@@ -125,8 +135,10 @@ export interface GroupDetailOptions {
|
|
|
125
135
|
*/
|
|
126
136
|
export declare function buildGroupDetailQuery(options: GroupDetailOptions): ParameterizedQuery;
|
|
127
137
|
export interface QueryOptions {
|
|
128
|
-
/** Table name to query */
|
|
129
|
-
table
|
|
138
|
+
/** Table name to query (mutually exclusive with `source`) */
|
|
139
|
+
table?: string;
|
|
140
|
+
/** Raw SQL subquery source (mutually exclusive with `table`) */
|
|
141
|
+
source?: string;
|
|
130
142
|
/** Filter conditions */
|
|
131
143
|
filters?: FilterCondition[];
|
|
132
144
|
/** Filter logic (and/or) */
|
package/dist/query/builder.js
CHANGED
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
* Column names are validated against an allowlist (from schema introspection)
|
|
9
9
|
* to prevent SQL injection via identifier manipulation.
|
|
10
10
|
*/
|
|
11
|
+
// ─── Source Resolution ──────────────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a FROM clause from either a table name or a raw SQL source.
|
|
14
|
+
* When `source` is provided, it's used as a subquery: `(source) AS _gridlite_sub`.
|
|
15
|
+
* When `table` is provided, it's quoted as an identifier: `"table"`.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveFrom(table, source) {
|
|
18
|
+
if (source) {
|
|
19
|
+
return `(${source}) AS _gridlite_sub`;
|
|
20
|
+
}
|
|
21
|
+
if (table) {
|
|
22
|
+
return quoteIdentifier(table);
|
|
23
|
+
}
|
|
24
|
+
throw new Error("Either `table` or `source` must be provided");
|
|
25
|
+
}
|
|
11
26
|
// ─── Column Name Validation ─────────────────────────────────────────────────
|
|
12
27
|
/**
|
|
13
28
|
* Valid SQL identifier pattern: letters, digits, underscores.
|
|
@@ -222,11 +237,11 @@ export function buildGlobalSearchClause(searchTerm, textColumns, paramOffset = 0
|
|
|
222
237
|
* FROM "employees" WHERE ... GROUP BY "department" ORDER BY "department"
|
|
223
238
|
*/
|
|
224
239
|
export function buildGroupSummaryQuery(options) {
|
|
225
|
-
const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, sorting = [], page, pageSize, } = options;
|
|
240
|
+
const { table, source, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, sorting = [], page, pageSize, } = options;
|
|
226
241
|
if (grouping.length === 0) {
|
|
227
242
|
throw new Error("buildGroupSummaryQuery requires at least one group");
|
|
228
243
|
}
|
|
229
|
-
const
|
|
244
|
+
const fromClause = resolveFrom(table, source);
|
|
230
245
|
const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
|
|
231
246
|
// WHERE from filters
|
|
232
247
|
const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
|
|
@@ -260,7 +275,7 @@ export function buildGroupSummaryQuery(options) {
|
|
|
260
275
|
: "";
|
|
261
276
|
const parts = [
|
|
262
277
|
`SELECT ${selectColumns}`,
|
|
263
|
-
`FROM ${
|
|
278
|
+
`FROM ${fromClause}`,
|
|
264
279
|
whereSQL,
|
|
265
280
|
groupBy,
|
|
266
281
|
orderBy,
|
|
@@ -272,11 +287,11 @@ export function buildGroupSummaryQuery(options) {
|
|
|
272
287
|
* Build a count query for group summaries (how many groups exist).
|
|
273
288
|
*/
|
|
274
289
|
export function buildGroupCountQuery(options) {
|
|
275
|
-
const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
|
|
290
|
+
const { table, source, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
|
|
276
291
|
if (grouping.length === 0) {
|
|
277
292
|
throw new Error("buildGroupCountQuery requires at least one group");
|
|
278
293
|
}
|
|
279
|
-
const
|
|
294
|
+
const fromClause = resolveFrom(table, source);
|
|
280
295
|
const groupCols = grouping.map((g) => quoteIdentifier(g.column, allowedColumns));
|
|
281
296
|
const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
|
|
282
297
|
const searchCols = searchColumns ?? allowedColumns ?? [];
|
|
@@ -293,7 +308,7 @@ export function buildGroupCountQuery(options) {
|
|
|
293
308
|
whereSQL = `WHERE ${globalSearchClause.sql}`;
|
|
294
309
|
}
|
|
295
310
|
const parts = [
|
|
296
|
-
`SELECT COUNT(*) AS "total" FROM (SELECT 1 FROM ${
|
|
311
|
+
`SELECT COUNT(*) AS "total" FROM (SELECT 1 FROM ${fromClause}`,
|
|
297
312
|
whereSQL,
|
|
298
313
|
`GROUP BY ${groupCols.join(", ")}`,
|
|
299
314
|
`) AS "_groups"`,
|
|
@@ -310,11 +325,11 @@ export function buildGroupCountQuery(options) {
|
|
|
310
325
|
* SELECT * FROM "employees" WHERE ... AND "department" = $N ORDER BY ...
|
|
311
326
|
*/
|
|
312
327
|
export function buildGroupDetailQuery(options) {
|
|
313
|
-
const { table, groupValues, filters = [], filterLogic = "and", sorting = [], allowedColumns, globalSearch, searchColumns, } = options;
|
|
328
|
+
const { table, source, groupValues, filters = [], filterLogic = "and", sorting = [], allowedColumns, globalSearch, searchColumns, } = options;
|
|
314
329
|
if (groupValues.length === 0) {
|
|
315
330
|
throw new Error("buildGroupDetailQuery requires at least one group value");
|
|
316
331
|
}
|
|
317
|
-
const
|
|
332
|
+
const fromClause = resolveFrom(table, source);
|
|
318
333
|
// Base WHERE from filters
|
|
319
334
|
const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
|
|
320
335
|
// Global search
|
|
@@ -345,7 +360,7 @@ export function buildGroupDetailQuery(options) {
|
|
|
345
360
|
const allWhereParts = [...baseWhereParts, ...groupConstraints];
|
|
346
361
|
const whereSQL = allWhereParts.length > 0 ? `WHERE ${allWhereParts.join(" AND ")}` : "";
|
|
347
362
|
const orderBy = buildOrderByClause(sorting, allowedColumns);
|
|
348
|
-
const parts = [`SELECT *`, `FROM ${
|
|
363
|
+
const parts = [`SELECT *`, `FROM ${fromClause}`, whereSQL, orderBy].filter(Boolean);
|
|
349
364
|
return { sql: parts.join(" "), params: allParams };
|
|
350
365
|
}
|
|
351
366
|
/**
|
|
@@ -355,8 +370,8 @@ export function buildGroupDetailQuery(options) {
|
|
|
355
370
|
* WHERE, ORDER BY, GROUP BY, and LIMIT/OFFSET clauses into a single query.
|
|
356
371
|
*/
|
|
357
372
|
export function buildQuery(options) {
|
|
358
|
-
const { table, filters = [], filterLogic = "and", sorting = [], grouping = [], page, pageSize, allowedColumns, globalSearch, searchColumns, } = options;
|
|
359
|
-
const
|
|
373
|
+
const { table, source, filters = [], filterLogic = "and", sorting = [], grouping = [], page, pageSize, allowedColumns, globalSearch, searchColumns, } = options;
|
|
374
|
+
const fromClause = resolveFrom(table, source);
|
|
360
375
|
// GROUP BY affects SELECT columns
|
|
361
376
|
const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
|
|
362
377
|
// WHERE clause from filters
|
|
@@ -386,7 +401,7 @@ export function buildQuery(options) {
|
|
|
386
401
|
// Compose
|
|
387
402
|
const parts = [
|
|
388
403
|
`SELECT ${selectColumns}`,
|
|
389
|
-
`FROM ${
|
|
404
|
+
`FROM ${fromClause}`,
|
|
390
405
|
whereSQL,
|
|
391
406
|
groupBy,
|
|
392
407
|
orderBy,
|
|
@@ -402,8 +417,8 @@ export function buildQuery(options) {
|
|
|
402
417
|
* Uses the same filters but no sorting/grouping/pagination.
|
|
403
418
|
*/
|
|
404
419
|
export function buildCountQuery(options) {
|
|
405
|
-
const { table, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
|
|
406
|
-
const
|
|
420
|
+
const { table, source, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
|
|
421
|
+
const fromClause = resolveFrom(table, source);
|
|
407
422
|
const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
|
|
408
423
|
// Global search clause
|
|
409
424
|
const searchCols = searchColumns ?? allowedColumns ?? [];
|
|
@@ -422,7 +437,7 @@ export function buildCountQuery(options) {
|
|
|
422
437
|
}
|
|
423
438
|
const parts = [
|
|
424
439
|
'SELECT COUNT(*) AS "total"',
|
|
425
|
-
`FROM ${
|
|
440
|
+
`FROM ${fromClause}`,
|
|
426
441
|
whereSQL,
|
|
427
442
|
].filter(Boolean);
|
|
428
443
|
return {
|