@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 +1 -1
- package/package.json +1 -1
- package/src/http/Kernel.d.ts.map +1 -1
- package/src/http/Kernel.js +59 -41
- package/src/index.js +3 -3
- package/src/orm/QueryBuilder.d.ts +23 -2
- package/src/orm/QueryBuilder.d.ts.map +1 -1
- package/src/orm/QueryBuilder.js +298 -18
- package/src/worker-runtime/rpc-client.d.ts.map +1 -1
- package/src/worker-runtime/rpc-client.js +24 -17
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
|
|
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
package/src/http/Kernel.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/src/http/Kernel.js
CHANGED
|
@@ -111,41 +111,39 @@ const maybeSetKernelTraceRoute = (traceSpan, method, routeLabel) => {
|
|
|
111
111
|
return;
|
|
112
112
|
OpenTelemetry.setHttpRoute(traceSpan.span, method, routeLabel);
|
|
113
113
|
};
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
return safeParams;
|
|
143
|
+
};
|
|
144
|
+
const resolveWrappedRouteMiddleware = (routeMiddlewareNames, routeMiddleware) => {
|
|
147
145
|
const traceMiddlewareEmitter = getTraceMiddlewareEmitter();
|
|
148
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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:
|
|
58
|
-
leftJoin(table: string, on:
|
|
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":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGtE,OAAO,KAAK,EAAE,SAAS,EAAY,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,aAAa,CAAC,EAAE,qBAAqB,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"QueryBuilder.d.ts","sourceRoot":"","sources":["../../../src/orm/QueryBuilder.ts"],"names":[],"mappings":"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"}
|
package/src/orm/QueryBuilder.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
996
|
+
applyJoin(state, tableJoin, on, 'INNER');
|
|
726
997
|
return builder;
|
|
727
998
|
};
|
|
728
999
|
builder.leftJoin = (tableJoin, on) => {
|
|
729
|
-
state
|
|
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;
|
|
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 (!(
|
|
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:
|
|
176
|
-
attempts,
|
|
181
|
+
name: resolvePulledJobName(record),
|
|
182
|
+
payload: resolvePulledJobPayload(record),
|
|
183
|
+
attempts: resolvePulledJobAttempts(record),
|
|
177
184
|
};
|
|
178
185
|
};
|
|
179
186
|
const createZedgiBullMqError = (method, error) => {
|