thatcher 1.0.4

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. package/src/ui/widgets.js +152 -0
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Query Engine - Read operations
3
+ * Adapted from moonlanding/src/lib/query-engine.js
4
+ */
5
+
6
+ import { getDatabase } from './database-core.js';
7
+ import { getSpec } from '../config/spec-helpers.js';
8
+ import { RECORD_STATUS } from '../config/constants.js';
9
+
10
+ const db = getDatabase();
11
+ const logger = createLogger('[QueryEngine]');
12
+
13
+ /**
14
+ * Execute a query with error handling
15
+ * @param {string} sql
16
+ * @param {Array} params
17
+ * @param {object} context
18
+ * @returns {Array}
19
+ */
20
+ function execQuery(sql, params = [], context = {}) {
21
+ try {
22
+ return db.prepare(sql).all(...params);
23
+ } catch (e) {
24
+ logger.error(`${context.operation || 'Query'} ${context.entity || ''}`, { sql, error: e.message });
25
+ throw new Error(`Database query failed: ${e.message}`);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Execute a single-row query
31
+ * @param {string} sql
32
+ * @param {Array} params
33
+ * @param {object} context
34
+ * @returns {object|null}
35
+ */
36
+ function execGet(sql, params = [], context = {}) {
37
+ try {
38
+ return db.prepare(sql).get(...params);
39
+ } catch (e) {
40
+ logger.error(`${context.operation || 'Get'} ${context.entity || ''}`, { sql, error: e.message });
41
+ throw new Error(`Database get failed: ${e.message}`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get table name for entity (users vs user)
47
+ * @param {object} spec
48
+ * @returns {string}
49
+ */
50
+ function tableName(spec) {
51
+ return spec.name === 'user' ? 'users' : spec.name;
52
+ }
53
+
54
+ /**
55
+ * Build SELECT query with joins, where, sorting, pagination
56
+ * @param {object} spec - Entity specification
57
+ * @param {object} where - Where conditions
58
+ * @param {object} options - Query options
59
+ * @returns {{sql: string, params: Array}}
60
+ */
61
+ function buildSpecQuery(spec, where = {}, options = {}) {
62
+ const tbl = tableName(spec);
63
+ const table = `"${tbl}"`;
64
+ const selects = [`${table}.*`];
65
+ const joins = [];
66
+
67
+ // Add computed fields
68
+ if (spec.computed) {
69
+ Object.entries(spec.computed).forEach(([k, c]) => {
70
+ selects.push(`${c.sql} as "${k}"`);
71
+ });
72
+ }
73
+
74
+ // Add joins for ref fields with display
75
+ Object.entries(spec.fields || {}).forEach(([k, f]) => {
76
+ if (f.type === 'ref' && f.display) {
77
+ const refTbl = f.ref === 'user' ? 'users' : f.ref;
78
+ const alias = `"${refTbl}_${k}"`;
79
+ joins.push(`LEFT JOIN "${refTbl}" ${alias} ON ${table}."${k}" = ${alias}.id`);
80
+
81
+ const displayField = f.display.split('.')[1] || 'name';
82
+ selects.push(`${alias}."${displayField}" as "${k}_display"`);
83
+ }
84
+ });
85
+
86
+ const wc = [];
87
+ const p = [];
88
+
89
+ // Where conditions
90
+ Object.entries(where).forEach(([k, v]) => {
91
+ if (v !== undefined && v !== null) {
92
+ wc.push(`${table}."${k}" = ?`);
93
+ p.push(v);
94
+ }
95
+ });
96
+
97
+ // Exclude soft-deleted by default
98
+ if (spec.fields?.status && !where.status && !options.includeDeleted) {
99
+ wc.push(`${table}."status" != '${RECORD_STATUS.DELETED}'`);
100
+ }
101
+ if (spec.fields?.archived && !where.archived && !options.includeArchived) {
102
+ wc.push(`${table}."archived" = 0`);
103
+ }
104
+
105
+ let sql = `SELECT ${selects.join(', ')} FROM ${table}`;
106
+ if (joins.length) {
107
+ sql += ' ' + joins.join(' ');
108
+ }
109
+ if (wc.length) {
110
+ sql += ` WHERE ` + wc.join(` AND `);
111
+ }
112
+
113
+ // Sorting
114
+ const sort = options.sort || spec.list?.defaultSort;
115
+ if (sort && sort.field && spec.fields?.[sort.field]) {
116
+ const dir = (sort.dir || 'ASC').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
117
+ sql += ` ORDER BY ${table}."${sort.field}" ${dir}`;
118
+ }
119
+
120
+ // Limit/offset
121
+ if (options.limit) {
122
+ sql += ` LIMIT ${parseInt(options.limit, 10)}`;
123
+ if (options.offset) {
124
+ sql += ` OFFSET ${parseInt(options.offset, 10)}`;
125
+ }
126
+ }
127
+
128
+ return { sql, params: p };
129
+ }
130
+
131
+ /**
132
+ * Get total count for entity (optionally with where)
133
+ * @param {string} entity
134
+ * @param {object} where
135
+ * @param {object} options
136
+ * @returns {number}
137
+ */
138
+ export function count(entity, where = {}, options = {}) {
139
+ const spec = getSpec(entity);
140
+ const tbl = tableName(spec);
141
+ const table = `"${tbl}"`;
142
+ const wc = [];
143
+ const p = [];
144
+
145
+ Object.entries(where).forEach(([k, v]) => {
146
+ if (v !== undefined && v !== null) {
147
+ wc.push(`${table}."${k}" = ?`);
148
+ p.push(v);
149
+ }
150
+ });
151
+
152
+ if (spec.fields?.status && !where.status && !options.includeDeleted) {
153
+ wc.push(`${table}."status" != '${RECORD_STATUS.DELETED}'`);
154
+ }
155
+
156
+ const whereClause = wc.length ? `WHERE ${wc.join(' AND ')}` : '';
157
+ const sql = `SELECT COUNT(*) as cnt FROM ${table} ${whereClause}`;
158
+
159
+ const result = execGet(sql, p, { entity, operation: 'Count' });
160
+ return result?.cnt || 0;
161
+ }
162
+
163
+ /**
164
+ * List all records (with optional where)
165
+ * @param {string} entity
166
+ * @param {object} where
167
+ * @param {object} options
168
+ * @returns {Array}
169
+ */
170
+ export function list(entity, where = {}, options = {}) {
171
+ const spec = getSpec(entity);
172
+ const { sql, params } = buildSpecQuery(spec, where, options);
173
+ return execQuery(sql, params, { entity, operation: 'List' });
174
+ }
175
+
176
+ /**
177
+ * List with pagination
178
+ * @param {string} entity
179
+ * @param {object} where
180
+ * @param {number} page
181
+ * @param {number|null} pageSize
182
+ * @returns {{items: Array, pagination: object}}
183
+ */
184
+ export async function listWithPagination(entity, where = {}, page = 1, pageSize = null) {
185
+ const spec = getSpec(entity);
186
+ const tbl = tableName(spec);
187
+
188
+ const paginationCfg = getPaginationConfig(spec);
189
+ const defaultPageSize = spec.list?.pageSize || paginationCfg.default_page_size;
190
+ const finalPageSize = pageSize || defaultPageSize;
191
+ const finalPage = Math.max(1, page);
192
+
193
+ const offset = (finalPage - 1) * finalPageSize;
194
+
195
+ const { sql, params } = buildSpecQuery(spec, where, {
196
+ ...options,
197
+ limit: finalPageSize,
198
+ offset,
199
+ });
200
+
201
+ const items = execQuery(sql, params, { entity, operation: 'ListWithPagination' });
202
+
203
+ const total = count(entity, where, options);
204
+
205
+ return {
206
+ items,
207
+ pagination: {
208
+ page: finalPage,
209
+ pageSize: finalPageSize,
210
+ total,
211
+ totalPages: Math.ceil(total / finalPageSize),
212
+ },
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Get single record by ID
218
+ * @param {string} entity
219
+ * @param {string|number} id
220
+ * @returns {object|null}
221
+ */
222
+ export function get(entity, id) {
223
+ const spec = getSpec(entity);
224
+ const tbl = tableName(spec);
225
+ const sql = `SELECT * FROM "${tbl}" WHERE id = ?`;
226
+ return execGet(sql, [id], { entity, operation: 'Get' });
227
+ }
228
+
229
+ /**
230
+ * Get single record by field value
231
+ * @param {string} entity
232
+ * @param {string} field
233
+ * @param {any} value
234
+ * @returns {object|null}
235
+ */
236
+ export function getBy(entity, field, value) {
237
+ const spec = getSpec(entity);
238
+ const tbl = tableName(spec);
239
+ const sql = `SELECT * FROM "${tbl}" WHERE "${field}" = ? LIMIT 1`;
240
+ return execGet(sql, [value], { entity, operation: 'GetBy' });
241
+ }
242
+
243
+ /**
244
+ * Search using FTS
245
+ * @param {string} entity
246
+ * @param {string} query
247
+ * @param {object} where
248
+ * @param {object} options
249
+ * @returns {Array}
250
+ */
251
+ export function search(entity, query, where = {}, options = {}) {
252
+ const spec = getSpec(entity);
253
+ const tbl = tableName(spec);
254
+ const ftsTable = `${tbl}_fts`;
255
+
256
+ // Check if FTS table exists
257
+ const ftsExists = db.prepare(`
258
+ SELECT name FROM sqlite_master WHERE type='table' AND name=?
259
+ `).get(ftsTable);
260
+
261
+ if (!ftsExists) {
262
+ // Fallback to simple LIKE search
263
+ const { sql, params } = buildSpecQuery(spec, where, options);
264
+ const baseSql = sql.replace('SELECT *', `SELECT *`);
265
+ const searchTerm = `%${query}%`;
266
+ const searchFields = Object.keys(spec.fields || {}).filter(
267
+ f => ['text', 'textarea', 'email'].includes(spec.fields[f].type)
268
+ );
269
+
270
+ if (searchFields.length > 0) {
271
+ const conditions = searchFields.map(f => `"${f}" LIKE ?`).join(' OR ');
272
+ const whereAnd = sql.includes('WHERE') ? ' AND ' : ' WHERE ';
273
+ const fullSql = baseSql + whereAnd + `(${conditions})`;
274
+ const searchParams = [...params, ...searchFields.map(() => searchTerm)];
275
+ return execQuery(fullSql, searchParams, { entity, operation: 'SearchFallback' });
276
+ }
277
+
278
+ return [];
279
+ }
280
+
281
+ // Use FTS
282
+ const ftsResults = db.prepare(`
283
+ SELECT rowid as id FROM ${ftsTable}
284
+ WHERE ${ftsTable} MATCH ?
285
+ `).all(query);
286
+
287
+ if (!ftsResults.length) return [];
288
+
289
+ const ids = ftsResults.map(r => r.id);
290
+ const idPlaceholders = ids.map(() => '?').join(',');
291
+ const { sql, params } = buildSpecQuery(spec, { ...where, id: { $in: ids } }, options);
292
+ const finalSql = sql.replace('WHERE', `WHERE "${tbl}"."id" IN (${idPlaceholders}) AND `);
293
+
294
+ return execQuery(finalSql, [...ids, ...params], { entity, operation: 'Search' });
295
+ }
296
+
297
+ /**
298
+ * Search with pagination
299
+ * @param {string} entity
300
+ * @param {string} query
301
+ * @param {object} where
302
+ * @param {number} page
303
+ * @param {number} pageSize
304
+ * @returns {{items: Array, pagination: object}}
305
+ */
306
+ export async function searchWithPagination(entity, query, where = {}, page = 1, pageSize = null) {
307
+ const spec = getSpec(entity);
308
+ const paginationCfg = getPaginationConfig(spec);
309
+ const defaultPageSize = spec.list?.pageSize || paginationCfg.default_page_size;
310
+ const finalPageSize = pageSize || defaultPageSize;
311
+ const finalPage = Math.max(1, page);
312
+
313
+ const items = search(entity, query, where, {
314
+ limit: finalPageSize,
315
+ offset: (finalPage - 1) * finalPageSize,
316
+ });
317
+
318
+ // For total count, we'd need a separate FTS count query - simplified here
319
+ const total = items.length; // Approximate
320
+
321
+ return {
322
+ items,
323
+ pagination: {
324
+ page: finalPage,
325
+ pageSize: finalPageSize,
326
+ total,
327
+ totalPages: Math.ceil(total / finalPageSize),
328
+ },
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Get children of a parent entity
334
+ * @param {string} parentEntity
335
+ * @param {string|number} parentId
336
+ * @param {object} childDef
337
+ * @returns {Array}
338
+ */
339
+ export function getChildren(parentEntity, parentId, childDef) {
340
+ const childSpec = getSpec(childDef.entity);
341
+ const childTbl = tableName(childSpec);
342
+ const fk = childDef.fk || `${parentEntity}_id`;
343
+
344
+ const sql = `SELECT * FROM "${childTbl}" WHERE "${fk}" = ? AND status != 'deleted'`;
345
+ return execQuery(sql, [parentId], { entity: childDef.entity, operation: 'GetChildren' });
346
+ }
347
+
348
+ /**
349
+ * Get pagination config from system settings
350
+ * @param {object} spec - Entity spec
351
+ * @returns {{default_page_size: number, max_page_size: number}}
352
+ */
353
+ function getPaginationConfig(spec) {
354
+ // Try to get from config engine, fallback to defaults
355
+ try {
356
+ // Will be provided by caller or fetched from global config
357
+ return {
358
+ default_page_size: spec.list?.pageSize || 50,
359
+ max_page_size: 500,
360
+ };
361
+ } catch {
362
+ return { default_page_size: 50, max_page_size: 500 };
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Simple logger
368
+ */
369
+ function createLogger(prefix) {
370
+ return {
371
+ error: (msg, meta = {}) => {
372
+ console.error(`${prefix} ${msg}`, meta);
373
+ },
374
+ warn: (msg, meta = {}) => {
375
+ console.warn(`${prefix} ${msg}`, meta);
376
+ },
377
+ info: (msg, meta = {}) => {
378
+ console.info(`${prefix} ${msg}`, meta);
379
+ },
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Run in transaction
385
+ * @param {Function} callback
386
+ * @returns {Promise<any>}
387
+ */
388
+ export async function withTransaction(callback) {
389
+ const dbInstance = getDatabase();
390
+ try {
391
+ dbInstance.prepare('BEGIN').run();
392
+ const result = await callback();
393
+ dbInstance.prepare('COMMIT').run();
394
+ return result;
395
+ } catch (e) {
396
+ dbInstance.prepare('ROLLBACK').run();
397
+ throw e;
398
+ }
399
+ }
@@ -0,0 +1,117 @@
1
+ const queryMetrics = new Map();
2
+ const slowQueries = [];
3
+ const MAX_SLOW_QUERIES = 100;
4
+ const SLOW_QUERY_THRESHOLD = 100;
5
+
6
+ export const trackQuery = (sql, duration, params = []) => {
7
+ const key = sql.substring(0, 200);
8
+
9
+ if (!queryMetrics.has(key)) {
10
+ queryMetrics.set(key, {
11
+ sql: key,
12
+ count: 0,
13
+ totalTime: 0,
14
+ minTime: Infinity,
15
+ maxTime: 0,
16
+ times: []
17
+ });
18
+ }
19
+
20
+ const metric = queryMetrics.get(key);
21
+ metric.count++;
22
+ metric.totalTime += duration;
23
+ metric.minTime = Math.min(metric.minTime, duration);
24
+ metric.maxTime = Math.max(metric.maxTime, duration);
25
+ metric.times.push(duration);
26
+
27
+ if (metric.times.length > 1000) metric.times.shift();
28
+
29
+ if (duration >= SLOW_QUERY_THRESHOLD) {
30
+ slowQueries.push({
31
+ sql: key,
32
+ duration,
33
+ params: params.slice(0, 10),
34
+ timestamp: Date.now()
35
+ });
36
+
37
+ if (slowQueries.length > MAX_SLOW_QUERIES) slowQueries.shift();
38
+ }
39
+ };
40
+
41
+ const percentile = (arr, p) => {
42
+ if (arr.length === 0) return 0;
43
+ const sorted = [...arr].sort((a, b) => a - b);
44
+ const index = Math.ceil(sorted.length * p / 100) - 1;
45
+ return sorted[Math.max(0, index)];
46
+ };
47
+
48
+ export const getMetrics = () => {
49
+ const metrics = [];
50
+
51
+ for (const [, metric] of queryMetrics) {
52
+ const avgTime = metric.count > 0 ? metric.totalTime / metric.count : 0;
53
+ const p50 = percentile(metric.times, 50);
54
+ const p95 = percentile(metric.times, 95);
55
+ const p99 = percentile(metric.times, 99);
56
+
57
+ metrics.push({
58
+ sql: metric.sql,
59
+ count: metric.count,
60
+ avgTime: avgTime.toFixed(2),
61
+ minTime: metric.minTime === Infinity ? 0 : metric.minTime.toFixed(2),
62
+ maxTime: metric.maxTime.toFixed(2),
63
+ p50: p50.toFixed(2),
64
+ p95: p95.toFixed(2),
65
+ p99: p99.toFixed(2),
66
+ totalTime: metric.totalTime.toFixed(2)
67
+ });
68
+ }
69
+
70
+ return metrics.sort((a, b) => parseFloat(b.totalTime) - parseFloat(a.totalTime));
71
+ };
72
+
73
+ export const getSlowQueries = () => {
74
+ return slowQueries.slice().reverse();
75
+ };
76
+
77
+ export const getSummary = () => {
78
+ const metrics = getMetrics();
79
+ const totalQueries = metrics.reduce((sum, m) => sum + m.count, 0);
80
+ const totalTime = metrics.reduce((sum, m) => sum + parseFloat(m.totalTime), 0);
81
+ const allTimes = [];
82
+
83
+ for (const [, metric] of queryMetrics) {
84
+ allTimes.push(...metric.times);
85
+ }
86
+
87
+ return {
88
+ totalQueries,
89
+ uniqueQueries: metrics.length,
90
+ totalTime: totalTime.toFixed(2),
91
+ avgTime: totalQueries > 0 ? (totalTime / totalQueries).toFixed(2) : '0',
92
+ p50: percentile(allTimes, 50).toFixed(2),
93
+ p95: percentile(allTimes, 95).toFixed(2),
94
+ p99: percentile(allTimes, 99).toFixed(2),
95
+ slowQueries: slowQueries.length,
96
+ slowQueryThreshold: SLOW_QUERY_THRESHOLD
97
+ };
98
+ };
99
+
100
+ export const clearMetrics = () => {
101
+ queryMetrics.clear();
102
+ slowQueries.length = 0;
103
+ };
104
+
105
+ export const withPerfTracking = (db, sql, params, executor) => {
106
+ const start = performance.now();
107
+ try {
108
+ const result = executor();
109
+ const duration = performance.now() - start;
110
+ trackQuery(sql, duration, params);
111
+ return result;
112
+ } catch (e) {
113
+ const duration = performance.now() - start;
114
+ trackQuery(sql, duration, params);
115
+ throw e;
116
+ }
117
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Query String Adapter - Parse URL query parameters into typed objects
3
+ * Adapted from moonlanding/src/lib/query-string-adapter.js
4
+ */
5
+
6
+ /**
7
+ * Parse query string from URL
8
+ * @param {object} request - Fetch Request or NextRequest-like
9
+ * @returns {Promise<{q?: string, page?: number, pageSize?: number, filters?: object, sort?: object}>}
10
+ */
11
+ export async function parseQuery(request) {
12
+ const url = request.url || (request._url ? `http://localhost${request._url}` : 'http://localhost/');
13
+ const searchParams = new URL(url).searchParams;
14
+
15
+ const result = {
16
+ q: searchParams.get('q') || null,
17
+ page: parseInt(searchParams.get('page') || '1', 10),
18
+ pageSize: parseInt(searchParams.get('pageSize') || searchParams.get('page_size') || '50', 10),
19
+ filters: {},
20
+ sort: null,
21
+ };
22
+
23
+ // Parse filters (filter[key]=value)
24
+ for (const [key, value] of searchParams.entries()) {
25
+ if (key.startsWith('filter_') || key.startsWith('filters[')) {
26
+ const filterKey = key.replace('filter_', '').replace(/filters\[(\w+)\]/, '$1');
27
+ result.filters[filterKey] = coerceValue(value);
28
+ }
29
+ }
30
+
31
+ // Parse sort
32
+ const sortBy = searchParams.get('sort') || searchParams.get('sortBy');
33
+ const sortDir = searchParams.get('dir') || searchParams.get('direction') || 'asc';
34
+ if (sortBy) {
35
+ result.sort = {
36
+ field: sortBy,
37
+ dir: sortDir.toLowerCase() === 'desc' ? 'desc' : 'asc',
38
+ };
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ /**
45
+ * Coerce string to appropriate type
46
+ * @param {string} value
47
+ * @returns {any}
48
+ */
49
+ function coerceValue(value) {
50
+ // Lowercase boolean strings
51
+ if (value === 'true') return true;
52
+ if (value === 'false') return false;
53
+ if (value === 'null' || value === '') return null;
54
+
55
+ // Number?
56
+ const num = Number(value);
57
+ if (!isNaN(num) && value.trim() !== '') return num;
58
+
59
+ return value;
60
+ }
61
+
62
+ /**
63
+ * Get default value for config key
64
+ * @param {string} key
65
+ * @returns {any}
66
+ */
67
+ export function getDefault(key) {
68
+ const defaults = {
69
+ page: 1,
70
+ pageSize: 50,
71
+ q: null,
72
+ filters: {},
73
+ };
74
+ return defaults[key];
75
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Realtime Server - WebSocket/Server-Sent Events for live updates
3
+ * Optional component - can be disabled if not needed
4
+ */
5
+
6
+ let subscribers = new Map(); // channel -> Set of callbacks
7
+ let enabled = false;
8
+
9
+ /**
10
+ * Initialize realtime (optional)
11
+ * @param {object} server - HTTP server to attach WS endpoint
12
+ */
13
+ export function initRealtime(server) {
14
+ if (!server) return;
15
+
16
+ // Can be extended with ws library for full WebSocket support
17
+ // For now, provides a polling-based notification system
18
+ enabled = true;
19
+ console.log('[Realtime] Initialized (polling mode)');
20
+ }
21
+
22
+ /**
23
+ * Broadcast update to all subscribers of a channel
24
+ * @param {string} channel
25
+ * @param {string} event
26
+ * @param {any} data
27
+ */
28
+ export function broadcastUpdate(channel, event, data) {
29
+ if (!enabled) return;
30
+
31
+ const channelSubscribers = subscribers.get(channel);
32
+ if (channelSubscribers) {
33
+ const payload = { event, data, timestamp: Date.now() };
34
+ channelSubscribers.forEach(cb => {
35
+ try { cb(payload); } catch {}
36
+ });
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Subscribe to updates for a channel
42
+ * @param {string} channel
43
+ * @param {Function} callback
44
+ * @returns {Function} Unsubscribe function
45
+ */
46
+ export function subscribe(channel, callback) {
47
+ if (!subscribers.has(channel)) {
48
+ subscribers.set(channel, new Set());
49
+ }
50
+ subscribers.get(channel).add(callback);
51
+
52
+ // Return unsubscribe function
53
+ return () => {
54
+ subscribers.get(channel)?.delete(callback);
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Get update stats
60
+ * @returns {object}
61
+ */
62
+ export function getStats() {
63
+ return {
64
+ channels: subscribers.size,
65
+ totalSubscribers: Array.from(subscribers.values()).reduce((sum, set) => sum + set.size, 0),
66
+ };
67
+ }
@@ -0,0 +1,61 @@
1
+ const cache = new Map()
2
+ const maxSize = 500
3
+ const ttl = 60000
4
+ let hits = 0
5
+ let misses = 0
6
+
7
+ function cacheKey(fn, args) {
8
+ return `${fn.name}:${JSON.stringify(args)}`
9
+ }
10
+
11
+ export function memoize(fn) {
12
+ return function(...args) {
13
+ const key = cacheKey(fn, args)
14
+ const cached = cache.get(key)
15
+ if (cached && Date.now() - cached.ts < ttl) {
16
+ hits++
17
+ return cached.value
18
+ }
19
+ misses++
20
+ const value = fn(...args)
21
+ cache.set(key, { value, ts: Date.now() })
22
+ if (cache.size > maxSize) {
23
+ const first = cache.keys().next().value
24
+ cache.delete(first)
25
+ }
26
+ return value
27
+ }
28
+ }
29
+
30
+ export function invalidate(pattern) {
31
+ if (!pattern) {
32
+ cache.clear()
33
+ return cache.size
34
+ }
35
+ let count = 0
36
+ for (const key of cache.keys()) {
37
+ if (key.includes(pattern)) {
38
+ cache.delete(key)
39
+ count++
40
+ }
41
+ }
42
+ return count
43
+ }
44
+
45
+ export function getCacheStats() {
46
+ const total = hits + misses
47
+ return {
48
+ size: cache.size,
49
+ hits,
50
+ misses,
51
+ hitRate: total > 0 ? (hits / total * 100).toFixed(2) + '%' : '0%',
52
+ maxSize,
53
+ ttl
54
+ }
55
+ }
56
+
57
+ export function clearCache() {
58
+ cache.clear()
59
+ hits = 0
60
+ misses = 0
61
+ }