@tablecraft/engine 0.1.0-beta.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/dist/src/__tests__/inputValidator.test.d.ts +2 -0
- package/dist/src/__tests__/inputValidator.test.d.ts.map +1 -0
- package/dist/src/__tests__/inputValidator.test.js +205 -0
- package/dist/src/__tests__/inputValidator.test.js.map +1 -0
- package/dist/src/__tests__/metadataBuilder.test.d.ts +2 -0
- package/dist/src/__tests__/metadataBuilder.test.d.ts.map +1 -0
- package/dist/src/__tests__/metadataBuilder.test.js +221 -0
- package/dist/src/__tests__/metadataBuilder.test.js.map +1 -0
- package/dist/src/core/aggregationBuilder.d.ts +17 -0
- package/dist/src/core/aggregationBuilder.d.ts.map +1 -0
- package/dist/src/core/aggregationBuilder.js +81 -0
- package/dist/src/core/aggregationBuilder.js.map +1 -0
- package/dist/src/core/cursorPagination.d.ts +36 -0
- package/dist/src/core/cursorPagination.d.ts.map +1 -0
- package/dist/src/core/cursorPagination.js +88 -0
- package/dist/src/core/cursorPagination.js.map +1 -0
- package/dist/src/core/datePresets.d.ts +19 -0
- package/dist/src/core/datePresets.d.ts.map +1 -0
- package/dist/src/core/datePresets.js +96 -0
- package/dist/src/core/datePresets.js.map +1 -0
- package/dist/src/core/dialect.d.ts +17 -0
- package/dist/src/core/dialect.d.ts.map +1 -0
- package/dist/src/core/dialect.js +60 -0
- package/dist/src/core/dialect.js.map +1 -0
- package/dist/src/core/fieldSelector.d.ts +19 -0
- package/dist/src/core/fieldSelector.d.ts.map +1 -0
- package/dist/src/core/fieldSelector.js +49 -0
- package/dist/src/core/fieldSelector.js.map +1 -0
- package/dist/src/core/filterBuilder.d.ts +22 -0
- package/dist/src/core/filterBuilder.d.ts.map +1 -0
- package/dist/src/core/filterBuilder.js +112 -0
- package/dist/src/core/filterBuilder.js.map +1 -0
- package/dist/src/core/filterGroupBuilder.d.ts +28 -0
- package/dist/src/core/filterGroupBuilder.d.ts.map +1 -0
- package/dist/src/core/filterGroupBuilder.js +73 -0
- package/dist/src/core/filterGroupBuilder.js.map +1 -0
- package/dist/src/core/groupByBuilder.d.ts +23 -0
- package/dist/src/core/groupByBuilder.d.ts.map +1 -0
- package/dist/src/core/groupByBuilder.js +127 -0
- package/dist/src/core/groupByBuilder.js.map +1 -0
- package/dist/src/core/inputValidator.d.ts +8 -0
- package/dist/src/core/inputValidator.d.ts.map +1 -0
- package/dist/src/core/inputValidator.js +117 -0
- package/dist/src/core/inputValidator.js.map +1 -0
- package/dist/src/core/metadataBuilder.d.ts +91 -0
- package/dist/src/core/metadataBuilder.d.ts.map +1 -0
- package/dist/src/core/metadataBuilder.js +220 -0
- package/dist/src/core/metadataBuilder.js.map +1 -0
- package/dist/src/core/paginationBuilder.d.ts +20 -0
- package/dist/src/core/paginationBuilder.d.ts.map +1 -0
- package/dist/src/core/paginationBuilder.js +42 -0
- package/dist/src/core/paginationBuilder.js.map +1 -0
- package/dist/src/core/queryBuilder.d.ts +20 -0
- package/dist/src/core/queryBuilder.d.ts.map +1 -0
- package/dist/src/core/queryBuilder.js +163 -0
- package/dist/src/core/queryBuilder.js.map +1 -0
- package/dist/src/core/recursiveBuilder.d.ts +25 -0
- package/dist/src/core/recursiveBuilder.d.ts.map +1 -0
- package/dist/src/core/recursiveBuilder.js +86 -0
- package/dist/src/core/recursiveBuilder.js.map +1 -0
- package/dist/src/core/relationBuilder.d.ts +19 -0
- package/dist/src/core/relationBuilder.d.ts.map +1 -0
- package/dist/src/core/relationBuilder.js +118 -0
- package/dist/src/core/relationBuilder.js.map +1 -0
- package/dist/src/core/roleFilter.d.ts +11 -0
- package/dist/src/core/roleFilter.d.ts.map +1 -0
- package/dist/src/core/roleFilter.js +24 -0
- package/dist/src/core/roleFilter.js.map +1 -0
- package/dist/src/core/searchBuilder.d.ts +17 -0
- package/dist/src/core/searchBuilder.d.ts.map +1 -0
- package/dist/src/core/searchBuilder.js +71 -0
- package/dist/src/core/searchBuilder.js.map +1 -0
- package/dist/src/core/softDelete.d.ts +12 -0
- package/dist/src/core/softDelete.d.ts.map +1 -0
- package/dist/src/core/softDelete.js +29 -0
- package/dist/src/core/softDelete.js.map +1 -0
- package/dist/src/core/sortBuilder.d.ts +14 -0
- package/dist/src/core/sortBuilder.d.ts.map +1 -0
- package/dist/src/core/sortBuilder.js +58 -0
- package/dist/src/core/sortBuilder.js.map +1 -0
- package/dist/src/core/subqueryBuilder.d.ts +13 -0
- package/dist/src/core/subqueryBuilder.d.ts.map +1 -0
- package/dist/src/core/subqueryBuilder.js +47 -0
- package/dist/src/core/subqueryBuilder.js.map +1 -0
- package/dist/src/core/validator.d.ts +18 -0
- package/dist/src/core/validator.d.ts.map +1 -0
- package/dist/src/core/validator.js +88 -0
- package/dist/src/core/validator.js.map +1 -0
- package/dist/src/define.d.ts +274 -0
- package/dist/src/define.d.ts.map +1 -0
- package/dist/src/define.js +690 -0
- package/dist/src/define.js.map +1 -0
- package/dist/src/engine.d.ts +17 -0
- package/dist/src/engine.d.ts.map +1 -0
- package/dist/src/engine.js +429 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/errors.d.ts +53 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +80 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +37 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +41 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/types/engine.d.ts +92 -0
- package/dist/src/types/engine.d.ts.map +1 -0
- package/dist/src/types/engine.js +2 -0
- package/dist/src/types/engine.js.map +1 -0
- package/dist/src/types/table.d.ts +867 -0
- package/dist/src/types/table.d.ts.map +1 -0
- package/dist/src/types/table.js +198 -0
- package/dist/src/types/table.js.map +1 -0
- package/dist/src/utils/adapterUtils.d.ts +16 -0
- package/dist/src/utils/adapterUtils.d.ts.map +1 -0
- package/dist/src/utils/adapterUtils.js +28 -0
- package/dist/src/utils/adapterUtils.js.map +1 -0
- package/dist/src/utils/codegen.d.ts +7 -0
- package/dist/src/utils/codegen.d.ts.map +1 -0
- package/dist/src/utils/codegen.js +126 -0
- package/dist/src/utils/codegen.js.map +1 -0
- package/dist/src/utils/export.d.ts +15 -0
- package/dist/src/utils/export.d.ts.map +1 -0
- package/dist/src/utils/export.js +42 -0
- package/dist/src/utils/export.js.map +1 -0
- package/dist/src/utils/introspect.d.ts +32 -0
- package/dist/src/utils/introspect.d.ts.map +1 -0
- package/dist/src/utils/introspect.js +174 -0
- package/dist/src/utils/introspect.js.map +1 -0
- package/dist/src/utils/openapi.d.ts +6 -0
- package/dist/src/utils/openapi.d.ts.map +1 -0
- package/dist/src/utils/openapi.js +138 -0
- package/dist/src/utils/openapi.js.map +1 -0
- package/dist/src/utils/operators.d.ts +8 -0
- package/dist/src/utils/operators.d.ts.map +1 -0
- package/dist/src/utils/operators.js +70 -0
- package/dist/src/utils/operators.js.map +1 -0
- package/dist/src/utils/requestParser.d.ts +18 -0
- package/dist/src/utils/requestParser.d.ts.map +1 -0
- package/dist/src/utils/requestParser.js +126 -0
- package/dist/src/utils/requestParser.js.map +1 -0
- package/dist/src/utils/responseFormatter.d.ts +12 -0
- package/dist/src/utils/responseFormatter.d.ts.map +1 -0
- package/dist/src/utils/responseFormatter.js +106 -0
- package/dist/src/utils/responseFormatter.js.map +1 -0
- package/dist/src/utils/typedSql.d.ts +70 -0
- package/dist/src/utils/typedSql.d.ts.map +1 -0
- package/dist/src/utils/typedSql.js +102 -0
- package/dist/src/utils/typedSql.js.map +1 -0
- package/dist/test/columnMeta.test.d.ts +2 -0
- package/dist/test/columnMeta.test.d.ts.map +1 -0
- package/dist/test/columnMeta.test.js +92 -0
- package/dist/test/columnMeta.test.js.map +1 -0
- package/dist/test/core/aggregationBuilder.test.d.ts +2 -0
- package/dist/test/core/aggregationBuilder.test.d.ts.map +1 -0
- package/dist/test/core/aggregationBuilder.test.js +64 -0
- package/dist/test/core/aggregationBuilder.test.js.map +1 -0
- package/dist/test/core/filterBuilder.test.d.ts +2 -0
- package/dist/test/core/filterBuilder.test.d.ts.map +1 -0
- package/dist/test/core/filterBuilder.test.js +80 -0
- package/dist/test/core/filterBuilder.test.js.map +1 -0
- package/dist/test/core/paginationBuilder.test.d.ts +2 -0
- package/dist/test/core/paginationBuilder.test.d.ts.map +1 -0
- package/dist/test/core/paginationBuilder.test.js +63 -0
- package/dist/test/core/paginationBuilder.test.js.map +1 -0
- package/dist/test/core/queryBuilder.test.d.ts +2 -0
- package/dist/test/core/queryBuilder.test.d.ts.map +1 -0
- package/dist/test/core/queryBuilder.test.js +92 -0
- package/dist/test/core/queryBuilder.test.js.map +1 -0
- package/dist/test/core/searchBuilder.test.d.ts +2 -0
- package/dist/test/core/searchBuilder.test.d.ts.map +1 -0
- package/dist/test/core/searchBuilder.test.js +68 -0
- package/dist/test/core/searchBuilder.test.js.map +1 -0
- package/dist/test/core/softDelete.test.d.ts +2 -0
- package/dist/test/core/softDelete.test.d.ts.map +1 -0
- package/dist/test/core/softDelete.test.js +60 -0
- package/dist/test/core/softDelete.test.js.map +1 -0
- package/dist/test/core/sortBuilder.test.d.ts +2 -0
- package/dist/test/core/sortBuilder.test.d.ts.map +1 -0
- package/dist/test/core/sortBuilder.test.js +59 -0
- package/dist/test/core/sortBuilder.test.js.map +1 -0
- package/dist/test/core/subqueryBuilder.test.d.ts +2 -0
- package/dist/test/core/subqueryBuilder.test.d.ts.map +1 -0
- package/dist/test/core/subqueryBuilder.test.js +48 -0
- package/dist/test/core/subqueryBuilder.test.js.map +1 -0
- package/dist/test/core/validator.test.d.ts +2 -0
- package/dist/test/core/validator.test.d.ts.map +1 -0
- package/dist/test/core/validator.test.js +81 -0
- package/dist/test/core/validator.test.js.map +1 -0
- package/dist/test/datePresets.test.d.ts +2 -0
- package/dist/test/datePresets.test.d.ts.map +1 -0
- package/dist/test/datePresets.test.js +57 -0
- package/dist/test/datePresets.test.js.map +1 -0
- package/dist/test/errors.test.d.ts +2 -0
- package/dist/test/errors.test.d.ts.map +1 -0
- package/dist/test/errors.test.js +62 -0
- package/dist/test/errors.test.js.map +1 -0
- package/dist/test/inputValidator.test.d.ts +2 -0
- package/dist/test/inputValidator.test.d.ts.map +1 -0
- package/dist/test/inputValidator.test.js +60 -0
- package/dist/test/inputValidator.test.js.map +1 -0
- package/dist/test/metadata.test.d.ts +2 -0
- package/dist/test/metadata.test.d.ts.map +1 -0
- package/dist/test/metadata.test.js +285 -0
- package/dist/test/metadata.test.js.map +1 -0
- package/dist/test/metadataComplete.test.d.ts +2 -0
- package/dist/test/metadataComplete.test.d.ts.map +1 -0
- package/dist/test/metadataComplete.test.js +113 -0
- package/dist/test/metadataComplete.test.js.map +1 -0
- package/dist/test/roleFilter.test.d.ts +2 -0
- package/dist/test/roleFilter.test.d.ts.map +1 -0
- package/dist/test/roleFilter.test.js +53 -0
- package/dist/test/roleFilter.test.js.map +1 -0
- package/dist/test/typedSql.test.d.ts +2 -0
- package/dist/test/typedSql.test.d.ts.map +1 -0
- package/dist/test/typedSql.test.js +63 -0
- package/dist/test/typedSql.test.js.map +1 -0
- package/dist/test/utils/codegen.test.d.ts +2 -0
- package/dist/test/utils/codegen.test.d.ts.map +1 -0
- package/dist/test/utils/codegen.test.js +65 -0
- package/dist/test/utils/codegen.test.js.map +1 -0
- package/dist/test/utils/export.test.d.ts +2 -0
- package/dist/test/utils/export.test.d.ts.map +1 -0
- package/dist/test/utils/export.test.js +54 -0
- package/dist/test/utils/export.test.js.map +1 -0
- package/dist/test/utils/openapi.test.d.ts +2 -0
- package/dist/test/utils/openapi.test.d.ts.map +1 -0
- package/dist/test/utils/openapi.test.js +65 -0
- package/dist/test/utils/openapi.test.js.map +1 -0
- package/dist/test/utils/requestParser.test.d.ts +2 -0
- package/dist/test/utils/requestParser.test.d.ts.map +1 -0
- package/dist/test/utils/requestParser.test.js +89 -0
- package/dist/test/utils/requestParser.test.js.map +1 -0
- package/dist/test/utils/responseFormatter.test.d.ts +2 -0
- package/dist/test/utils/responseFormatter.test.d.ts.map +1 -0
- package/dist/test/utils/responseFormatter.test.js +79 -0
- package/dist/test/utils/responseFormatter.test.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { sql, getTableColumns, gt, lt, asc, desc } from 'drizzle-orm';
|
|
2
|
+
/**
|
|
3
|
+
* Cursor-based pagination.
|
|
4
|
+
* Uses the sort column value as the cursor instead of OFFSET.
|
|
5
|
+
* O(1) performance regardless of page depth.
|
|
6
|
+
*
|
|
7
|
+
* Cursor format: base64({ field: value, field2: value2 })
|
|
8
|
+
*/
|
|
9
|
+
export class CursorPaginationBuilder {
|
|
10
|
+
schema;
|
|
11
|
+
constructor(schema) {
|
|
12
|
+
this.schema = schema;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Decodes a cursor string and builds WHERE + ORDER BY for the next page.
|
|
16
|
+
*/
|
|
17
|
+
build(config, cursor, pageSize, sort) {
|
|
18
|
+
const table = this.schema[config.base];
|
|
19
|
+
if (!table) {
|
|
20
|
+
return { whereCondition: undefined, orderBy: [], limit: pageSize + 1 };
|
|
21
|
+
}
|
|
22
|
+
const columns = getTableColumns(table);
|
|
23
|
+
// Determine sort fields (default to id or createdAt)
|
|
24
|
+
const sortFields = sort?.length
|
|
25
|
+
? sort
|
|
26
|
+
: config.defaultSort?.length
|
|
27
|
+
? config.defaultSort
|
|
28
|
+
: [{ field: 'id', order: 'asc' }];
|
|
29
|
+
// Build ORDER BY
|
|
30
|
+
const orderBy = sortFields.map((s) => {
|
|
31
|
+
const col = columns[s.field];
|
|
32
|
+
if (!col)
|
|
33
|
+
return s.order === 'desc' ? desc(sql.identifier(s.field)) : asc(sql.identifier(s.field));
|
|
34
|
+
return s.order === 'desc' ? desc(col) : asc(col);
|
|
35
|
+
});
|
|
36
|
+
// Decode cursor and build WHERE
|
|
37
|
+
let whereCondition;
|
|
38
|
+
if (cursor) {
|
|
39
|
+
const decoded = decodeCursor(cursor);
|
|
40
|
+
if (decoded && sortFields.length > 0) {
|
|
41
|
+
const primary = sortFields[0];
|
|
42
|
+
const col = columns[primary.field];
|
|
43
|
+
if (col && decoded[primary.field] !== undefined) {
|
|
44
|
+
whereCondition = primary.order === 'desc'
|
|
45
|
+
? lt(col, decoded[primary.field])
|
|
46
|
+
: gt(col, decoded[primary.field]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Fetch one extra row to determine if there's a next page
|
|
51
|
+
return {
|
|
52
|
+
whereCondition,
|
|
53
|
+
orderBy,
|
|
54
|
+
limit: pageSize + 1,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* From the fetched data (with 1 extra row), determine next cursor.
|
|
59
|
+
*/
|
|
60
|
+
buildMeta(data, pageSize, sort) {
|
|
61
|
+
const hasMore = data.length > pageSize;
|
|
62
|
+
const trimmed = hasMore ? data.slice(0, pageSize) : data;
|
|
63
|
+
let nextCursor = null;
|
|
64
|
+
if (hasMore && trimmed.length > 0) {
|
|
65
|
+
const lastRow = trimmed[trimmed.length - 1];
|
|
66
|
+
const sortField = sort?.[0]?.field ?? 'id';
|
|
67
|
+
nextCursor = encodeCursor({ [sortField]: lastRow[sortField] });
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
data: trimmed,
|
|
71
|
+
meta: { nextCursor, pageSize },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ── Cursor encoding ──
|
|
76
|
+
export function encodeCursor(values) {
|
|
77
|
+
return Buffer.from(JSON.stringify(values)).toString('base64url');
|
|
78
|
+
}
|
|
79
|
+
export function decodeCursor(cursor) {
|
|
80
|
+
try {
|
|
81
|
+
const json = Buffer.from(cursor, 'base64url').toString('utf-8');
|
|
82
|
+
return JSON.parse(json);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=cursorPagination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursorPagination.js","sourceRoot":"","sources":["../../../src/core/cursorPagination.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Column, SQL } from 'drizzle-orm';
|
|
2
|
+
import { DatePreset } from '../types/table';
|
|
3
|
+
/**
|
|
4
|
+
* Resolves a date preset name into a concrete date range.
|
|
5
|
+
* All dates are in UTC.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveDatePreset(preset: DatePreset): {
|
|
8
|
+
start: Date;
|
|
9
|
+
end: Date;
|
|
10
|
+
} | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Builds a SQL WHERE condition from a date preset.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDatePresetCondition(column: Column, preset: DatePreset): SQL | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a filter value is a date preset name.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isDatePreset(value: unknown): value is DatePreset;
|
|
19
|
+
//# sourceMappingURL=datePresets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datePresets.d.ts","sourceRoot":"","sources":["../../../src/core/datePresets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,GAAG,EAAgB,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,IAAI,CAAA;CAAE,GAAG,SAAS,CA+D5F;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,GACjB,GAAG,GAAG,SAAS,CAQjB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAOhE"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { and, gte, lt } from 'drizzle-orm';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a date preset name into a concrete date range.
|
|
4
|
+
* All dates are in UTC.
|
|
5
|
+
*/
|
|
6
|
+
export function resolveDatePreset(preset) {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const today = startOfDay(now);
|
|
9
|
+
const tomorrow = addDays(today, 1);
|
|
10
|
+
switch (preset) {
|
|
11
|
+
case 'today':
|
|
12
|
+
return { start: today, end: tomorrow };
|
|
13
|
+
case 'yesterday':
|
|
14
|
+
return { start: addDays(today, -1), end: today };
|
|
15
|
+
case 'last7days':
|
|
16
|
+
return { start: addDays(today, -7), end: tomorrow };
|
|
17
|
+
case 'last30days':
|
|
18
|
+
return { start: addDays(today, -30), end: tomorrow };
|
|
19
|
+
case 'last90days':
|
|
20
|
+
return { start: addDays(today, -90), end: tomorrow };
|
|
21
|
+
case 'thisWeek':
|
|
22
|
+
return { start: startOfWeek(today), end: tomorrow };
|
|
23
|
+
case 'lastWeek': {
|
|
24
|
+
const thisWeekStart = startOfWeek(today);
|
|
25
|
+
return { start: addDays(thisWeekStart, -7), end: thisWeekStart };
|
|
26
|
+
}
|
|
27
|
+
case 'thisMonth':
|
|
28
|
+
return { start: startOfMonth(today), end: tomorrow };
|
|
29
|
+
case 'lastMonth': {
|
|
30
|
+
const thisMonthStart = startOfMonth(today);
|
|
31
|
+
const lastMonthStart = new Date(thisMonthStart);
|
|
32
|
+
lastMonthStart.setMonth(lastMonthStart.getMonth() - 1);
|
|
33
|
+
return { start: lastMonthStart, end: thisMonthStart };
|
|
34
|
+
}
|
|
35
|
+
case 'thisQuarter':
|
|
36
|
+
return { start: startOfQuarter(today), end: tomorrow };
|
|
37
|
+
case 'lastQuarter': {
|
|
38
|
+
const thisQ = startOfQuarter(today);
|
|
39
|
+
const lastQ = new Date(thisQ);
|
|
40
|
+
lastQ.setMonth(lastQ.getMonth() - 3);
|
|
41
|
+
return { start: lastQ, end: thisQ };
|
|
42
|
+
}
|
|
43
|
+
case 'thisYear':
|
|
44
|
+
return { start: new Date(Date.UTC(today.getUTCFullYear(), 0, 1)), end: tomorrow };
|
|
45
|
+
case 'lastYear': {
|
|
46
|
+
const year = today.getUTCFullYear();
|
|
47
|
+
return { start: new Date(Date.UTC(year - 1, 0, 1)), end: new Date(Date.UTC(year, 0, 1)) };
|
|
48
|
+
}
|
|
49
|
+
case 'custom':
|
|
50
|
+
return undefined;
|
|
51
|
+
default:
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds a SQL WHERE condition from a date preset.
|
|
57
|
+
*/
|
|
58
|
+
export function buildDatePresetCondition(column, preset) {
|
|
59
|
+
const range = resolveDatePreset(preset);
|
|
60
|
+
if (!range)
|
|
61
|
+
return undefined;
|
|
62
|
+
return and(gte(column, range.start), lt(column, range.end));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Checks if a filter value is a date preset name.
|
|
66
|
+
*/
|
|
67
|
+
export function isDatePreset(value) {
|
|
68
|
+
const presets = new Set([
|
|
69
|
+
'today', 'yesterday', 'last7days', 'last30days', 'last90days',
|
|
70
|
+
'thisWeek', 'lastWeek', 'thisMonth', 'lastMonth',
|
|
71
|
+
'thisQuarter', 'lastQuarter', 'thisYear', 'lastYear', 'custom',
|
|
72
|
+
]);
|
|
73
|
+
return typeof value === 'string' && presets.has(value);
|
|
74
|
+
}
|
|
75
|
+
// ── Date helpers (no dependencies) ──
|
|
76
|
+
function startOfDay(d) {
|
|
77
|
+
return new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
78
|
+
}
|
|
79
|
+
function addDays(d, n) {
|
|
80
|
+
const result = new Date(d);
|
|
81
|
+
result.setUTCDate(result.getUTCDate() + n);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
function startOfWeek(d) {
|
|
85
|
+
const day = d.getUTCDay();
|
|
86
|
+
const diff = day === 0 ? 6 : day - 1; // Monday = start
|
|
87
|
+
return addDays(startOfDay(d), -diff);
|
|
88
|
+
}
|
|
89
|
+
function startOfMonth(d) {
|
|
90
|
+
return new Date(Date.UTC(d.getFullYear(), d.getMonth(), 1));
|
|
91
|
+
}
|
|
92
|
+
function startOfQuarter(d) {
|
|
93
|
+
const q = Math.floor(d.getMonth() / 3) * 3;
|
|
94
|
+
return new Date(Date.UTC(d.getFullYear(), q, 1));
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=datePresets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"datePresets.js","sourceRoot":"","sources":["../../../src/core/datePresets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database dialect detection and feature gating.
|
|
3
|
+
* Prevents PostgreSQL-only features from silently failing on MySQL/SQLite.
|
|
4
|
+
*/
|
|
5
|
+
export type Dialect = 'postgresql' | 'mysql' | 'sqlite' | 'unknown';
|
|
6
|
+
/**
|
|
7
|
+
* Auto-detect dialect from a Drizzle database instance.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detectDialect(db: any): Dialect;
|
|
10
|
+
export declare function supportsFeature(dialect: Dialect, feature: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Returns LIKE or ILIKE based on dialect.
|
|
13
|
+
* MySQL/SQLite: LIKE is case-insensitive by default.
|
|
14
|
+
* PostgreSQL: needs ILIKE for case-insensitive.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCaseInsensitiveLike(dialect: Dialect): 'like' | 'ilike';
|
|
17
|
+
//# sourceMappingURL=dialect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../../../src/core/dialect.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,OAAO,GAAG,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEpE;;GAEG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,GAAG,GAAG,OAAO,CAuB9C;AAeD,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK1E;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAEzE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database dialect detection and feature gating.
|
|
3
|
+
* Prevents PostgreSQL-only features from silently failing on MySQL/SQLite.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Auto-detect dialect from a Drizzle database instance.
|
|
7
|
+
*/
|
|
8
|
+
export function detectDialect(db) {
|
|
9
|
+
// Drizzle instances have internal markers we can check
|
|
10
|
+
const constructor = db?.constructor?.name?.toLowerCase() ?? '';
|
|
11
|
+
if (constructor.includes('pg') || constructor.includes('postgres') || constructor.includes('neon')) {
|
|
12
|
+
return 'postgresql';
|
|
13
|
+
}
|
|
14
|
+
if (constructor.includes('mysql') || constructor.includes('planetscale')) {
|
|
15
|
+
return 'mysql';
|
|
16
|
+
}
|
|
17
|
+
if (constructor.includes('sqlite') || constructor.includes('libsql') || constructor.includes('turso') || constructor.includes('d1')) {
|
|
18
|
+
return 'sqlite';
|
|
19
|
+
}
|
|
20
|
+
// Check for dialect property that some Drizzle instances expose
|
|
21
|
+
if (db?.dialect?.name) {
|
|
22
|
+
const name = db.dialect.name.toLowerCase();
|
|
23
|
+
if (name.includes('pg'))
|
|
24
|
+
return 'postgresql';
|
|
25
|
+
if (name.includes('mysql'))
|
|
26
|
+
return 'mysql';
|
|
27
|
+
if (name.includes('sqlite'))
|
|
28
|
+
return 'sqlite';
|
|
29
|
+
}
|
|
30
|
+
return 'unknown';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Feature support matrix per dialect.
|
|
34
|
+
*/
|
|
35
|
+
const FEATURES = {
|
|
36
|
+
ilike: new Set(['postgresql']),
|
|
37
|
+
fullTextSearch: new Set(['postgresql']),
|
|
38
|
+
recursiveCTE: new Set(['postgresql', 'sqlite']),
|
|
39
|
+
returning: new Set(['postgresql', 'sqlite']),
|
|
40
|
+
lateral: new Set(['postgresql']),
|
|
41
|
+
distinct: new Set(['postgresql', 'mysql', 'sqlite']),
|
|
42
|
+
estimatedCount: new Set(['postgresql']),
|
|
43
|
+
};
|
|
44
|
+
export function supportsFeature(dialect, feature) {
|
|
45
|
+
const supported = FEATURES[feature];
|
|
46
|
+
if (!supported)
|
|
47
|
+
return true; // unknown features assumed supported
|
|
48
|
+
if (dialect === 'unknown')
|
|
49
|
+
return true; // don't block unknown dialects
|
|
50
|
+
return supported.has(dialect);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns LIKE or ILIKE based on dialect.
|
|
54
|
+
* MySQL/SQLite: LIKE is case-insensitive by default.
|
|
55
|
+
* PostgreSQL: needs ILIKE for case-insensitive.
|
|
56
|
+
*/
|
|
57
|
+
export function getCaseInsensitiveLike(dialect) {
|
|
58
|
+
return dialect === 'postgresql' ? 'ilike' : 'like';
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=dialect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialect.js","sourceRoot":"","sources":["../../../src/core/dialect.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,EAAO;IACnC,uDAAuD;IACvD,MAAM,WAAW,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAE/D,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACnG,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACzE,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,IAAI,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACpI,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,gEAAgE;IAChE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAC3C,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,YAAY,CAAC;QAC7C,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,OAAO,OAAO,CAAC;QAC3C,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,QAAQ,CAAC;IAC/C,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;GAEG;AACH,MAAM,QAAQ,GAAiC;IAC7C,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;IAC9B,cAAc,EAAE,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;IACvC,YAAY,EAAE,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC/C,SAAS,EAAE,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC5C,OAAO,EAAE,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;IAChC,QAAQ,EAAE,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IACpD,cAAc,EAAE,IAAI,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,OAAgB,EAAE,OAAe;IAC/D,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,qCAAqC;IAClE,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,+BAA+B;IACvE,OAAO,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAgB;IACrD,OAAO,OAAO,KAAK,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Column, SQL } from 'drizzle-orm';
|
|
2
|
+
import { TableConfig } from '../types/table';
|
|
3
|
+
/**
|
|
4
|
+
* Filters the selection to only include requested fields.
|
|
5
|
+
* Supports `?select=id,name,email` from URL.
|
|
6
|
+
*/
|
|
7
|
+
export declare class FieldSelector {
|
|
8
|
+
/**
|
|
9
|
+
* Narrows down a selection object to only include the requested fields.
|
|
10
|
+
* If no fields requested, returns the original selection.
|
|
11
|
+
*/
|
|
12
|
+
applyFieldSelection(selection: Record<string, SQL | Column>, requestedFields: string[] | undefined, config: TableConfig): Record<string, SQL | Column>;
|
|
13
|
+
/**
|
|
14
|
+
* Filters the response data to only include requested fields.
|
|
15
|
+
* Defense-in-depth — even if SELECT returns extra columns.
|
|
16
|
+
*/
|
|
17
|
+
filterResponseFields(data: Record<string, unknown>[], requestedFields: string[] | undefined): Record<string, unknown>[];
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=fieldSelector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldSelector.d.ts","sourceRoot":"","sources":["../../../src/core/fieldSelector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C;;;GAGG;AACH,qBAAa,aAAa;IACxB;;;OAGG;IACH,mBAAmB,CACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,CAAC,EACvC,eAAe,EAAE,MAAM,EAAE,GAAG,SAAS,EACrC,MAAM,EAAE,WAAW,GAClB,MAAM,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,CAAC;IA2B/B;;;OAGG;IACH,oBAAoB,CAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,eAAe,EAAE,MAAM,EAAE,GAAG,SAAS,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE;CAe7B"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filters the selection to only include requested fields.
|
|
3
|
+
* Supports `?select=id,name,email` from URL.
|
|
4
|
+
*/
|
|
5
|
+
export class FieldSelector {
|
|
6
|
+
/**
|
|
7
|
+
* Narrows down a selection object to only include the requested fields.
|
|
8
|
+
* If no fields requested, returns the original selection.
|
|
9
|
+
*/
|
|
10
|
+
applyFieldSelection(selection, requestedFields, config) {
|
|
11
|
+
if (!requestedFields?.length)
|
|
12
|
+
return selection;
|
|
13
|
+
// Build a whitelist of allowed (non-hidden) fields
|
|
14
|
+
const allowed = new Set(config.columns.filter((c) => !c.hidden).map((c) => c.name));
|
|
15
|
+
const filtered = {};
|
|
16
|
+
for (const field of requestedFields) {
|
|
17
|
+
if (allowed.has(field) && selection[field]) {
|
|
18
|
+
filtered[field] = selection[field];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Always include primary key if it exists (needed for includes/relations)
|
|
22
|
+
if (selection['id'] && !filtered['id']) {
|
|
23
|
+
const idCol = config.columns.find((c) => c.name === 'id');
|
|
24
|
+
if (idCol && !idCol.hidden) {
|
|
25
|
+
filtered['id'] = selection['id'];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return Object.keys(filtered).length > 0 ? filtered : selection;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Filters the response data to only include requested fields.
|
|
32
|
+
* Defense-in-depth — even if SELECT returns extra columns.
|
|
33
|
+
*/
|
|
34
|
+
filterResponseFields(data, requestedFields) {
|
|
35
|
+
if (!requestedFields?.length)
|
|
36
|
+
return data;
|
|
37
|
+
const fieldSet = new Set(requestedFields);
|
|
38
|
+
return data.map((row) => {
|
|
39
|
+
const filtered = {};
|
|
40
|
+
for (const field of fieldSet) {
|
|
41
|
+
if (field in row) {
|
|
42
|
+
filtered[field] = row[field];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return filtered;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=fieldSelector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldSelector.js","sourceRoot":"","sources":["../../../src/core/fieldSelector.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,OAAO,aAAa;IACxB;;;OAGG;IACH,mBAAmB,CACjB,SAAuC,EACvC,eAAqC,EACrC,MAAmB;QAEnB,IAAI,CAAC,eAAe,EAAE,MAAM;YAAE,OAAO,SAAS,CAAC;QAE/C,mDAAmD;QACnD,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAC3D,CAAC;QAEF,MAAM,QAAQ,GAAiC,EAAE,CAAC;QAElD,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;YACpC,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3C,QAAQ,CAAC,KAAK,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QAED,0EAA0E;QAC1E,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;YAC1D,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC3B,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;IACjE,CAAC;IAED;;;OAGG;IACH,oBAAoB,CAClB,IAA+B,EAC/B,eAAqC;QAErC,IAAI,CAAC,eAAe,EAAE,MAAM;YAAE,OAAO,IAAI,CAAC;QAE1C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC;QAE1C,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YACtB,MAAM,QAAQ,GAA4B,EAAE,CAAC;YAC7C,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;gBAC7B,IAAI,KAAK,IAAI,GAAG,EAAE,CAAC;oBACjB,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SQL } from 'drizzle-orm';
|
|
2
|
+
import { TableConfig } from '../types/table';
|
|
3
|
+
import { FilterParam } from '../types/engine';
|
|
4
|
+
export declare class FilterBuilder {
|
|
5
|
+
private schema;
|
|
6
|
+
constructor(schema: Record<string, unknown>);
|
|
7
|
+
/**
|
|
8
|
+
* Builds dynamic WHERE conditions from user-provided filter params.
|
|
9
|
+
* Only allows filtering on columns explicitly marked as filterable.
|
|
10
|
+
*/
|
|
11
|
+
buildFilters(config: TableConfig, params: Record<string, FilterParam>): SQL | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Builds conditions for filters with type='static' (preset values in config).
|
|
14
|
+
*/
|
|
15
|
+
buildStaticFilters(config: TableConfig): SQL | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Resolves a column reference. Supports "joinedTable.column" dot-syntax
|
|
18
|
+
* for columns on joined tables.
|
|
19
|
+
*/
|
|
20
|
+
private resolveColumn;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=filterBuilder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filterBuilder.d.ts","sourceRoot":"","sources":["../../../src/core/filterBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,GAAG,EAGJ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAI9C,qBAAa,aAAa;IACZ,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEnD;;;OAGG;IACH,YAAY,CACV,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAClC,GAAG,GAAG,SAAS;IAyDlB;;OAEG;IACH,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,GAAG,SAAS;IAwBxD;;;OAGG;IACH,OAAO,CAAC,aAAa;CA4BtB"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getTableColumns, and, } from 'drizzle-orm';
|
|
2
|
+
import { applyOperator } from '../utils/operators';
|
|
3
|
+
import { isDatePreset, buildDatePresetCondition } from './datePresets';
|
|
4
|
+
export class FilterBuilder {
|
|
5
|
+
schema;
|
|
6
|
+
constructor(schema) {
|
|
7
|
+
this.schema = schema;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds dynamic WHERE conditions from user-provided filter params.
|
|
11
|
+
* Only allows filtering on columns explicitly marked as filterable.
|
|
12
|
+
*/
|
|
13
|
+
buildFilters(config, params) {
|
|
14
|
+
if (!params || Object.keys(params).length === 0) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const table = this.schema[config.base];
|
|
18
|
+
if (!table)
|
|
19
|
+
return undefined;
|
|
20
|
+
const columns = getTableColumns(table);
|
|
21
|
+
// Build a whitelist of filterable field names
|
|
22
|
+
const filterableFields = new Set();
|
|
23
|
+
// From explicit dynamic filter definitions
|
|
24
|
+
if (config.filters) {
|
|
25
|
+
for (const f of config.filters) {
|
|
26
|
+
if (f.type !== 'static') {
|
|
27
|
+
filterableFields.add(f.field);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// From columns marked filterable (default true)
|
|
32
|
+
for (const col of config.columns) {
|
|
33
|
+
if (col.filterable !== false) {
|
|
34
|
+
filterableFields.add(col.name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const conditions = [];
|
|
38
|
+
for (const [field, param] of Object.entries(params)) {
|
|
39
|
+
// Security: reject fields not in the whitelist
|
|
40
|
+
if (!filterableFields.has(field))
|
|
41
|
+
continue;
|
|
42
|
+
// Find the config for this field to resolve "field" property
|
|
43
|
+
const colConfig = config.columns.find(c => c.name === field);
|
|
44
|
+
const dbFieldName = colConfig?.field ?? field;
|
|
45
|
+
// Resolve the column — could be on the base table or a joined table
|
|
46
|
+
const col = this.resolveColumn(config, columns, dbFieldName);
|
|
47
|
+
if (!col)
|
|
48
|
+
continue;
|
|
49
|
+
// Check if value is a date preset
|
|
50
|
+
if (isDatePreset(param.value)) {
|
|
51
|
+
const presetCondition = buildDatePresetCondition(col, param.value);
|
|
52
|
+
if (presetCondition)
|
|
53
|
+
conditions.push(presetCondition);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const condition = applyOperator(param.operator, col, param.value);
|
|
57
|
+
if (condition)
|
|
58
|
+
conditions.push(condition);
|
|
59
|
+
}
|
|
60
|
+
return conditions.length > 0 ? and(...conditions) : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds conditions for filters with type='static' (preset values in config).
|
|
64
|
+
*/
|
|
65
|
+
buildStaticFilters(config) {
|
|
66
|
+
if (!config.filters)
|
|
67
|
+
return undefined;
|
|
68
|
+
const table = this.schema[config.base];
|
|
69
|
+
if (!table)
|
|
70
|
+
return undefined;
|
|
71
|
+
const columns = getTableColumns(table);
|
|
72
|
+
const conditions = [];
|
|
73
|
+
for (const filter of config.filters) {
|
|
74
|
+
if (filter.type !== 'static' || filter.value === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
const col = columns[filter.field];
|
|
77
|
+
if (!col)
|
|
78
|
+
continue;
|
|
79
|
+
// Default to 'eq' if not specified (though schema defaults it, z.input might miss it)
|
|
80
|
+
const op = filter.operator ?? 'eq';
|
|
81
|
+
const condition = applyOperator(op, col, filter.value);
|
|
82
|
+
if (condition)
|
|
83
|
+
conditions.push(condition);
|
|
84
|
+
}
|
|
85
|
+
return conditions.length > 0 ? and(...conditions) : undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolves a column reference. Supports "joinedTable.column" dot-syntax
|
|
89
|
+
* for columns on joined tables.
|
|
90
|
+
*/
|
|
91
|
+
resolveColumn(config, baseColumns, field) {
|
|
92
|
+
// Direct column on the base table
|
|
93
|
+
if (baseColumns[field]) {
|
|
94
|
+
return baseColumns[field];
|
|
95
|
+
}
|
|
96
|
+
// Dot-syntax for joined tables: "orders.total"
|
|
97
|
+
if (field.includes('.')) {
|
|
98
|
+
const [tableName, colName] = field.split('.');
|
|
99
|
+
const joinedTable = this.schema[tableName];
|
|
100
|
+
if (!joinedTable)
|
|
101
|
+
return undefined;
|
|
102
|
+
// Verify this table is actually joined
|
|
103
|
+
const isJoined = config.joins?.some((j) => j.table === tableName || j.alias === tableName);
|
|
104
|
+
if (!isJoined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const joinedCols = getTableColumns(joinedTable);
|
|
107
|
+
return joinedCols[colName];
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=filterBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filterBuilder.js","sourceRoot":"","sources":["../../../src/core/filterBuilder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SQL } from 'drizzle-orm';
|
|
2
|
+
import { FilterExpression, TableConfig } from '../types/table';
|
|
3
|
+
export declare class FilterGroupBuilder {
|
|
4
|
+
private schema;
|
|
5
|
+
constructor(schema: Record<string, unknown>);
|
|
6
|
+
/**
|
|
7
|
+
* Builds SQL from a filter expression tree.
|
|
8
|
+
* Supports arbitrary nesting of AND/OR groups.
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* { type: 'or', conditions: [
|
|
12
|
+
* { field: 'status', operator: 'eq', value: 'active' },
|
|
13
|
+
* { type: 'and', conditions: [
|
|
14
|
+
* { field: 'priority', operator: 'eq', value: 'high' },
|
|
15
|
+
* { field: 'total', operator: 'gt', value: 1000 },
|
|
16
|
+
* ]}
|
|
17
|
+
* ]}
|
|
18
|
+
* → (status = 'active' OR (priority = 'high' AND total > 1000))
|
|
19
|
+
*/
|
|
20
|
+
build(expression: FilterExpression, config: TableConfig): SQL | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Builds SQL from an array of filter expressions (top-level AND).
|
|
23
|
+
*/
|
|
24
|
+
buildAll(expressions: FilterExpression[], config: TableConfig): SQL | undefined;
|
|
25
|
+
private resolve;
|
|
26
|
+
private resolveCondition;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=filterGroupBuilder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filterGroupBuilder.d.ts","sourceRoot":"","sources":["../../../src/core/filterGroupBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,GAAG,EAIJ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAmB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGhF,qBAAa,kBAAkB;IACjB,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEnD;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,GAAG,GAAG,GAAG,SAAS;IAQzE;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,gBAAgB,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,GAAG,GAAG,SAAS;IAc/E,OAAO,CAAC,OAAO;IAqBf,OAAO,CAAC,gBAAgB;CAQzB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { getTableColumns, and, or, } from 'drizzle-orm';
|
|
2
|
+
import { applyOperator } from '../utils/operators';
|
|
3
|
+
export class FilterGroupBuilder {
|
|
4
|
+
schema;
|
|
5
|
+
constructor(schema) {
|
|
6
|
+
this.schema = schema;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds SQL from a filter expression tree.
|
|
10
|
+
* Supports arbitrary nesting of AND/OR groups.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* { type: 'or', conditions: [
|
|
14
|
+
* { field: 'status', operator: 'eq', value: 'active' },
|
|
15
|
+
* { type: 'and', conditions: [
|
|
16
|
+
* { field: 'priority', operator: 'eq', value: 'high' },
|
|
17
|
+
* { field: 'total', operator: 'gt', value: 1000 },
|
|
18
|
+
* ]}
|
|
19
|
+
* ]}
|
|
20
|
+
* → (status = 'active' OR (priority = 'high' AND total > 1000))
|
|
21
|
+
*/
|
|
22
|
+
build(expression, config) {
|
|
23
|
+
const table = this.schema[config.base];
|
|
24
|
+
if (!table)
|
|
25
|
+
return undefined;
|
|
26
|
+
const columns = getTableColumns(table);
|
|
27
|
+
return this.resolve(expression, columns);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Builds SQL from an array of filter expressions (top-level AND).
|
|
31
|
+
*/
|
|
32
|
+
buildAll(expressions, config) {
|
|
33
|
+
if (!expressions.length)
|
|
34
|
+
return undefined;
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const expr of expressions) {
|
|
37
|
+
const sql = this.build(expr, config);
|
|
38
|
+
if (sql)
|
|
39
|
+
parts.push(sql);
|
|
40
|
+
}
|
|
41
|
+
if (parts.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (parts.length === 1)
|
|
44
|
+
return parts[0];
|
|
45
|
+
return and(...parts);
|
|
46
|
+
}
|
|
47
|
+
resolve(expr, columns) {
|
|
48
|
+
// Leaf node: a single condition
|
|
49
|
+
if ('field' in expr && 'operator' in expr) {
|
|
50
|
+
return this.resolveCondition(expr, columns);
|
|
51
|
+
}
|
|
52
|
+
// Branch node: AND/OR group
|
|
53
|
+
const group = expr;
|
|
54
|
+
const parts = [];
|
|
55
|
+
for (const child of group.conditions) {
|
|
56
|
+
const childSql = this.resolve(child, columns);
|
|
57
|
+
if (childSql)
|
|
58
|
+
parts.push(childSql);
|
|
59
|
+
}
|
|
60
|
+
if (parts.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (parts.length === 1)
|
|
63
|
+
return parts[0];
|
|
64
|
+
return group.type === 'or' ? or(...parts) : and(...parts);
|
|
65
|
+
}
|
|
66
|
+
resolveCondition(condition, columns) {
|
|
67
|
+
const col = columns[condition.field];
|
|
68
|
+
if (!col)
|
|
69
|
+
return undefined;
|
|
70
|
+
return applyOperator(condition.operator, col, condition.value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=filterGroupBuilder.js.map
|