@shotleybuilder/svelte-gridlite-kit 0.3.1 → 0.4.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 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 the `query` prop |
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
 
@@ -164,33 +164,31 @@ async function rebuildQuery() {
164
164
  }
165
165
  let sql;
166
166
  let params = [];
167
- if (query) {
168
- sql = query;
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
- if (!table) return;
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
- table,
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
- table,
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
- if (!table) return;
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
- table,
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
- table,
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
- if (!table) return;
375
+ const querySource = query ? { source: query } : table ? { table } : null;
376
+ if (!querySource) return;
376
377
  try {
377
378
  const countQuery = buildCountQuery({
378
- table,
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 table && toolbarLayout !== 'aggrid'}
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 table && toolbarLayout === 'aggrid'}
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
- {#if table}
1163
- <button
1164
- class="gridlite-th-menu-btn"
1165
- on:click|stopPropagation={() =>
1166
- (columnMenuOpen = columnMenuOpen === col.name ? null : col.name)}
1167
- title="Column options"
1168
- type="button"
1169
- >
1170
- <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1171
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1172
- </svg>
1173
- </button>
1174
- <ColumnMenu
1175
- columnName={col.name}
1176
- isOpen={columnMenuOpen === col.name}
1177
- {sorting}
1178
- canSort={features.sorting ?? false}
1179
- canFilter={features.filtering ?? false}
1180
- canGroup={features.grouping ?? false}
1181
- onSort={handleColumnMenuSort}
1182
- onFilter={handleColumnMenuFilter}
1183
- onGroup={handleColumnMenuGroup}
1184
- onHide={handleColumnMenuHide}
1185
- onClose={() => (columnMenuOpen = null)}
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 ${quoteIdentifier(table)} WHERE ${quotedCol} IS NOT NULL ORDER BY val LIMIT 200`;
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 ${quoteIdentifier(table)}`;
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: string;
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
@@ -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: string;
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: string;
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: string;
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) */
@@ -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.
@@ -24,7 +39,9 @@ export function quoteIdentifier(name, allowedColumns) {
24
39
  if (!VALID_IDENTIFIER.test(name)) {
25
40
  throw new Error(`Invalid column name: ${JSON.stringify(name)}`);
26
41
  }
27
- if (allowedColumns && !allowedColumns.includes(name)) {
42
+ if (allowedColumns &&
43
+ allowedColumns.length > 0 &&
44
+ !allowedColumns.includes(name)) {
28
45
  throw new Error(`Column not found: ${JSON.stringify(name)}`);
29
46
  }
30
47
  return `"${name}"`;
@@ -222,11 +239,11 @@ export function buildGlobalSearchClause(searchTerm, textColumns, paramOffset = 0
222
239
  * FROM "employees" WHERE ... GROUP BY "department" ORDER BY "department"
223
240
  */
224
241
  export function buildGroupSummaryQuery(options) {
225
- const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, sorting = [], page, pageSize, } = options;
242
+ const { table, source, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, sorting = [], page, pageSize, } = options;
226
243
  if (grouping.length === 0) {
227
244
  throw new Error("buildGroupSummaryQuery requires at least one group");
228
245
  }
229
- const tableName = quoteIdentifier(table);
246
+ const fromClause = resolveFrom(table, source);
230
247
  const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
231
248
  // WHERE from filters
232
249
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
@@ -260,7 +277,7 @@ export function buildGroupSummaryQuery(options) {
260
277
  : "";
261
278
  const parts = [
262
279
  `SELECT ${selectColumns}`,
263
- `FROM ${tableName}`,
280
+ `FROM ${fromClause}`,
264
281
  whereSQL,
265
282
  groupBy,
266
283
  orderBy,
@@ -272,11 +289,11 @@ export function buildGroupSummaryQuery(options) {
272
289
  * Build a count query for group summaries (how many groups exist).
273
290
  */
274
291
  export function buildGroupCountQuery(options) {
275
- const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
292
+ const { table, source, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
276
293
  if (grouping.length === 0) {
277
294
  throw new Error("buildGroupCountQuery requires at least one group");
278
295
  }
279
- const tableName = quoteIdentifier(table);
296
+ const fromClause = resolveFrom(table, source);
280
297
  const groupCols = grouping.map((g) => quoteIdentifier(g.column, allowedColumns));
281
298
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
282
299
  const searchCols = searchColumns ?? allowedColumns ?? [];
@@ -293,7 +310,7 @@ export function buildGroupCountQuery(options) {
293
310
  whereSQL = `WHERE ${globalSearchClause.sql}`;
294
311
  }
295
312
  const parts = [
296
- `SELECT COUNT(*) AS "total" FROM (SELECT 1 FROM ${tableName}`,
313
+ `SELECT COUNT(*) AS "total" FROM (SELECT 1 FROM ${fromClause}`,
297
314
  whereSQL,
298
315
  `GROUP BY ${groupCols.join(", ")}`,
299
316
  `) AS "_groups"`,
@@ -310,11 +327,11 @@ export function buildGroupCountQuery(options) {
310
327
  * SELECT * FROM "employees" WHERE ... AND "department" = $N ORDER BY ...
311
328
  */
312
329
  export function buildGroupDetailQuery(options) {
313
- const { table, groupValues, filters = [], filterLogic = "and", sorting = [], allowedColumns, globalSearch, searchColumns, } = options;
330
+ const { table, source, groupValues, filters = [], filterLogic = "and", sorting = [], allowedColumns, globalSearch, searchColumns, } = options;
314
331
  if (groupValues.length === 0) {
315
332
  throw new Error("buildGroupDetailQuery requires at least one group value");
316
333
  }
317
- const tableName = quoteIdentifier(table);
334
+ const fromClause = resolveFrom(table, source);
318
335
  // Base WHERE from filters
319
336
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
320
337
  // Global search
@@ -345,7 +362,7 @@ export function buildGroupDetailQuery(options) {
345
362
  const allWhereParts = [...baseWhereParts, ...groupConstraints];
346
363
  const whereSQL = allWhereParts.length > 0 ? `WHERE ${allWhereParts.join(" AND ")}` : "";
347
364
  const orderBy = buildOrderByClause(sorting, allowedColumns);
348
- const parts = [`SELECT *`, `FROM ${tableName}`, whereSQL, orderBy].filter(Boolean);
365
+ const parts = [`SELECT *`, `FROM ${fromClause}`, whereSQL, orderBy].filter(Boolean);
349
366
  return { sql: parts.join(" "), params: allParams };
350
367
  }
351
368
  /**
@@ -355,8 +372,8 @@ export function buildGroupDetailQuery(options) {
355
372
  * WHERE, ORDER BY, GROUP BY, and LIMIT/OFFSET clauses into a single query.
356
373
  */
357
374
  export function buildQuery(options) {
358
- const { table, filters = [], filterLogic = "and", sorting = [], grouping = [], page, pageSize, allowedColumns, globalSearch, searchColumns, } = options;
359
- const tableName = quoteIdentifier(table);
375
+ const { table, source, filters = [], filterLogic = "and", sorting = [], grouping = [], page, pageSize, allowedColumns, globalSearch, searchColumns, } = options;
376
+ const fromClause = resolveFrom(table, source);
360
377
  // GROUP BY affects SELECT columns
361
378
  const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
362
379
  // WHERE clause from filters
@@ -386,7 +403,7 @@ export function buildQuery(options) {
386
403
  // Compose
387
404
  const parts = [
388
405
  `SELECT ${selectColumns}`,
389
- `FROM ${tableName}`,
406
+ `FROM ${fromClause}`,
390
407
  whereSQL,
391
408
  groupBy,
392
409
  orderBy,
@@ -402,8 +419,8 @@ export function buildQuery(options) {
402
419
  * Uses the same filters but no sorting/grouping/pagination.
403
420
  */
404
421
  export function buildCountQuery(options) {
405
- const { table, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
406
- const tableName = quoteIdentifier(table);
422
+ const { table, source, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
423
+ const fromClause = resolveFrom(table, source);
407
424
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
408
425
  // Global search clause
409
426
  const searchCols = searchColumns ?? allowedColumns ?? [];
@@ -422,7 +439,7 @@ export function buildCountQuery(options) {
422
439
  }
423
440
  const parts = [
424
441
  'SELECT COUNT(*) AS "total"',
425
- `FROM ${tableName}`,
442
+ `FROM ${fromClause}`,
426
443
  whereSQL,
427
444
  ].filter(Boolean);
428
445
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotleybuilder/svelte-gridlite-kit",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "A SQL-native data grid component for Svelte and SvelteKit, powered by PGLite",
5
5
  "author": "Sertantai",
6
6
  "license": "MIT",