@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.
Files changed (237) hide show
  1. package/dist/src/__tests__/inputValidator.test.d.ts +2 -0
  2. package/dist/src/__tests__/inputValidator.test.d.ts.map +1 -0
  3. package/dist/src/__tests__/inputValidator.test.js +205 -0
  4. package/dist/src/__tests__/inputValidator.test.js.map +1 -0
  5. package/dist/src/__tests__/metadataBuilder.test.d.ts +2 -0
  6. package/dist/src/__tests__/metadataBuilder.test.d.ts.map +1 -0
  7. package/dist/src/__tests__/metadataBuilder.test.js +221 -0
  8. package/dist/src/__tests__/metadataBuilder.test.js.map +1 -0
  9. package/dist/src/core/aggregationBuilder.d.ts +17 -0
  10. package/dist/src/core/aggregationBuilder.d.ts.map +1 -0
  11. package/dist/src/core/aggregationBuilder.js +81 -0
  12. package/dist/src/core/aggregationBuilder.js.map +1 -0
  13. package/dist/src/core/cursorPagination.d.ts +36 -0
  14. package/dist/src/core/cursorPagination.d.ts.map +1 -0
  15. package/dist/src/core/cursorPagination.js +88 -0
  16. package/dist/src/core/cursorPagination.js.map +1 -0
  17. package/dist/src/core/datePresets.d.ts +19 -0
  18. package/dist/src/core/datePresets.d.ts.map +1 -0
  19. package/dist/src/core/datePresets.js +96 -0
  20. package/dist/src/core/datePresets.js.map +1 -0
  21. package/dist/src/core/dialect.d.ts +17 -0
  22. package/dist/src/core/dialect.d.ts.map +1 -0
  23. package/dist/src/core/dialect.js +60 -0
  24. package/dist/src/core/dialect.js.map +1 -0
  25. package/dist/src/core/fieldSelector.d.ts +19 -0
  26. package/dist/src/core/fieldSelector.d.ts.map +1 -0
  27. package/dist/src/core/fieldSelector.js +49 -0
  28. package/dist/src/core/fieldSelector.js.map +1 -0
  29. package/dist/src/core/filterBuilder.d.ts +22 -0
  30. package/dist/src/core/filterBuilder.d.ts.map +1 -0
  31. package/dist/src/core/filterBuilder.js +112 -0
  32. package/dist/src/core/filterBuilder.js.map +1 -0
  33. package/dist/src/core/filterGroupBuilder.d.ts +28 -0
  34. package/dist/src/core/filterGroupBuilder.d.ts.map +1 -0
  35. package/dist/src/core/filterGroupBuilder.js +73 -0
  36. package/dist/src/core/filterGroupBuilder.js.map +1 -0
  37. package/dist/src/core/groupByBuilder.d.ts +23 -0
  38. package/dist/src/core/groupByBuilder.d.ts.map +1 -0
  39. package/dist/src/core/groupByBuilder.js +127 -0
  40. package/dist/src/core/groupByBuilder.js.map +1 -0
  41. package/dist/src/core/inputValidator.d.ts +8 -0
  42. package/dist/src/core/inputValidator.d.ts.map +1 -0
  43. package/dist/src/core/inputValidator.js +117 -0
  44. package/dist/src/core/inputValidator.js.map +1 -0
  45. package/dist/src/core/metadataBuilder.d.ts +91 -0
  46. package/dist/src/core/metadataBuilder.d.ts.map +1 -0
  47. package/dist/src/core/metadataBuilder.js +220 -0
  48. package/dist/src/core/metadataBuilder.js.map +1 -0
  49. package/dist/src/core/paginationBuilder.d.ts +20 -0
  50. package/dist/src/core/paginationBuilder.d.ts.map +1 -0
  51. package/dist/src/core/paginationBuilder.js +42 -0
  52. package/dist/src/core/paginationBuilder.js.map +1 -0
  53. package/dist/src/core/queryBuilder.d.ts +20 -0
  54. package/dist/src/core/queryBuilder.d.ts.map +1 -0
  55. package/dist/src/core/queryBuilder.js +163 -0
  56. package/dist/src/core/queryBuilder.js.map +1 -0
  57. package/dist/src/core/recursiveBuilder.d.ts +25 -0
  58. package/dist/src/core/recursiveBuilder.d.ts.map +1 -0
  59. package/dist/src/core/recursiveBuilder.js +86 -0
  60. package/dist/src/core/recursiveBuilder.js.map +1 -0
  61. package/dist/src/core/relationBuilder.d.ts +19 -0
  62. package/dist/src/core/relationBuilder.d.ts.map +1 -0
  63. package/dist/src/core/relationBuilder.js +118 -0
  64. package/dist/src/core/relationBuilder.js.map +1 -0
  65. package/dist/src/core/roleFilter.d.ts +11 -0
  66. package/dist/src/core/roleFilter.d.ts.map +1 -0
  67. package/dist/src/core/roleFilter.js +24 -0
  68. package/dist/src/core/roleFilter.js.map +1 -0
  69. package/dist/src/core/searchBuilder.d.ts +17 -0
  70. package/dist/src/core/searchBuilder.d.ts.map +1 -0
  71. package/dist/src/core/searchBuilder.js +71 -0
  72. package/dist/src/core/searchBuilder.js.map +1 -0
  73. package/dist/src/core/softDelete.d.ts +12 -0
  74. package/dist/src/core/softDelete.d.ts.map +1 -0
  75. package/dist/src/core/softDelete.js +29 -0
  76. package/dist/src/core/softDelete.js.map +1 -0
  77. package/dist/src/core/sortBuilder.d.ts +14 -0
  78. package/dist/src/core/sortBuilder.d.ts.map +1 -0
  79. package/dist/src/core/sortBuilder.js +58 -0
  80. package/dist/src/core/sortBuilder.js.map +1 -0
  81. package/dist/src/core/subqueryBuilder.d.ts +13 -0
  82. package/dist/src/core/subqueryBuilder.d.ts.map +1 -0
  83. package/dist/src/core/subqueryBuilder.js +47 -0
  84. package/dist/src/core/subqueryBuilder.js.map +1 -0
  85. package/dist/src/core/validator.d.ts +18 -0
  86. package/dist/src/core/validator.d.ts.map +1 -0
  87. package/dist/src/core/validator.js +88 -0
  88. package/dist/src/core/validator.js.map +1 -0
  89. package/dist/src/define.d.ts +274 -0
  90. package/dist/src/define.d.ts.map +1 -0
  91. package/dist/src/define.js +690 -0
  92. package/dist/src/define.js.map +1 -0
  93. package/dist/src/engine.d.ts +17 -0
  94. package/dist/src/engine.d.ts.map +1 -0
  95. package/dist/src/engine.js +429 -0
  96. package/dist/src/engine.js.map +1 -0
  97. package/dist/src/errors.d.ts +53 -0
  98. package/dist/src/errors.d.ts.map +1 -0
  99. package/dist/src/errors.js +80 -0
  100. package/dist/src/errors.js.map +1 -0
  101. package/dist/src/index.d.ts +37 -0
  102. package/dist/src/index.d.ts.map +1 -0
  103. package/dist/src/index.js +41 -0
  104. package/dist/src/index.js.map +1 -0
  105. package/dist/src/types/engine.d.ts +92 -0
  106. package/dist/src/types/engine.d.ts.map +1 -0
  107. package/dist/src/types/engine.js +2 -0
  108. package/dist/src/types/engine.js.map +1 -0
  109. package/dist/src/types/table.d.ts +867 -0
  110. package/dist/src/types/table.d.ts.map +1 -0
  111. package/dist/src/types/table.js +198 -0
  112. package/dist/src/types/table.js.map +1 -0
  113. package/dist/src/utils/adapterUtils.d.ts +16 -0
  114. package/dist/src/utils/adapterUtils.d.ts.map +1 -0
  115. package/dist/src/utils/adapterUtils.js +28 -0
  116. package/dist/src/utils/adapterUtils.js.map +1 -0
  117. package/dist/src/utils/codegen.d.ts +7 -0
  118. package/dist/src/utils/codegen.d.ts.map +1 -0
  119. package/dist/src/utils/codegen.js +126 -0
  120. package/dist/src/utils/codegen.js.map +1 -0
  121. package/dist/src/utils/export.d.ts +15 -0
  122. package/dist/src/utils/export.d.ts.map +1 -0
  123. package/dist/src/utils/export.js +42 -0
  124. package/dist/src/utils/export.js.map +1 -0
  125. package/dist/src/utils/introspect.d.ts +32 -0
  126. package/dist/src/utils/introspect.d.ts.map +1 -0
  127. package/dist/src/utils/introspect.js +174 -0
  128. package/dist/src/utils/introspect.js.map +1 -0
  129. package/dist/src/utils/openapi.d.ts +6 -0
  130. package/dist/src/utils/openapi.d.ts.map +1 -0
  131. package/dist/src/utils/openapi.js +138 -0
  132. package/dist/src/utils/openapi.js.map +1 -0
  133. package/dist/src/utils/operators.d.ts +8 -0
  134. package/dist/src/utils/operators.d.ts.map +1 -0
  135. package/dist/src/utils/operators.js +70 -0
  136. package/dist/src/utils/operators.js.map +1 -0
  137. package/dist/src/utils/requestParser.d.ts +18 -0
  138. package/dist/src/utils/requestParser.d.ts.map +1 -0
  139. package/dist/src/utils/requestParser.js +126 -0
  140. package/dist/src/utils/requestParser.js.map +1 -0
  141. package/dist/src/utils/responseFormatter.d.ts +12 -0
  142. package/dist/src/utils/responseFormatter.d.ts.map +1 -0
  143. package/dist/src/utils/responseFormatter.js +106 -0
  144. package/dist/src/utils/responseFormatter.js.map +1 -0
  145. package/dist/src/utils/typedSql.d.ts +70 -0
  146. package/dist/src/utils/typedSql.d.ts.map +1 -0
  147. package/dist/src/utils/typedSql.js +102 -0
  148. package/dist/src/utils/typedSql.js.map +1 -0
  149. package/dist/test/columnMeta.test.d.ts +2 -0
  150. package/dist/test/columnMeta.test.d.ts.map +1 -0
  151. package/dist/test/columnMeta.test.js +92 -0
  152. package/dist/test/columnMeta.test.js.map +1 -0
  153. package/dist/test/core/aggregationBuilder.test.d.ts +2 -0
  154. package/dist/test/core/aggregationBuilder.test.d.ts.map +1 -0
  155. package/dist/test/core/aggregationBuilder.test.js +64 -0
  156. package/dist/test/core/aggregationBuilder.test.js.map +1 -0
  157. package/dist/test/core/filterBuilder.test.d.ts +2 -0
  158. package/dist/test/core/filterBuilder.test.d.ts.map +1 -0
  159. package/dist/test/core/filterBuilder.test.js +80 -0
  160. package/dist/test/core/filterBuilder.test.js.map +1 -0
  161. package/dist/test/core/paginationBuilder.test.d.ts +2 -0
  162. package/dist/test/core/paginationBuilder.test.d.ts.map +1 -0
  163. package/dist/test/core/paginationBuilder.test.js +63 -0
  164. package/dist/test/core/paginationBuilder.test.js.map +1 -0
  165. package/dist/test/core/queryBuilder.test.d.ts +2 -0
  166. package/dist/test/core/queryBuilder.test.d.ts.map +1 -0
  167. package/dist/test/core/queryBuilder.test.js +92 -0
  168. package/dist/test/core/queryBuilder.test.js.map +1 -0
  169. package/dist/test/core/searchBuilder.test.d.ts +2 -0
  170. package/dist/test/core/searchBuilder.test.d.ts.map +1 -0
  171. package/dist/test/core/searchBuilder.test.js +68 -0
  172. package/dist/test/core/searchBuilder.test.js.map +1 -0
  173. package/dist/test/core/softDelete.test.d.ts +2 -0
  174. package/dist/test/core/softDelete.test.d.ts.map +1 -0
  175. package/dist/test/core/softDelete.test.js +60 -0
  176. package/dist/test/core/softDelete.test.js.map +1 -0
  177. package/dist/test/core/sortBuilder.test.d.ts +2 -0
  178. package/dist/test/core/sortBuilder.test.d.ts.map +1 -0
  179. package/dist/test/core/sortBuilder.test.js +59 -0
  180. package/dist/test/core/sortBuilder.test.js.map +1 -0
  181. package/dist/test/core/subqueryBuilder.test.d.ts +2 -0
  182. package/dist/test/core/subqueryBuilder.test.d.ts.map +1 -0
  183. package/dist/test/core/subqueryBuilder.test.js +48 -0
  184. package/dist/test/core/subqueryBuilder.test.js.map +1 -0
  185. package/dist/test/core/validator.test.d.ts +2 -0
  186. package/dist/test/core/validator.test.d.ts.map +1 -0
  187. package/dist/test/core/validator.test.js +81 -0
  188. package/dist/test/core/validator.test.js.map +1 -0
  189. package/dist/test/datePresets.test.d.ts +2 -0
  190. package/dist/test/datePresets.test.d.ts.map +1 -0
  191. package/dist/test/datePresets.test.js +57 -0
  192. package/dist/test/datePresets.test.js.map +1 -0
  193. package/dist/test/errors.test.d.ts +2 -0
  194. package/dist/test/errors.test.d.ts.map +1 -0
  195. package/dist/test/errors.test.js +62 -0
  196. package/dist/test/errors.test.js.map +1 -0
  197. package/dist/test/inputValidator.test.d.ts +2 -0
  198. package/dist/test/inputValidator.test.d.ts.map +1 -0
  199. package/dist/test/inputValidator.test.js +60 -0
  200. package/dist/test/inputValidator.test.js.map +1 -0
  201. package/dist/test/metadata.test.d.ts +2 -0
  202. package/dist/test/metadata.test.d.ts.map +1 -0
  203. package/dist/test/metadata.test.js +285 -0
  204. package/dist/test/metadata.test.js.map +1 -0
  205. package/dist/test/metadataComplete.test.d.ts +2 -0
  206. package/dist/test/metadataComplete.test.d.ts.map +1 -0
  207. package/dist/test/metadataComplete.test.js +113 -0
  208. package/dist/test/metadataComplete.test.js.map +1 -0
  209. package/dist/test/roleFilter.test.d.ts +2 -0
  210. package/dist/test/roleFilter.test.d.ts.map +1 -0
  211. package/dist/test/roleFilter.test.js +53 -0
  212. package/dist/test/roleFilter.test.js.map +1 -0
  213. package/dist/test/typedSql.test.d.ts +2 -0
  214. package/dist/test/typedSql.test.d.ts.map +1 -0
  215. package/dist/test/typedSql.test.js +63 -0
  216. package/dist/test/typedSql.test.js.map +1 -0
  217. package/dist/test/utils/codegen.test.d.ts +2 -0
  218. package/dist/test/utils/codegen.test.d.ts.map +1 -0
  219. package/dist/test/utils/codegen.test.js +65 -0
  220. package/dist/test/utils/codegen.test.js.map +1 -0
  221. package/dist/test/utils/export.test.d.ts +2 -0
  222. package/dist/test/utils/export.test.d.ts.map +1 -0
  223. package/dist/test/utils/export.test.js +54 -0
  224. package/dist/test/utils/export.test.js.map +1 -0
  225. package/dist/test/utils/openapi.test.d.ts +2 -0
  226. package/dist/test/utils/openapi.test.d.ts.map +1 -0
  227. package/dist/test/utils/openapi.test.js +65 -0
  228. package/dist/test/utils/openapi.test.js.map +1 -0
  229. package/dist/test/utils/requestParser.test.d.ts +2 -0
  230. package/dist/test/utils/requestParser.test.d.ts.map +1 -0
  231. package/dist/test/utils/requestParser.test.js +89 -0
  232. package/dist/test/utils/requestParser.test.js.map +1 -0
  233. package/dist/test/utils/responseFormatter.test.d.ts +2 -0
  234. package/dist/test/utils/responseFormatter.test.d.ts.map +1 -0
  235. package/dist/test/utils/responseFormatter.test.js +79 -0
  236. package/dist/test/utils/responseFormatter.test.js.map +1 -0
  237. package/package.json +42 -0
