@zintrust/core 3.0.4 → 3.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,7 +80,7 @@ ZinTrust is a **minimal-core** backend framework built on:
80
80
 
81
81
  ### 🛡️ Secure by Default
82
82
 
83
- - No raw SQL - QueryBuilder enforces parameterized queries
83
+ - No free-form SQL - QueryBuilder enforces safe identifiers + parameterized values (see [Query Builder](./query-builder.md) for exists, joins, groupBy, latestPer)
84
84
  - Automatic CSRF protection
85
85
  - JWT token management
86
86
  - XSS vulnerability prevention
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/core",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "Production-grade TypeScript backend framework for JavaScript",
5
5
  "homepage": "https://zintrust.com",
6
6
  "repository": {
@@ -1 +1 @@
1
- {"version":3,"file":"Kernel.d.ts","sourceRoot":"","sources":["../../../src/http/Kernel.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAErE,OAAO,KAAK,EAAE,OAAO,EAAc,MAAM,qBAAqB,CAAC;AAE/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAK7E,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,mBAAmB,CAAC;AAgBpE,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/C,wBAAwB,CAAC,GAAG,UAAU,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAC5D,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IACpE,SAAS,IAAI,OAAO,CAAC;IACrB,YAAY,IAAI,iBAAiB,CAAC;IAClC,kBAAkB,IAAI,gBAAgB,CAAC;IAGvC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5C,cAAc,IAAI,IAAI,CAAC;IACvB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C;AA4XD,eAAO,MAAM,MAAM;qBAvEK,OAAO,aAAa,iBAAiB,KAAG,OAAO;EAyErE,CAAC;AAEH,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Kernel.d.ts","sourceRoot":"","sources":["../../../src/http/Kernel.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAErE,OAAO,KAAK,EAAE,OAAO,EAAc,MAAM,qBAAqB,CAAC;AAE/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAK7E,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,mBAAmB,CAAC;AAgBpE,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/C,wBAAwB,CAAC,GAAG,UAAU,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAC5D,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IACpE,SAAS,IAAI,OAAO,CAAC;IACrB,YAAY,IAAI,iBAAiB,CAAC;IAClC,kBAAkB,IAAI,gBAAgB,CAAC;IAGvC,gBAAgB,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5C,cAAc,IAAI,IAAI,CAAC;IACvB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C;AA8ZD,eAAO,MAAM,MAAM;qBAvEK,OAAO,aAAa,iBAAiB,KAAG,OAAO;EAyErE,CAAC;AAEH,eAAe,MAAM,CAAC"}
@@ -111,41 +111,39 @@ const maybeSetKernelTraceRoute = (traceSpan, method, routeLabel) => {
111
111
  return;
112
112
  OpenTelemetry.setHttpRoute(traceSpan.span, method, routeLabel);
113
113
  };
114
- const runKernelPipeline = async (router, globalMiddleware, routeMiddleware, req, res, context, traceSpan) => {
115
- const route = resolveRouteWithPreflightFallback(router, req.getMethod(), req.getPath());
116
- if (route === null) {
117
- const routeLabel = 'not_found';
118
- maybeSetKernelTraceRoute(traceSpan, context.method, routeLabel);
119
- const handleNotFound = ErrorRouting.handleNotFound;
120
- await handleNotFound(req, res, context.requestId);
121
- return routeLabel;
122
- }
123
- // Safe type guard to ensure route.routePath is a non-empty string before calling .trim()
124
- const hasNonEmptyRoutePath = (r) => {
125
- if (typeof r !== 'object' || r === null)
126
- return false;
127
- const rp = r.routePath;
128
- return typeof rp === 'string' && rp.trim() !== '';
114
+ const runMiddlewareChain = async (middleware, req, res, terminal) => {
115
+ let index = 0;
116
+ const next = async () => {
117
+ if (index < middleware.length) {
118
+ const mw = middleware[index++];
119
+ await mw(req, res, next);
120
+ return;
121
+ }
122
+ await terminal();
129
123
  };
130
- const routeLabel = hasNonEmptyRoutePath(route) ? route.routePath : req.getPath();
131
- maybeSetKernelTraceRoute(traceSpan, context.method, routeLabel);
132
- // Use a typed view of the matched route to avoid unsafe member access
133
- const matchedRoute = route;
134
- // Coerce route params (which may be undefined or non-string) into the
135
- // expected Record<string, string> shape required by Request.setParams.
124
+ await next();
125
+ };
126
+ // Safe type guard to ensure route.routePath is a non-empty string before calling .trim()
127
+ const hasNonEmptyRoutePath = (r) => {
128
+ if (typeof r !== 'object' || r === null)
129
+ return false;
130
+ const rp = r.routePath;
131
+ return typeof rp === 'string' && rp.trim() !== '';
132
+ };
133
+ const coerceRouteParams = (params) => {
136
134
  const safeParams = {};
137
- if (typeof matchedRoute.params === 'object' && matchedRoute.params !== null) {
138
- for (const [k, v] of Object.entries(matchedRoute.params)) {
139
- if (v === undefined || v === null)
140
- continue;
141
- safeParams[k] = typeof v === 'string' ? v : String(v);
142
- }
135
+ if (typeof params !== 'object' || params === null)
136
+ return safeParams;
137
+ for (const [k, v] of Object.entries(params)) {
138
+ if (v === undefined || v === null)
139
+ continue;
140
+ safeParams[k] = typeof v === 'string' ? v : String(v);
143
141
  }
144
- req.setParams(safeParams);
145
- const routeMiddlewareNames = getRouteMiddlewareNames(matchedRoute);
146
- req.context['traceRouteMiddleware'] = routeMiddlewareNames;
142
+ return safeParams;
143
+ };
144
+ const resolveWrappedRouteMiddleware = (routeMiddlewareNames, routeMiddleware) => {
147
145
  const traceMiddlewareEmitter = getTraceMiddlewareEmitter();
148
- const resolvedRouteMiddleware = routeMiddlewareNames
146
+ return routeMiddlewareNames
149
147
  .map((name) => ({ name, middleware: routeMiddleware[name] }))
150
148
  .filter((entry) => typeof entry.middleware === 'function')
151
149
  .map(({ name, middleware }) => {
@@ -163,17 +161,37 @@ const runKernelPipeline = async (router, globalMiddleware, routeMiddleware, req,
163
161
  };
164
162
  return wrapped;
165
163
  });
166
- const middlewareToRun = [...globalMiddleware, ...resolvedRouteMiddleware];
167
- let index = 0;
168
- const next = async () => {
169
- if (index < middlewareToRun.length) {
170
- const mw = middlewareToRun[index++];
171
- await mw(req, res, next);
172
- return;
173
- }
164
+ };
165
+ const runNotFoundPipeline = async (globalMiddleware, req, res, context, traceSpan) => {
166
+ // Run global middleware (including HttpWatcher) so routing 404s are still observed.
167
+ // Route middleware is intentionally skipped — there is no matched route.
168
+ const routeLabel = 'not_found';
169
+ maybeSetKernelTraceRoute(traceSpan, context.method, routeLabel);
170
+ req.context['traceRouteMiddleware'] = [];
171
+ const handleNotFound = ErrorRouting.handleNotFound;
172
+ await runMiddlewareChain(globalMiddleware, req, res, async () => {
173
+ await handleNotFound(req, res, context.requestId);
174
+ });
175
+ return routeLabel;
176
+ };
177
+ const runKernelPipeline = async (router, globalMiddleware, routeMiddleware, req, res, context, traceSpan) => {
178
+ const route = resolveRouteWithPreflightFallback(router, req.getMethod(), req.getPath());
179
+ if (route === null) {
180
+ return runNotFoundPipeline(globalMiddleware, req, res, context, traceSpan);
181
+ }
182
+ const matchedRoute = route;
183
+ const routeLabel = hasNonEmptyRoutePath(matchedRoute) ? matchedRoute.routePath : req.getPath();
184
+ maybeSetKernelTraceRoute(traceSpan, context.method, routeLabel);
185
+ req.setParams(coerceRouteParams(matchedRoute.params));
186
+ const routeMiddlewareNames = getRouteMiddlewareNames(matchedRoute);
187
+ req.context['traceRouteMiddleware'] = routeMiddlewareNames;
188
+ const middlewareToRun = [
189
+ ...globalMiddleware,
190
+ ...resolveWrappedRouteMiddleware(routeMiddlewareNames, routeMiddleware),
191
+ ];
192
+ await runMiddlewareChain(middlewareToRun, req, res, async () => {
174
193
  await matchedRoute.handler(req, res);
175
- };
176
- await next();
194
+ });
177
195
  return routeLabel;
178
196
  };
179
197
  const finalizeKernelObservability = (context, res, routeLabel, thrown, traceSpan) => {
package/src/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @zintrust/core v3.0.4
2
+ * @zintrust/core v3.0.6
3
3
  *
4
4
  * ZinTrust Framework - Production-Grade TypeScript Backend
5
5
  * Built for performance, type safety, and exceptional developer experience
6
6
  *
7
7
  * Build Information:
8
- * Built: 2026-07-14T19:37:30.278Z
8
+ * Built: 2026-07-17T06:17:14.731Z
9
9
  * Node: >=20.0.0
10
10
  * License: MIT
11
11
  *
@@ -21,7 +21,7 @@
21
21
  * Available at runtime for debugging and health checks
22
22
  */
23
23
  export const ZINTRUST_VERSION = '0.1.41';
24
- export const ZINTRUST_BUILD_DATE = '2026-07-14T19:37:30.239Z'; // Replaced during build
24
+ export const ZINTRUST_BUILD_DATE = '2026-07-17T06:17:14.680Z'; // Replaced during build
25
25
  export { Application } from './boot/Application.js';
26
26
  export { AwsSigV4 } from './common/index.js';
27
27
  export { SignedRequest } from './security/SignedRequest.js';
@@ -17,6 +17,14 @@ export interface NormalizedTextOptions {
17
17
  trim?: boolean;
18
18
  lowercase?: boolean;
19
19
  }
20
+ export interface IJoinOnBuilder {
21
+ on(left: string, operator: string, right: string): IJoinOnBuilder;
22
+ }
23
+ export type LatestPerOptions = {
24
+ orderBy: Array<[string, 'ASC' | 'DESC']>;
25
+ alias?: string;
26
+ };
27
+ export type JoinOnInput = string | ((on: IJoinOnBuilder) => unknown);
20
28
  /**
21
29
  * Result returned from INSERT operations
22
30
  * Provides access to the created record ID, affected rows count, and the full record if available
@@ -34,6 +42,11 @@ export interface QueryBuilderOptions {
34
42
  export interface PaginationOptions {
35
43
  baseUrl?: string;
36
44
  query?: PaginationQuery;
45
+ /**
46
+ * When joins are present, paginate totals use COUNT(DISTINCT …).
47
+ * Defaults to `<table>.id`. Pass an explicit safe identifier path when the primary key differs.
48
+ */
49
+ countDistinct?: string;
37
50
  }
38
51
  export type EagerLoadConstraint = (builder: IQueryBuilder) => IQueryBuilder;
39
52
  export type EagerLoadConstraints = Record<string, EagerLoadConstraint>;
@@ -49,13 +62,20 @@ export interface IQueryBuilder {
49
62
  whereNormalized(column: string, value: unknown, options?: NormalizedTextOptions): IQueryBuilder;
50
63
  orWhereNormalized(column: string, value: unknown, options?: NormalizedTextOptions): IQueryBuilder;
51
64
  whereNull(column: string): IQueryBuilder;
65
+ whereNotNull(column: string): IQueryBuilder;
52
66
  whereIn(column: string, values: unknown[]): IQueryBuilder;
53
67
  whereNotIn(column: string, values: unknown[]): IQueryBuilder;
68
+ whereColumn(left: string, operator: string, right: string): IQueryBuilder;
69
+ whereExists(callback: (builder: IQueryBuilder) => unknown): IQueryBuilder;
70
+ whereNotExists(callback: (builder: IQueryBuilder) => unknown): IQueryBuilder;
71
+ from(table: string): IQueryBuilder;
54
72
  withTrashed(): IQueryBuilder;
55
73
  onlyTrashed(): IQueryBuilder;
56
74
  withoutTrashed(): IQueryBuilder;
57
- join(table: string, on: string): IQueryBuilder;
58
- leftJoin(table: string, on: string): IQueryBuilder;
75
+ join(table: string, on: JoinOnInput): IQueryBuilder;
76
+ leftJoin(table: string, on: JoinOnInput): IQueryBuilder;
77
+ groupBy(...columns: string[]): IQueryBuilder;
78
+ latestPer(partitionBy: string | string[], options: LatestPerOptions): IQueryBuilder;
59
79
  orderBy(column: string, direction?: 'ASC' | 'DESC'): IQueryBuilder;
60
80
  inRandomOrder(): IQueryBuilder;
61
81
  limit(count: number): IQueryBuilder;
@@ -72,6 +92,7 @@ export interface IQueryBuilder {
72
92
  getJoins(): Array<{
73
93
  table: string;
74
94
  on: string;
95
+ type: 'INNER' | 'LEFT';
75
96
  }>;
76
97
  isReadOperation(): boolean;
77
98
  toSQL(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"QueryBuilder.d.ts","sourceRoot":"","sources":["../../../src/orm/QueryBuilder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"QueryBuilder.d.ts","sourceRoot":"","sources":["../../../src/orm/QueryBuilder.ts"],"names":[],"mappings":"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"}
@@ -110,7 +110,17 @@ const buildNormalizedColumnSql = (column, dialect, options) => {
110
110
  return sql;
111
111
  };
112
112
  const isWhereGroupClause = (value) => 'kind' in value && value.kind === 'group';
113
+ const isWhereExistsClause = (value) => 'kind' in value && value.kind === 'exists';
114
+ const isWhereColumnCompareClause = (value) => 'kind' in value && value.kind === 'column-compare';
113
115
  const normalizeOperator = (operator) => operator.trim().toUpperCase();
116
+ const ALLOWED_COLUMN_COMPARE_OPERATORS = new Set(['=', '!=', '<>', '<', '<=', '>', '>=']);
117
+ const assertSafeColumnCompareOperator = (operator) => {
118
+ const normalized = operator.trim();
119
+ if (!ALLOWED_COLUMN_COMPARE_OPERATORS.has(normalized)) {
120
+ throw ErrorFactory.createDatabaseError('Unsafe SQL column comparison operator');
121
+ }
122
+ return normalized;
123
+ };
114
124
  const ALLOWED_OPERATORS = new Set([
115
125
  '=',
116
126
  '==',
@@ -289,19 +299,49 @@ const compileSingleWhereClause = (clause, dialect) => {
289
299
  return nullClause;
290
300
  return compileComparisonWhereClause(columnSql, operator, clause.value, clause.expression, clause.normalization);
291
301
  };
302
+ const compileColumnCompareClause = (clause, dialect) => {
303
+ assertSafeIdentifierPath(clause.left, 'where column left');
304
+ assertSafeIdentifierPath(clause.right, 'where column right');
305
+ const operator = assertSafeColumnCompareOperator(clause.operator);
306
+ return {
307
+ sql: `${escapeIdentifier(clause.left, dialect)} ${operator} ${escapeIdentifier(clause.right, dialect)}`,
308
+ parameters: [],
309
+ };
310
+ };
311
+ const compileExistsClause = (clause, dialect) => {
312
+ assertSafeIdentifierPath(clause.table, 'exists table');
313
+ const inner = compileWhereFragments(clause.conditions, dialect);
314
+ const whereSql = inner.sql.length > 0 ? ` WHERE ${inner.sql}` : '';
315
+ const keyword = clause.not ? 'NOT EXISTS' : 'EXISTS';
316
+ return {
317
+ sql: `${keyword} (SELECT 1 FROM ${escapeIdentifier(clause.table, dialect)}${whereSql})`,
318
+ parameters: inner.parameters,
319
+ };
320
+ };
321
+ const compileWherePredicate = (condition, dialect) => {
322
+ if (isWhereGroupClause(condition)) {
323
+ const compiled = compileWhereFragments(condition.conditions, dialect);
324
+ return { ...compiled, grouped: true };
325
+ }
326
+ if (isWhereExistsClause(condition)) {
327
+ return { ...compileExistsClause(condition, dialect), grouped: false };
328
+ }
329
+ if (isWhereColumnCompareClause(condition)) {
330
+ return { ...compileColumnCompareClause(condition, dialect), grouped: false };
331
+ }
332
+ return { ...compileSingleWhereClause(condition, dialect), grouped: false };
333
+ };
292
334
  const compileWhereFragments = (conditions, dialect) => {
293
335
  if (conditions.length === 0)
294
336
  return { sql: '', parameters: [] };
295
337
  const parameters = [];
296
338
  const fragments = [];
297
339
  for (const condition of conditions) {
298
- const compiled = isWhereGroupClause(condition)
299
- ? compileWhereFragments(condition.conditions, dialect)
300
- : compileSingleWhereClause(condition, dialect);
340
+ const compiled = compileWherePredicate(condition, dialect);
301
341
  if (compiled.sql.length === 0)
302
342
  continue;
303
343
  parameters.push(...compiled.parameters);
304
- const fragment = isWhereGroupClause(condition) ? `(${compiled.sql})` : compiled.sql;
344
+ const fragment = compiled.grouped ? `(${compiled.sql})` : compiled.sql;
305
345
  const boolean = normalizeClauseBoolean(condition.boolean);
306
346
  fragments.push(fragments.length === 0 ? fragment : `${boolean} ${fragment}`);
307
347
  }
@@ -386,35 +426,171 @@ const buildLimitOffsetClause = (limit, offset) => {
386
426
  // `users.id = profiles.user_id`). Operator alternation lists the two-char
387
427
  // operators before their single-char prefixes so `<=`/`>=`/`<>` win the match.
388
428
  const JOIN_CONDITION = /^([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*(=|!=|<>|<=|>=|<|>)\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)$/;
429
+ const parseJoinOnTerms = (on) => {
430
+ const parts = on
431
+ .split(/\s+AND\s+/i)
432
+ .map((part) => part.trim())
433
+ .filter((part) => part.length > 0);
434
+ if (parts.length === 0) {
435
+ throw ErrorFactory.createDatabaseError('Unsafe SQL join condition');
436
+ }
437
+ return parts.map((part) => {
438
+ const match = JOIN_CONDITION.exec(part);
439
+ if (!match) {
440
+ throw ErrorFactory.createDatabaseError('Unsafe SQL join condition');
441
+ }
442
+ const [, left, op, right] = match;
443
+ return { left: left ?? '', op: op ?? '=', right: right ?? '' };
444
+ });
445
+ };
446
+ const resolveJoinOnInput = (on) => {
447
+ if (typeof on === 'string') {
448
+ // Validate early so bad ON strings fail at builder time, not only at compile.
449
+ const terms = parseJoinOnTerms(on);
450
+ return terms.map((term) => `${term.left} ${term.op} ${term.right}`).join(' AND ');
451
+ }
452
+ const terms = [];
453
+ const joinOnBuilder = Object.freeze({
454
+ on(left, operator, right) {
455
+ assertSafeIdentifierPath(left, 'join on left');
456
+ assertSafeIdentifierPath(right, 'join on right');
457
+ const op = assertSafeColumnCompareOperator(operator);
458
+ terms.push({ left: left.trim(), op, right: right.trim() });
459
+ return joinOnBuilder;
460
+ },
461
+ });
462
+ on(joinOnBuilder);
463
+ if (terms.length === 0) {
464
+ throw ErrorFactory.createDatabaseError('Join ON builder requires at least one on() term');
465
+ }
466
+ return terms.map((term) => `${term.left} ${term.op} ${term.right}`).join(' AND ');
467
+ };
468
+ const buildJoinOnSql = (on, dialect) => {
469
+ const terms = parseJoinOnTerms(on);
470
+ return terms
471
+ .map((term) => `${escapeIdentifier(term.left, dialect)} ${term.op} ${escapeIdentifier(term.right, dialect)}`)
472
+ .join(' AND ');
473
+ };
389
474
  const buildJoinClause = (joins, dialect) => joins
390
475
  .map(({ table, on, type }) => {
391
476
  assertSafeIdentifierPath(table, 'join table');
392
- const match = JOIN_CONDITION.exec(on.trim());
393
- if (!match) {
394
- throw ErrorFactory.createDatabaseError('Unsafe SQL join condition');
395
- }
396
- const [, left, op, right] = match;
397
- return ` ${type} JOIN ${escapeIdentifier(table, dialect)} ON ${escapeIdentifier(left, dialect)} ${op} ${escapeIdentifier(right, dialect)}`;
477
+ return ` ${type} JOIN ${escapeIdentifier(table, dialect)} ON ${buildJoinOnSql(on, dialect)}`;
398
478
  })
399
479
  .join('');
400
- const buildSelectQuery = (state) => {
480
+ const buildGroupByClause = (columns, dialect) => {
481
+ if (columns.length === 0)
482
+ return '';
483
+ const parts = columns.map((column) => {
484
+ const col = column.trim();
485
+ assertSafeIdentifierPath(col, 'group by column');
486
+ return escapeIdentifier(col, dialect);
487
+ });
488
+ return ` GROUP BY ${parts.join(', ')}`;
489
+ };
490
+ const buildWindowOrderBySql = (orderBy, dialect) => {
491
+ if (orderBy.length === 0) {
492
+ throw ErrorFactory.createDatabaseError('latestPer requires at least one orderBy column');
493
+ }
494
+ return orderBy
495
+ .map((clause) => {
496
+ assertSafeIdentifierPath(clause.column, 'window order by column');
497
+ return `${escapeIdentifier(clause.column, dialect)} ${clause.direction}`;
498
+ })
499
+ .join(', ');
500
+ };
501
+ const buildWindowPartitionSql = (partitionBy, dialect) => {
502
+ if (partitionBy.length === 0) {
503
+ throw ErrorFactory.createDatabaseError('latestPer requires at least one partition column');
504
+ }
505
+ return partitionBy
506
+ .map((column) => {
507
+ assertSafeIdentifierPath(column, 'window partition column');
508
+ return escapeIdentifier(column, dialect);
509
+ })
510
+ .join(', ');
511
+ };
512
+ const buildBaseSelectBody = (state, columnsSql, options) => {
401
513
  if (state.tableName.length > 0) {
402
514
  assertSafeIdentifierPath(state.tableName, 'table name');
403
515
  }
404
- const columns = buildSelectClause(state.selectColumns, state.dialect);
405
516
  const fromClause = state.tableName.length > 0 ? ` FROM ${escapeIdentifier(state.tableName, state.dialect)}` : '';
406
517
  const joinClause = buildJoinClause(state.joins, state.dialect);
407
518
  const where = compileWhere(getEffectiveWhereConditions(state), state.dialect);
408
- const sql = `SELECT ${columns}${fromClause}${joinClause}${where.sql}${buildOrderByClause(state.orderByClauses, state.randomOrder, state.dialect)}${buildLimitOffsetClause(state.limitValue, state.offsetValue)}`;
519
+ const groupByClause = options.includeGroupBy
520
+ ? buildGroupByClause(state.groupByColumns, state.dialect)
521
+ : '';
522
+ const orderLimit = options.includeOrderLimit
523
+ ? `${buildOrderByClause(state.orderByClauses, state.randomOrder, state.dialect)}${buildLimitOffsetClause(state.limitValue, state.offsetValue)}`
524
+ : '';
525
+ const sql = `SELECT ${columnsSql}${fromClause}${joinClause}${where.sql}${groupByClause}${orderLimit}`;
409
526
  return { sql, parameters: where.parameters };
410
527
  };
411
- const buildCountQuery = (state) => {
528
+ const buildLatestPerQuery = (state) => {
529
+ const latest = state.latestPer;
530
+ if (latest === undefined) {
531
+ throw ErrorFactory.createDatabaseError('latestPer state is missing');
532
+ }
533
+ const baseColumns = buildSelectClause(state.selectColumns, state.dialect);
534
+ const partitionSql = buildWindowPartitionSql(latest.partitionBy, state.dialect);
535
+ const orderSql = buildWindowOrderBySql(latest.orderBy, state.dialect);
536
+ const aliasSql = escapeIdentifier(latest.alias, state.dialect);
537
+ const windowSql = `ROW_NUMBER() OVER (PARTITION BY ${partitionSql} ORDER BY ${orderSql}) AS ${aliasSql}`;
538
+ const innerColumns = baseColumns === '*' || baseColumns.trim().length === 0
539
+ ? `*, ${windowSql}`
540
+ : `${baseColumns}, ${windowSql}`;
541
+ // Window wrap owns outer ORDER/LIMIT; inner keeps filters/joins only.
542
+ const inner = buildBaseSelectBody(state, innerColumns, {
543
+ includeOrderLimit: false,
544
+ includeGroupBy: false,
545
+ });
546
+ const outerOrderLimit = `${buildOrderByClause(state.orderByClauses, state.randomOrder, state.dialect)}${buildLimitOffsetClause(state.limitValue, state.offsetValue)}`;
547
+ const sql = `SELECT * FROM (${inner.sql}) AS ${escapeIdentifier('_zt_window', state.dialect)} WHERE ${aliasSql} = 1${outerOrderLimit}`;
548
+ return { sql, parameters: inner.parameters };
549
+ };
550
+ const buildSelectQuery = (state) => {
551
+ if (state.latestPer !== undefined) {
552
+ return buildLatestPerQuery(state);
553
+ }
554
+ const columns = buildSelectClause(state.selectColumns, state.dialect);
555
+ return buildBaseSelectBody(state, columns, {
556
+ includeOrderLimit: true,
557
+ includeGroupBy: true,
558
+ });
559
+ };
560
+ const buildCountQuery = (state, options) => {
412
561
  if (state.tableName.length > 0) {
413
562
  assertSafeIdentifierPath(state.tableName, 'table name');
414
563
  }
564
+ // GROUP BY / window queries: count rows of the unconstrained result set via subquery.
565
+ if (state.groupByColumns.length > 0 || state.latestPer !== undefined) {
566
+ const prevLimit = state.limitValue;
567
+ const prevOffset = state.offsetValue;
568
+ const prevOrder = state.orderByClauses;
569
+ const prevRandom = state.randomOrder;
570
+ state.limitValue = undefined;
571
+ state.offsetValue = undefined;
572
+ state.orderByClauses = [];
573
+ state.randomOrder = false;
574
+ const inner = buildSelectQuery(state);
575
+ state.limitValue = prevLimit;
576
+ state.offsetValue = prevOffset;
577
+ state.orderByClauses = prevOrder;
578
+ state.randomOrder = prevRandom;
579
+ const sql = `SELECT COUNT(*) AS total FROM (${inner.sql}) AS ${escapeIdentifier('_zt_count', state.dialect)}`;
580
+ return { sql, parameters: inner.parameters };
581
+ }
415
582
  const fromClause = state.tableName.length > 0 ? ` FROM ${escapeIdentifier(state.tableName, state.dialect)}` : '';
583
+ const joinClause = buildJoinClause(state.joins, state.dialect);
416
584
  const where = compileWhere(getEffectiveWhereConditions(state), state.dialect);
417
- const sql = `SELECT COUNT(*) AS total${fromClause}${where.sql}`;
585
+ let countExpr = 'COUNT(*)';
586
+ if (state.joins.length > 0) {
587
+ const distinctCol = typeof options?.countDistinct === 'string' && options.countDistinct.trim().length > 0
588
+ ? options.countDistinct.trim()
589
+ : `${state.tableName}.id`;
590
+ assertSafeIdentifierPath(distinctCol, 'count distinct column');
591
+ countExpr = `COUNT(DISTINCT ${escapeIdentifier(distinctCol, state.dialect)})`;
592
+ }
593
+ const sql = `SELECT ${countExpr} AS total${fromClause}${joinClause}${where.sql}`;
418
594
  return { sql, parameters: where.parameters };
419
595
  };
420
596
  const applyWhereCondition = (state, column, operator, value, boolean = 'AND', extra = {}) => {
@@ -449,11 +625,81 @@ const createPredicateState = (dialect) => ({
449
625
  orderByClauses: [],
450
626
  randomOrder: false,
451
627
  joins: [],
628
+ groupByColumns: [],
452
629
  eagerLoads: [],
453
630
  eagerLoadConstraints: {},
454
631
  eagerLoadCounts: [],
455
632
  dialect,
456
633
  });
634
+ const applyWhereColumnCompare = (state, left, operator, right, boolean = 'AND') => {
635
+ assertSafeIdentifierPath(left, 'where column left');
636
+ assertSafeIdentifierPath(right, 'where column right');
637
+ const op = assertSafeColumnCompareOperator(operator);
638
+ state.whereConditions.push({
639
+ kind: 'column-compare',
640
+ boolean,
641
+ left: left.trim(),
642
+ operator: op,
643
+ right: right.trim(),
644
+ });
645
+ };
646
+ const applyWhereExists = (state, callback, not, boolean = 'AND') => {
647
+ const subState = createPredicateState(state.dialect);
648
+ const subBuilder = createBuilder(subState);
649
+ callback(subBuilder);
650
+ const table = subState.tableName.trim();
651
+ if (table.length === 0) {
652
+ throw ErrorFactory.createDatabaseError('EXISTS subquery requires from(table)');
653
+ }
654
+ assertSafeIdentifierPath(table, 'exists table');
655
+ state.whereConditions.push({
656
+ kind: 'exists',
657
+ not,
658
+ boolean,
659
+ table,
660
+ conditions: subState.whereConditions,
661
+ });
662
+ };
663
+ const applyJoin = (state, table, on, type) => {
664
+ assertSafeIdentifierPath(table, 'join table');
665
+ const onSql = resolveJoinOnInput(on);
666
+ state.joins.push({ table: table.trim(), on: onSql, type });
667
+ };
668
+ const applyGroupBy = (state, columns) => {
669
+ for (const column of columns) {
670
+ const col = String(column).trim();
671
+ assertSafeIdentifierPath(col, 'group by column');
672
+ state.groupByColumns.push(col);
673
+ }
674
+ };
675
+ const applyLatestPer = (state, partitionBy, options) => {
676
+ const partitions = (Array.isArray(partitionBy) ? partitionBy : [partitionBy])
677
+ .map((column) => String(column).trim())
678
+ .filter((column) => column.length > 0);
679
+ if (partitions.length === 0) {
680
+ throw ErrorFactory.createDatabaseError('latestPer requires at least one partition column');
681
+ }
682
+ for (const column of partitions) {
683
+ assertSafeIdentifierPath(column, 'window partition column');
684
+ }
685
+ if (!Array.isArray(options.orderBy) || options.orderBy.length === 0) {
686
+ throw ErrorFactory.createDatabaseError('latestPer requires at least one orderBy column');
687
+ }
688
+ const orderBy = options.orderBy.map(([column, direction]) => {
689
+ const col = String(column).trim();
690
+ assertSafeIdentifierPath(col, 'window order by column');
691
+ return { column: col, direction: normalizeOrderDirection(direction) };
692
+ });
693
+ const aliasRaw = typeof options.alias === 'string' && options.alias.trim().length > 0
694
+ ? options.alias.trim()
695
+ : 'rn';
696
+ assertSafeIdentifier(aliasRaw, 'window alias');
697
+ state.latestPer = {
698
+ partitionBy: partitions,
699
+ orderBy,
700
+ alias: aliasRaw,
701
+ };
702
+ };
457
703
  const applyWhereGroup = (state, callback, boolean) => {
458
704
  const groupState = createPredicateState(state.dialect);
459
705
  const groupBuilder = createBuilder(groupState);
@@ -474,6 +720,10 @@ const flattenWherePredicates = (conditions) => {
474
720
  flattened.push(...flattenWherePredicates(condition.conditions));
475
721
  continue;
476
722
  }
723
+ // Exists / column-compare predicates are not simple column clauses.
724
+ if (isWhereExistsClause(condition) || isWhereColumnCompareClause(condition)) {
725
+ continue;
726
+ }
477
727
  flattened.push(condition);
478
728
  }
479
729
  return flattened;
@@ -596,7 +846,9 @@ async function executePaginate(builder, state, db, page, perPage, options) {
596
846
  throw ErrorFactory.createDatabaseError('Database instance not provided to QueryBuilder');
597
847
  const safePage = normalizePaginationValue(page, 'page');
598
848
  const safePerPage = normalizePaginationValue(perPage, 'perPage');
599
- const countQuery = buildCountQuery(state);
849
+ const countQuery = buildCountQuery(state, {
850
+ countDistinct: options?.countDistinct,
851
+ });
600
852
  const countRows = (await db.query(countQuery.sql, countQuery.parameters, true));
601
853
  const rawTotal = countRows.at(0)?.['total'];
602
854
  const total = typeof rawTotal === 'bigint' ? Number(rawTotal) : Number(rawTotal ?? 0);
@@ -682,6 +934,7 @@ function attachWhereMethods(builder, state) {
682
934
  return builder;
683
935
  };
684
936
  builder.whereNull = (column) => builder.where(column, 'IS', null);
937
+ builder.whereNotNull = (column) => builder.where(column, 'IS NOT', null);
685
938
  builder.whereIn = (column, values) => {
686
939
  builder.where(column, 'IN', values);
687
940
  return builder;
@@ -690,6 +943,24 @@ function attachWhereMethods(builder, state) {
690
943
  builder.where(column, 'NOT IN', values);
691
944
  return builder;
692
945
  };
946
+ builder.whereColumn = (left, operator, right) => {
947
+ applyWhereColumnCompare(state, left, operator, right, 'AND');
948
+ return builder;
949
+ };
950
+ builder.whereExists = (callback) => {
951
+ applyWhereExists(state, callback, false, 'AND');
952
+ return builder;
953
+ };
954
+ builder.whereNotExists = (callback) => {
955
+ applyWhereExists(state, callback, true, 'AND');
956
+ return builder;
957
+ };
958
+ builder.from = (table) => {
959
+ const name = String(table).trim();
960
+ assertSafeIdentifierPath(name, 'table name');
961
+ state.tableName = name;
962
+ return builder;
963
+ };
693
964
  }
694
965
  function attachSoftDeleteMethods(builder, state) {
695
966
  builder.withTrashed = () => {
@@ -722,11 +993,19 @@ function attachSoftDeleteMethods(builder, state) {
722
993
  }
723
994
  function attachJoinOrderPagingMethods(builder, state) {
724
995
  builder.join = (tableJoin, on) => {
725
- state.joins.push({ table: tableJoin, on, type: 'INNER' });
996
+ applyJoin(state, tableJoin, on, 'INNER');
726
997
  return builder;
727
998
  };
728
999
  builder.leftJoin = (tableJoin, on) => {
729
- state.joins.push({ table: tableJoin, on, type: 'LEFT' });
1000
+ applyJoin(state, tableJoin, on, 'LEFT');
1001
+ return builder;
1002
+ };
1003
+ builder.groupBy = (...columns) => {
1004
+ applyGroupBy(state, columns);
1005
+ return builder;
1006
+ };
1007
+ builder.latestPer = (partitionBy, options) => {
1008
+ applyLatestPer(state, partitionBy, options);
730
1009
  return builder;
731
1010
  };
732
1011
  builder.orderBy = (column, direction = 'ASC') => {
@@ -1312,6 +1591,7 @@ export const QueryBuilder = Object.freeze({
1312
1591
  orderByClauses: [],
1313
1592
  randomOrder: false,
1314
1593
  joins: [],
1594
+ groupByColumns: [],
1315
1595
  eagerLoads: [],
1316
1596
  eagerLoadConstraints: {},
1317
1597
  eagerLoadCounts: [],
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/rpc-client.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAWF,eAAO,MAAM,oBAAoB,QAAO,OAAkC,CAAC;AAE3E,eAAO,MAAM,4BAA4B,QAAO,MAI/C,CAAC;AAOF,eAAO,MAAM,8BAA8B,QAAO,OAC0B,CAAC;AAiO7E,wFAAwF;AACxF,eAAO,MAAM,OAAO,GAClB,WAAW,MAAM,EACjB,qBAAqB,MAAM,KAC1B,OAAO,CAAC,SAAS,GAAG,SAAS,CAkB/B,CAAC;AAEF,mCAAmC;AACnC,eAAO,MAAM,MAAM,GACjB,WAAW,MAAM,EACjB,OAAO,MAAM,EACb,cAAc,OAAO,KACpB,OAAO,CAAC,IAAI,CAOd,CAAC;AAEF,gCAAgC;AAChC,eAAO,MAAM,OAAO,GAAU,WAAW,MAAM,EAAE,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAW5F,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,gBAAgB,EAAE,CAO9D,CAAC"}
1
+ {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../../src/worker-runtime/rpc-client.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAWF,eAAO,MAAM,oBAAoB,QAAO,OAAkC,CAAC;AAE3E,eAAO,MAAM,4BAA4B,QAAO,MAI/C,CAAC;AAOF,eAAO,MAAM,8BAA8B,QAAO,OAC0B,CAAC;AAkO7E,wFAAwF;AACxF,eAAO,MAAM,OAAO,GAClB,WAAW,MAAM,EACjB,qBAAqB,MAAM,KAC1B,OAAO,CAAC,SAAS,GAAG,SAAS,CAkB/B,CAAC;AAEF,mCAAmC;AACnC,eAAO,MAAM,MAAM,GACjB,WAAW,MAAM,EACjB,OAAO,MAAM,EACb,cAAc,OAAO,KACpB,OAAO,CAAC,IAAI,CAOd,CAAC;AAEF,gCAAgC;AAChC,eAAO,MAAM,OAAO,GAAU,WAAW,MAAM,EAAE,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAW5F,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,gBAAgB,EAAE,CAO9D,CAAC"}
@@ -44,6 +44,8 @@ const queuePayload = (extra) => {
44
44
  return db === undefined ? extra : { db, ...extra };
45
45
  };
46
46
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
47
+ const isPullJobId = (value) => (typeof value === 'string' && value.trim() !== '') ||
48
+ (typeof value === 'number' && Number.isFinite(value));
47
49
  const parseHeaderEnv = (key) => {
48
50
  const raw = Env.get(key, '').trim();
49
51
  if (raw === '')
@@ -145,6 +147,24 @@ const call = async (service, method, payload = {}) => {
145
147
  }
146
148
  return parsed.result;
147
149
  };
150
+ const resolvePulledJobPayload = (record) => {
151
+ if (hasOwn(record, 'payload'))
152
+ return record['payload'];
153
+ if (hasOwn(record, 'data'))
154
+ return record['data'];
155
+ throw ErrorFactory.createConfigError('Worker queue pull returned a job without payload/data');
156
+ };
157
+ const resolvePulledJobAttempts = (record) => {
158
+ const rawAttempts = hasOwn(record, 'attempts') ? record['attempts'] : record['attemptsMade'];
159
+ return typeof rawAttempts === 'number' && Number.isFinite(rawAttempts)
160
+ ? Math.max(0, Math.floor(rawAttempts))
161
+ : 0;
162
+ };
163
+ const resolvePulledJobName = (record) => {
164
+ return typeof record['name'] === 'string' && record['name'].trim() !== ''
165
+ ? record['name'].trim()
166
+ : 'default';
167
+ };
148
168
  const normalizePulledJob = (raw) => {
149
169
  if (raw === null || raw === undefined)
150
170
  return undefined;
@@ -153,27 +173,14 @@ const normalizePulledJob = (raw) => {
153
173
  }
154
174
  const record = raw;
155
175
  const rawId = record['id'];
156
- if (!((typeof rawId === 'string' && rawId.trim() !== '') ||
157
- (typeof rawId === 'number' && Number.isFinite(rawId)))) {
176
+ if (!isPullJobId(rawId)) {
158
177
  throw ErrorFactory.createConfigError('Worker queue pull returned a job without an id');
159
178
  }
160
- const hasPayload = hasOwn(record, 'payload');
161
- const hasData = hasOwn(record, 'data');
162
- if (!hasPayload && !hasData) {
163
- throw ErrorFactory.createConfigError('Worker queue pull returned a job without payload/data');
164
- }
165
- const rawAttempts = hasOwn(record, 'attempts') ? record['attempts'] : record['attemptsMade'];
166
- const attempts = typeof rawAttempts === 'number' && Number.isFinite(rawAttempts)
167
- ? Math.max(0, Math.floor(rawAttempts))
168
- : 0;
169
- const name = typeof record['name'] === 'string' && record['name'].trim() !== ''
170
- ? record['name'].trim()
171
- : 'default';
172
179
  return {
173
180
  id: String(rawId),
174
- name,
175
- payload: hasPayload ? record['payload'] : record['data'],
176
- attempts,
181
+ name: resolvePulledJobName(record),
182
+ payload: resolvePulledJobPayload(record),
183
+ attempts: resolvePulledJobAttempts(record),
177
184
  };
178
185
  };
179
186
  const createZedgiBullMqError = (method, error) => {