@@ -0,0 +1,690 @@
1
+ import { getTableName, } from 'drizzle-orm';
2
+ import { introspectTable, getSensitiveColumnNames, detectSensitiveColumns, } from './utils/introspect';
3
+ function emptyExtensions() {
4
+ return {
5
+ computedExpressions: new Map(),
6
+ transforms: new Map(),
7
+ rawSelects: new Map(),
8
+ rawWheres: [],
9
+ rawJoins: [],
10
+ rawOrderBys: [],
11
+ ctes: new Map(),
12
+ sqlJoinConditions: new Map(),
13
+ };
14
+ }
15
+ // ── Builder ──
16
+ export class TableDefinitionBuilder {
17
+ _config;
18
+ _table;
19
+ _ext;
20
+ constructor(table, config) {
21
+ this._table = table;
22
+ this._config = config;
23
+ this._ext = emptyExtensions();
24
+ }
25
+ // ──── Column Format / Metadata ────
26
+ /** Set display format for a column */
27
+ format(column, fmt) {
28
+ const col = this._config.columns.find((c) => c.name === column);
29
+ if (col)
30
+ col.format = fmt;
31
+ return this;
32
+ }
33
+ /** Set column alignment */
34
+ align(column, alignment) {
35
+ const col = this._config.columns.find((c) => c.name === column);
36
+ if (col)
37
+ col.align = alignment;
38
+ return this;
39
+ }
40
+ /** Set column width (px) */
41
+ width(column, w, options) {
42
+ const col = this._config.columns.find((c) => c.name === column);
43
+ if (col) {
44
+ col.width = w;
45
+ if (options?.min)
46
+ col.minWidth = options.min;
47
+ if (options?.max)
48
+ col.maxWidth = options.max;
49
+ }
50
+ return this;
51
+ }
52
+ // ──── Enum Options ────
53
+ /**
54
+ * Declare the valid values for a column.
55
+ * Used by frontend to render dropdown filters.
56
+ * @example
57
+ * .options('status', [
58
+ * { value: 'active', label: 'Active', color: 'green' },
59
+ * { value: 'inactive', label: 'Inactive', color: 'gray' },
60
+ * { value: 'banned', label: 'Banned', color: 'red' },
61
+ * ])
62
+ */
63
+ options(column, opts) {
64
+ const col = this._config.columns.find((c) => c.name === column);
65
+ if (col)
66
+ col.options = opts;
67
+ return this;
68
+ }
69
+ // ──── Date Presets ────
70
+ /**
71
+ * Set which date presets are available for filtering.
72
+ * @example
73
+ * .datePresets('createdAt', ['today', 'last7days', 'thisMonth', 'custom'])
74
+ */
75
+ datePresets(column, presets) {
76
+ const col = this._config.columns.find((c) => c.name === column);
77
+ if (col)
78
+ col.datePresets = presets;
79
+ return this;
80
+ }
81
+ // ──── Role-Based Column Visibility ────
82
+ /**
83
+ * Restrict column visibility to specific roles.
84
+ * Columns without visibleTo are visible to everyone.
85
+ * @example
86
+ * .visibleTo('salary', ['admin', 'hr'])
87
+ * .visibleTo('internalNotes', ['admin'])
88
+ */
89
+ visibleTo(column, roles) {
90
+ const col = this._config.columns.find((c) => c.name === column);
91
+ if (col)
92
+ col.visibleTo = roles;
93
+ return this;
94
+ }
95
+ // ──── Column Visibility ────
96
+ hide(...columns) {
97
+ for (const name of columns) {
98
+ const col = this._config.columns.find((c) => c.name === name);
99
+ if (col)
100
+ col.hidden = true;
101
+ }
102
+ return this;
103
+ }
104
+ show(...columns) {
105
+ for (const name of columns) {
106
+ const col = this._config.columns.find((c) => c.name === name);
107
+ if (col)
108
+ col.hidden = false;
109
+ }
110
+ return this;
111
+ }
112
+ only(...columns) {
113
+ const keep = new Set(columns);
114
+ for (const col of this._config.columns) {
115
+ col.hidden = !keep.has(col.name);
116
+ }
117
+ return this;
118
+ }
119
+ autoHide() {
120
+ const sensitive = getSensitiveColumnNames();
121
+ for (const col of this._config.columns) {
122
+ if (sensitive.has(col.name))
123
+ col.hidden = true;
124
+ }
125
+ return this;
126
+ }
127
+ inspectSensitive() {
128
+ return detectSensitiveColumns(this._table);
129
+ }
130
+ // ──── Labels ────
131
+ label(column, lbl) {
132
+ const col = this._config.columns.find((c) => c.name === column);
133
+ if (col)
134
+ col.label = lbl;
135
+ return this;
136
+ }
137
+ labels(map) {
138
+ for (const [name, lbl] of Object.entries(map)) {
139
+ if (lbl)
140
+ this.label(name, lbl);
141
+ }
142
+ return this;
143
+ }
144
+ // ──── Search ────
145
+ search(...columns) {
146
+ // Cast to unknown first to avoid tuple/array type mismatch in strict mode
147
+ this._config.search = { fields: columns, enabled: true };
148
+ return this;
149
+ }
150
+ searchAll() {
151
+ const textCols = this._config.columns
152
+ .filter((c) => c.type === 'string' && !c.hidden)
153
+ .map((c) => c.name);
154
+ this._config.search = { fields: textCols, enabled: true };
155
+ return this;
156
+ }
157
+ noSearch() {
158
+ this._config.search = { fields: [], enabled: false };
159
+ return this;
160
+ }
161
+ // ──── Filtering ────
162
+ filter(...columns) {
163
+ const filterSet = new Set(columns);
164
+ for (const col of this._config.columns) {
165
+ col.filterable = filterSet.has(col.name);
166
+ }
167
+ return this;
168
+ }
169
+ staticFilter(field, operator, value) {
170
+ if (!this._config.filters)
171
+ this._config.filters = [];
172
+ this._config.filters.push({ field: field, operator, value, type: 'static' });
173
+ return this;
174
+ }
175
+ noFilter() {
176
+ for (const col of this._config.columns)
177
+ col.filterable = false;
178
+ return this;
179
+ }
180
+ // ──── OR Logic / Filter Groups ────
181
+ /**
182
+ * Add an OR group of conditions.
183
+ * @example .whereOr(
184
+ * { field: 'status', op: 'eq', value: 'active' },
185
+ * { field: 'priority', op: 'eq', value: 'high' },
186
+ * )
187
+ * → WHERE ... AND (status = 'active' OR priority = 'high')
188
+ */
189
+ whereOr(...conditions) {
190
+ if (!this._config.filterGroups)
191
+ this._config.filterGroups = [];
192
+ this._config.filterGroups.push({
193
+ type: 'or',
194
+ conditions: conditions.map((c) => ({
195
+ field: c.field,
196
+ operator: c.op,
197
+ value: c.value,
198
+ })),
199
+ });
200
+ return this;
201
+ }
202
+ /**
203
+ * Add a filter group with explicit AND/OR type.
204
+ * Supports nested groups for complex logic.
205
+ * @example .whereGroup('or', [
206
+ * { field: 'status', operator: 'eq', value: 'active' },
207
+ * { type: 'and', conditions: [
208
+ * { field: 'priority', operator: 'eq', value: 'high' },
209
+ * { field: 'total', operator: 'gt', value: 1000 },
210
+ * ]},
211
+ * ])
212
+ */
213
+ whereGroup(type, conditions) {
214
+ if (!this._config.filterGroups)
215
+ this._config.filterGroups = [];
216
+ this._config.filterGroups.push({ type, conditions });
217
+ return this;
218
+ }
219
+ // ──── Sorting ────
220
+ sort(...specs) {
221
+ this._config.defaultSort = specs.map(parseSortSpec);
222
+ return this;
223
+ }
224
+ sortable(...columns) {
225
+ const sortSet = new Set(columns);
226
+ for (const col of this._config.columns) {
227
+ col.sortable = sortSet.has(col.name);
228
+ }
229
+ return this;
230
+ }
231
+ noSort() {
232
+ for (const col of this._config.columns)
233
+ col.sortable = false;
234
+ this._config.defaultSort = undefined;
235
+ return this;
236
+ }
237
+ // ──── Pagination ────
238
+ pageSize(size, options) {
239
+ this._config.pagination = {
240
+ ...this._config.pagination,
241
+ defaultPageSize: size,
242
+ maxPageSize: options?.max ?? this._config.pagination?.maxPageSize ?? 100,
243
+ enabled: true,
244
+ };
245
+ return this;
246
+ }
247
+ noPagination() {
248
+ this._config.pagination = { defaultPageSize: 10, maxPageSize: 100, enabled: false };
249
+ return this;
250
+ }
251
+ // ──── Joins (accepts string OR SQL) ────
252
+ join(table, options) {
253
+ if (!this._config.joins)
254
+ this._config.joins = [];
255
+ const joinedName = getTableName(table);
256
+ const baseName = this._config.base;
257
+ const key = options?.alias ?? joinedName;
258
+ let onString = '';
259
+ if (options?.on) {
260
+ if (typeof options.on === 'string') {
261
+ // Shorthand: "customerId" → "base.customerId = joined.id"
262
+ if (!options.on.includes('=') && !options.on.includes('.')) {
263
+ onString = `${baseName}.${options.on} = ${joinedName}.id`;
264
+ }
265
+ else {
266
+ onString = options.on;
267
+ }
268
+ }
269
+ else {
270
+ // It's a SQL object. Store it in extensions map.
271
+ // We use a placeholder string for the config.
272
+ onString = `__SQL__:${key}`;
273
+ this._ext.sqlJoinConditions.set(key, options.on);
274
+ }
275
+ }
276
+ else {
277
+ // Default guess
278
+ onString = `${baseName}.${joinedName}Id = ${joinedName}.id`;
279
+ }
280
+ const joinConfig = {
281
+ table: joinedName,
282
+ type: options?.type ?? 'left',
283
+ on: onString,
284
+ ...(options?.alias && { alias: options.alias }),
285
+ ...(options?.columns && {
286
+ columns: options.columns.map((name) => ({
287
+ name,
288
+ type: 'string',
289
+ hidden: false,
290
+ sortable: true,
291
+ filterable: true,
292
+ })),
293
+ }),
294
+ };
295
+ this._config.joins.push(joinConfig);
296
+ return this;
297
+ }
298
+ // ──── Computed Columns ────
299
+ computed(name, expression, options) {
300
+ this._config.columns.push({
301
+ name,
302
+ type: options?.type ?? 'string',
303
+ label: options?.label ?? name,
304
+ hidden: false,
305
+ sortable: true,
306
+ filterable: false,
307
+ computed: true,
308
+ });
309
+ this._ext.computedExpressions.set(name, expression);
310
+ return this;
311
+ }
312
+ // ──── Backend Conditions ────
313
+ where(condition) {
314
+ if (!this._config.backendConditions)
315
+ this._config.backendConditions = [];
316
+ this._config.backendConditions.push({
317
+ field: condition.field,
318
+ operator: condition.op,
319
+ value: condition.value,
320
+ });
321
+ return this;
322
+ }
323
+ // ──── Transforms ────
324
+ dbTransform(column, ...transforms) {
325
+ const col = this._config.columns.find((c) => c.name === column);
326
+ if (col)
327
+ col.dbTransform = transforms;
328
+ return this;
329
+ }
330
+ jsTransform(column, ...transforms) {
331
+ const col = this._config.columns.find((c) => c.name === column);
332
+ if (col)
333
+ col.jsTransform = transforms;
334
+ return this;
335
+ }
336
+ transform(column, fn) {
337
+ this._ext.transforms.set(column, fn);
338
+ return this;
339
+ }
340
+ // ──── GROUP BY & HAVING ────
341
+ groupBy(...fields) {
342
+ if (!this._config.groupBy)
343
+ this._config.groupBy = { fields: [] };
344
+ this._config.groupBy.fields = fields;
345
+ return this;
346
+ }
347
+ having(alias, operator, value) {
348
+ if (!this._config.groupBy)
349
+ this._config.groupBy = { fields: [] };
350
+ if (!this._config.groupBy.having)
351
+ this._config.groupBy.having = [];
352
+ this._config.groupBy.having.push({ alias, operator, value });
353
+ return this;
354
+ }
355
+ // ──── Aggregations ────
356
+ aggregate(alias, type, field) {
357
+ if (!this._config.aggregations)
358
+ this._config.aggregations = [];
359
+ this._config.aggregations.push({ alias, type, field: field });
360
+ return this;
361
+ }
362
+ // ──── Nested Relations (Includes) ────
363
+ include(table, options) {
364
+ if (!this._config.include)
365
+ this._config.include = [];
366
+ const includeConfig = {
367
+ table: getTableName(table),
368
+ foreignKey: options.foreignKey,
369
+ localKey: options.localKey ?? 'id',
370
+ as: options.as,
371
+ columns: options.columns,
372
+ limit: options.limit,
373
+ where: options.where?.map((w) => ({
374
+ field: w.field,
375
+ operator: w.op,
376
+ value: w.value,
377
+ })),
378
+ orderBy: options.orderBy?.map(parseSortSpec),
379
+ };
380
+ this._config.include.push(includeConfig);
381
+ return this;
382
+ }
383
+ // ──── Recursive Queries (CTE) ────
384
+ recursive(options) {
385
+ this._config.recursive = {
386
+ parentKey: options.parentKey,
387
+ childKey: options.childKey ?? 'id',
388
+ maxDepth: options.maxDepth ?? 10,
389
+ depthAlias: options.depthAlias ?? 'depth',
390
+ pathAlias: options.pathAlias,
391
+ startWith: options.startWith
392
+ ? {
393
+ field: options.startWith.field,
394
+ operator: options.startWith.op,
395
+ value: options.startWith.value,
396
+ }
397
+ : undefined,
398
+ };
399
+ return this;
400
+ }
401
+ // ──── Column Metadata (universal) ────
402
+ /**
403
+ * Enrich any column's metadata — works on base columns, join columns,
404
+ * computed columns, rawSelect columns, or any column by name.
405
+ *
406
+ * Use this to describe raw SQL columns to the frontend, or override
407
+ * any auto-detected metadata.
408
+ *
409
+ * @example
410
+ * .rawSelect('revenue', sql`SUM(orders.total)`)
411
+ * .columnMeta('revenue', {
412
+ * type: 'number',
413
+ * label: 'Total Revenue',
414
+ * format: 'currency',
415
+ * align: 'right',
416
+ * filterable: true,
417
+ * sortable: true,
418
+ * options: [
419
+ * { value: 'high', label: 'High (>$1000)', color: 'green' },
420
+ * ],
421
+ * })
422
+ */
423
+ columnMeta(column, meta) {
424
+ // Find in base columns first
425
+ let col = this._config.columns.find((c) => c.name === column);
426
+ // Then search join columns
427
+ if (!col && this._config.joins) {
428
+ col = this._findJoinColumn(this._config.joins, column);
429
+ }
430
+ if (!col) {
431
+ // Column doesn't exist yet — this is fine for rawSelect/computed that
432
+ // haven't been registered yet. Create a placeholder.
433
+ col = {
434
+ name: column,
435
+ type: meta.type ?? 'string',
436
+ hidden: false,
437
+ computed: true,
438
+ };
439
+ this._config.columns.push(col);
440
+ }
441
+ // Apply all provided metadata
442
+ if (meta.type !== undefined)
443
+ col.type = meta.type;
444
+ if (meta.label !== undefined)
445
+ col.label = meta.label;
446
+ if (meta.sortable !== undefined)
447
+ col.sortable = meta.sortable;
448
+ if (meta.filterable !== undefined)
449
+ col.filterable = meta.filterable;
450
+ if (meta.hidden !== undefined)
451
+ col.hidden = meta.hidden;
452
+ if (meta.format !== undefined)
453
+ col.format = meta.format;
454
+ if (meta.align !== undefined)
455
+ col.align = meta.align;
456
+ if (meta.width !== undefined)
457
+ col.width = meta.width;
458
+ if (meta.minWidth !== undefined)
459
+ col.minWidth = meta.minWidth;
460
+ if (meta.maxWidth !== undefined)
461
+ col.maxWidth = meta.maxWidth;
462
+ if (meta.options !== undefined)
463
+ col.options = meta.options;
464
+ if (meta.datePresets !== undefined)
465
+ col.datePresets = meta.datePresets;
466
+ if (meta.visibleTo !== undefined)
467
+ col.visibleTo = meta.visibleTo;
468
+ return this;
469
+ }
470
+ /** Find a column inside the join tree by name */
471
+ _findJoinColumn(joins, name) {
472
+ for (const join of joins) {
473
+ if (join.columns) {
474
+ const col = join.columns.find((c) => c.name === name);
475
+ if (col)
476
+ return col;
477
+ }
478
+ if (join.joins) {
479
+ const found = this._findJoinColumn(join.joins, name);
480
+ if (found)
481
+ return found;
482
+ }
483
+ }
484
+ return undefined;
485
+ }
486
+ // ──── Raw SQL Escape Hatches ────
487
+ /**
488
+ * Add a raw SQL expression as a select column.
489
+ * By default typed as 'string'. Use the options parameter or
490
+ * chain `.columnMeta()` to set the correct type, label, format, etc.
491
+ *
492
+ * @example
493
+ * // Basic
494
+ * .rawSelect('revenue', sql`SUM(orders.total)`)
495
+ *
496
+ * // With inline metadata
497
+ * .rawSelect('revenue', sql`SUM(orders.total)`, {
498
+ * type: 'number', label: 'Revenue', format: 'currency',
499
+ * })
500
+ *
501
+ * // Or chain .columnMeta() for full control
502
+ * .rawSelect('revenue', sql`SUM(orders.total)`)
503
+ * .columnMeta('revenue', { type: 'number', label: 'Revenue', format: 'currency' })
504
+ */
505
+ rawSelect(alias, sqlExpr, options) {
506
+ this._ext.rawSelects.set(alias, sqlExpr);
507
+ // Register as a computed column with metadata
508
+ this._config.columns.push({
509
+ name: alias,
510
+ type: options?.type ?? 'string',
511
+ label: options?.label,
512
+ hidden: options?.hidden ?? false,
513
+ sortable: options?.sortable ?? true,
514
+ filterable: options?.filterable ?? false,
515
+ computed: true,
516
+ ...(options?.format && { format: options.format }),
517
+ ...(options?.align && { align: options.align }),
518
+ ...(options?.width && { width: options.width }),
519
+ ...(options?.options && { options: options.options }),
520
+ ...(options?.visibleTo && { visibleTo: options.visibleTo }),
521
+ });
522
+ return this;
523
+ }
524
+ rawWhere(sqlExpr) {
525
+ this._ext.rawWheres.push(sqlExpr);
526
+ return this;
527
+ }
528
+ rawJoin(sqlExpr) {
529
+ this._ext.rawJoins.push(sqlExpr);
530
+ return this;
531
+ }
532
+ rawOrderBy(sqlExpr) {
533
+ this._ext.rawOrderBys.push(sqlExpr);
534
+ return this;
535
+ }
536
+ cte(name, sqlExpr) {
537
+ this._ext.ctes.set(name, sqlExpr);
538
+ return this;
539
+ }
540
+ // ──── Subqueries ────
541
+ subquery(alias, table, type, filter) {
542
+ if (!this._config.subqueries)
543
+ this._config.subqueries = [];
544
+ this._config.subqueries.push({
545
+ alias,
546
+ table: getTableName(table),
547
+ type,
548
+ filter,
549
+ });
550
+ return this;
551
+ }
552
+ // ──── Platform Features ────
553
+ softDelete(field) {
554
+ this._config.softDelete = {
555
+ field: field ?? this._config.softDelete?.field ?? 'deletedAt',
556
+ enabled: true,
557
+ };
558
+ return this;
559
+ }
560
+ tenant(field) {
561
+ this._config.tenant = {
562
+ field: field ?? this._config.tenant?.field ?? 'tenantId',
563
+ enabled: true,
564
+ };
565
+ return this;
566
+ }
567
+ exportable(...formats) {
568
+ this._config.export = {
569
+ formats: formats.length > 0 ? formats : ['csv', 'json'],
570
+ enabled: true,
571
+ };
572
+ return this;
573
+ }
574
+ access(options) {
575
+ this._config.access = options;
576
+ return this;
577
+ }
578
+ // ──── Name Override ────
579
+ as(name) {
580
+ this._config.name = name;
581
+ return this;
582
+ }
583
+ // ──── Count Mode ────
584
+ /**
585
+ * Control how row counting works.
586
+ * 'exact' = SELECT COUNT(*) — accurate but slow on large tables
587
+ * 'estimated' = PostgreSQL's reltuples — fast but approximate
588
+ * 'none' = skip counting entirely — fastest
589
+ */
590
+ countMode(mode) {
591
+ if (!this._config._countMode) {
592
+ this._config._countMode = mode;
593
+ }
594
+ this._config._countMode = mode;
595
+ return this;
596
+ }
597
+ // ──── DISTINCT ────
598
+ /** Enable DISTINCT on queries */
599
+ distinct() {
600
+ this._config._distinct = true;
601
+ return this;
602
+ }
603
+ // ──── Hooks ────
604
+ /** Add a hook that runs before every query */
605
+ beforeQuery(fn) {
606
+ this._ext.hooks = this._ext.hooks ?? {};
607
+ this._ext.hooks.beforeQuery = fn;
608
+ return this;
609
+ }
610
+ /** Add a hook that runs after every query */
611
+ afterQuery(fn) {
612
+ this._ext.hooks = this._ext.hooks ?? {};
613
+ this._ext.hooks.afterQuery = fn;
614
+ return this;
615
+ }
616
+ /** Add an error handler */
617
+ onError(fn) {
618
+ this._ext.hooks = this._ext.hooks ?? {};
619
+ this._ext.hooks.onError = fn;
620
+ return this;
621
+ }
622
+ // ──── Output ────
623
+ toConfig() {
624
+ return { ...this._config };
625
+ }
626
+ }
627
+ // ── Main Entry ──
628
+ export function defineTable(table, options) {
629
+ if (options && 'columns' in options && Array.isArray(options.columns)) {
630
+ return new TableDefinitionBuilder(table, options);
631
+ }
632
+ const config = introspectTable(table);
633
+ if (options) {
634
+ applyQuickOptions(config, options);
635
+ }
636
+ return new TableDefinitionBuilder(table, config);
637
+ }
638
+ // ── Helpers ──
639
+ function applyQuickOptions(config, options) {
640
+ if (options.hide) {
641
+ for (const name of options.hide) {
642
+ const col = config.columns.find((c) => c.name === name);
643
+ if (col)
644
+ col.hidden = true;
645
+ }
646
+ }
647
+ if (options.search) {
648
+ config.search = { fields: options.search, enabled: true };
649
+ }
650
+ if (options.filter) {
651
+ const filterSet = new Set(options.filter);
652
+ for (const col of config.columns) {
653
+ col.filterable = filterSet.has(col.name);
654
+ }
655
+ }
656
+ if (options.sort) {
657
+ const specs = options.sort.split(',').map((s) => s.trim()).filter(Boolean);
658
+ config.defaultSort = specs.map(parseSortSpec);
659
+ }
660
+ if (options.pageSize !== undefined) {
661
+ config.pagination = {
662
+ ...config.pagination,
663
+ defaultPageSize: options.pageSize,
664
+ maxPageSize: options.maxPageSize ?? config.pagination?.maxPageSize ?? 100,
665
+ enabled: true,
666
+ };
667
+ }
668
+ if (options.maxPageSize !== undefined && config.pagination) {
669
+ config.pagination.maxPageSize = options.maxPageSize;
670
+ }
671
+ if (options.labels) {
672
+ for (const [name, label] of Object.entries(options.labels)) {
673
+ if (!label)
674
+ continue;
675
+ const col = config.columns.find((c) => c.name === name);
676
+ if (col)
677
+ col.label = label;
678
+ }
679
+ }
680
+ }
681
+ function parseSortSpec(spec) {
682
+ if (spec.startsWith('-')) {
683
+ return { field: String(spec.slice(1)), order: 'desc' };
684
+ }
685
+ if (spec.startsWith('+')) {
686
+ return { field: String(spec.slice(1)), order: 'asc' };
687
+ }
688
+ return { field: String(spec), order: 'asc' };
689
+ }
690
+ //# sourceMappingURL=define.js.map