@agile-team/mach-report 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder/template-builder.d.ts +53 -0
- package/dist/builder/template-builder.js +153 -0
- package/dist/builder/template-builder.js.map +1 -0
- package/dist/compat/jh4j-template.d.ts +12 -0
- package/dist/compat/jh4j-template.js +196 -0
- package/dist/compat/jh4j-template.js.map +1 -0
- package/dist/defaults.d.ts +27 -0
- package/dist/defaults.js +28 -0
- package/dist/defaults.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/paginate.d.ts +51 -0
- package/dist/layout/paginate.js +197 -0
- package/dist/layout/paginate.js.map +1 -0
- package/dist/layout/textwrap.d.ts +36 -0
- package/dist/layout/textwrap.js +173 -0
- package/dist/layout/textwrap.js.map +1 -0
- package/dist/manager/api.d.ts +45 -0
- package/dist/manager/api.js +101 -0
- package/dist/manager/api.js.map +1 -0
- package/dist/manager/index.d.ts +2 -0
- package/dist/manager/index.js +3 -0
- package/dist/manager/index.js.map +1 -0
- package/dist/manager/types.d.ts +50 -0
- package/dist/manager/types.js +8 -0
- package/dist/manager/types.js.map +1 -0
- package/dist/manager.d.ts +5 -0
- package/dist/manager.js +6 -0
- package/dist/manager.js.map +1 -0
- package/dist/pdf/font-loader.d.ts +5 -0
- package/dist/pdf/font-loader.js +84 -0
- package/dist/pdf/font-loader.js.map +1 -0
- package/dist/pdf/index.d.ts +2 -0
- package/dist/pdf/index.js +3 -0
- package/dist/pdf/index.js.map +1 -0
- package/dist/pdf/render-pdf.d.ts +19 -0
- package/dist/pdf/render-pdf.js +284 -0
- package/dist/pdf/render-pdf.js.map +1 -0
- package/dist/pdf.d.ts +6 -0
- package/dist/pdf.js +7 -0
- package/dist/pdf.js.map +1 -0
- package/dist/render/canvas.d.ts +61 -0
- package/dist/render/canvas.js +402 -0
- package/dist/render/canvas.js.map +1 -0
- package/dist/render/dom.d.ts +10 -0
- package/dist/render/dom.js +311 -0
- package/dist/render/dom.js.map +1 -0
- package/dist/render/grid-geometry.d.ts +41 -0
- package/dist/render/grid-geometry.js +131 -0
- package/dist/render/grid-geometry.js.map +1 -0
- package/dist/render/style.d.ts +40 -0
- package/dist/render/style.js +39 -0
- package/dist/render/style.js.map +1 -0
- package/dist/render/window.d.ts +23 -0
- package/dist/render/window.js +58 -0
- package/dist/render/window.js.map +1 -0
- package/dist/schema/render-plan.d.ts +96 -0
- package/dist/schema/render-plan.js +25 -0
- package/dist/schema/render-plan.js.map +1 -0
- package/dist/schema/validate.d.ts +15 -0
- package/dist/schema/validate.js +129 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/sql/ast-check.d.ts +14 -0
- package/dist/sql/ast-check.js +44 -0
- package/dist/sql/ast-check.js.map +1 -0
- package/dist/sql/compile.d.ts +31 -0
- package/dist/sql/compile.js +145 -0
- package/dist/sql/compile.js.map +1 -0
- package/dist/sql/expr.d.ts +21 -0
- package/dist/sql/expr.js +123 -0
- package/dist/sql/expr.js.map +1 -0
- package/dist/sql/index.d.ts +4 -0
- package/dist/sql/index.js +5 -0
- package/dist/sql/index.js.map +1 -0
- package/dist/sql/tokenizer.d.ts +25 -0
- package/dist/sql/tokenizer.js +124 -0
- package/dist/sql/tokenizer.js.map +1 -0
- package/dist/sql.d.ts +5 -0
- package/dist/sql.js +6 -0
- package/dist/sql.js.map +1 -0
- package/dist/units.d.ts +11 -0
- package/dist/units.js +26 -0
- package/dist/units.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 页级视口虚拟化:给定每页高度与滚动位置,返回需要真实渲染的页区间
|
|
3
|
+
* 与上下占位高度。start/end 为闭区间索引。
|
|
4
|
+
*
|
|
5
|
+
* 布局模型:每页占据 height + gap(gap 为页间距,默认 0),
|
|
6
|
+
* padTop/padBottom 同口径累计,保证 占位+渲染页+间距 还原总滚动高度。
|
|
7
|
+
* 内部用前缀和 + 二分定位覆盖页与可见终点。
|
|
8
|
+
*/
|
|
9
|
+
export function computePageWindow(input) {
|
|
10
|
+
const { pageHeightsPx, viewportHeightPx, scrollTopPx, overscan = 1, gapPx = 0 } = input;
|
|
11
|
+
const n = pageHeightsPx.length;
|
|
12
|
+
if (n === 0) {
|
|
13
|
+
return { start: 0, end: -1, padTopPx: 0, padBottomPx: 0 };
|
|
14
|
+
}
|
|
15
|
+
const gap = Math.max(0, gapPx);
|
|
16
|
+
const offsets = new Array(n + 1);
|
|
17
|
+
offsets[0] = 0;
|
|
18
|
+
for (let i = 0; i < n; i++) {
|
|
19
|
+
offsets[i + 1] = offsets[i] + Math.max(0, pageHeightsPx[i] ?? 0) + gap;
|
|
20
|
+
}
|
|
21
|
+
const total = offsets[n];
|
|
22
|
+
const top = Math.max(0, scrollTopPx);
|
|
23
|
+
// 覆盖页:最后一个 offsets[i] <= top 的页(滚动位置所在页)
|
|
24
|
+
let cover = 0;
|
|
25
|
+
let lo = 0;
|
|
26
|
+
let hi = n - 1;
|
|
27
|
+
while (lo <= hi) {
|
|
28
|
+
const mid = (lo + hi) >> 1;
|
|
29
|
+
if (offsets[mid] <= top) {
|
|
30
|
+
cover = mid;
|
|
31
|
+
lo = mid + 1;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
hi = mid - 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const start = Math.max(0, Math.min(n - 1, cover - overscan));
|
|
38
|
+
// 可见终点:从覆盖页页顶起累计一屏(+间距)的最后一页
|
|
39
|
+
const target = offsets[cover] + Math.max(0, viewportHeightPx);
|
|
40
|
+
let visibleEnd = n - 1;
|
|
41
|
+
lo = 0;
|
|
42
|
+
hi = n - 1;
|
|
43
|
+
while (lo <= hi) {
|
|
44
|
+
const mid = (lo + hi) >> 1;
|
|
45
|
+
if (offsets[mid + 1] < target) {
|
|
46
|
+
lo = mid + 1;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
visibleEnd = mid;
|
|
50
|
+
hi = mid - 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const end = Math.min(n - 1, visibleEnd + overscan);
|
|
54
|
+
const padTopPx = offsets[start];
|
|
55
|
+
const padBottomPx = Math.max(0, total - offsets[end + 1]);
|
|
56
|
+
return { start, end, padTopPx, padBottomPx };
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"window.js","sourceRoot":"","sources":["../../src/render/window.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type ComponentKind = "text" | "image" | "barcode" | "qrcode" | "chart" | "line" | "rect" | "ellipse";
|
|
2
|
+
export interface ComponentStyle {
|
|
3
|
+
fontSizePx?: number;
|
|
4
|
+
fontSize?: number;
|
|
5
|
+
fontFamily?: string;
|
|
6
|
+
bold?: boolean;
|
|
7
|
+
italic?: boolean;
|
|
8
|
+
underline?: boolean;
|
|
9
|
+
color?: string;
|
|
10
|
+
backgroundColor?: string;
|
|
11
|
+
align?: "left" | "center" | "right" | "justify";
|
|
12
|
+
verticalAlign?: "top" | "middle" | "bottom";
|
|
13
|
+
lineColor?: string;
|
|
14
|
+
borderColor?: string;
|
|
15
|
+
lineWidthPt?: number;
|
|
16
|
+
borderTop?: boolean;
|
|
17
|
+
borderRight?: boolean;
|
|
18
|
+
borderBottom?: boolean;
|
|
19
|
+
borderLeft?: boolean;
|
|
20
|
+
lineHeight?: number;
|
|
21
|
+
letterSpacing?: number;
|
|
22
|
+
opacity?: number;
|
|
23
|
+
rotateDeg?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface RichParagraph {
|
|
26
|
+
segments: RichSegment[];
|
|
27
|
+
align?: ComponentStyle["align"];
|
|
28
|
+
lineHeight?: number;
|
|
29
|
+
spaceBeforeMm?: number;
|
|
30
|
+
spaceAfterMm?: number;
|
|
31
|
+
indentMm?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface RichSegment {
|
|
34
|
+
kind?: "text" | "field";
|
|
35
|
+
text?: string;
|
|
36
|
+
field?: string;
|
|
37
|
+
style?: ComponentStyle;
|
|
38
|
+
}
|
|
39
|
+
export interface GridCell {
|
|
40
|
+
text?: string;
|
|
41
|
+
rawText?: string;
|
|
42
|
+
children?: PlanComponent[];
|
|
43
|
+
colSpan?: number;
|
|
44
|
+
rowSpan?: number;
|
|
45
|
+
style?: ComponentStyle;
|
|
46
|
+
}
|
|
47
|
+
export interface PlanGrid {
|
|
48
|
+
/** 行内允许 null 空位(jh4j 契约数据存在稀疏行;渲染器按空单元格处理) */
|
|
49
|
+
cells: (GridCell | null)[][];
|
|
50
|
+
colWidthsMm?: number[];
|
|
51
|
+
rowHeightsMm?: number[];
|
|
52
|
+
gapMm?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface PlanComponent {
|
|
55
|
+
kind: ComponentKind;
|
|
56
|
+
leftMm: number;
|
|
57
|
+
topMm: number;
|
|
58
|
+
widthMm: number;
|
|
59
|
+
heightMm: number;
|
|
60
|
+
nid?: string;
|
|
61
|
+
rkey?: string;
|
|
62
|
+
text?: string;
|
|
63
|
+
rawText?: string;
|
|
64
|
+
lines?: string[];
|
|
65
|
+
richParagraphs?: RichParagraph[];
|
|
66
|
+
imageData?: string;
|
|
67
|
+
grid?: PlanGrid;
|
|
68
|
+
style?: ComponentStyle;
|
|
69
|
+
meta?: Record<string, unknown>;
|
|
70
|
+
/** MachReport 扩展字段:未知 kind 的兜底渲染标记 */
|
|
71
|
+
fallbackText?: string;
|
|
72
|
+
}
|
|
73
|
+
export interface PlanPage {
|
|
74
|
+
pageWidthMm: number;
|
|
75
|
+
pageHeightMm: number;
|
|
76
|
+
marginTopMm?: number;
|
|
77
|
+
marginBottomMm?: number;
|
|
78
|
+
marginLeftMm?: number;
|
|
79
|
+
marginRightMm?: number;
|
|
80
|
+
components: PlanComponent[];
|
|
81
|
+
}
|
|
82
|
+
export interface GridPlanResponse {
|
|
83
|
+
code?: number;
|
|
84
|
+
message?: string;
|
|
85
|
+
data?: {
|
|
86
|
+
pages?: PlanPage[];
|
|
87
|
+
} | null;
|
|
88
|
+
}
|
|
89
|
+
export interface RenderPlan {
|
|
90
|
+
schemaVersion: string;
|
|
91
|
+
pages: PlanPage[];
|
|
92
|
+
}
|
|
93
|
+
export declare const RENDER_PLAN_SCHEMA_VERSION = "1.0.0-mach";
|
|
94
|
+
export declare function toRenderPlan(response: GridPlanResponse): RenderPlan;
|
|
95
|
+
export declare function isImageBackedComponent(comp: PlanComponent): boolean;
|
|
96
|
+
export declare function isRichText(comp: PlanComponent): boolean;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SCHEMA_VERSION_MACH } from "../defaults";
|
|
2
|
+
export const RENDER_PLAN_SCHEMA_VERSION = SCHEMA_VERSION_MACH;
|
|
3
|
+
export function toRenderPlan(response) {
|
|
4
|
+
if (response && response.code != null && response.code !== 200) {
|
|
5
|
+
throw new Error(String(response.message || "渲染计划加载失败"));
|
|
6
|
+
}
|
|
7
|
+
const pages = response?.data?.pages;
|
|
8
|
+
return {
|
|
9
|
+
schemaVersion: RENDER_PLAN_SCHEMA_VERSION,
|
|
10
|
+
pages: Array.isArray(pages) ? pages : []
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function isImageBackedComponent(comp) {
|
|
14
|
+
return ((comp.kind === "image" ||
|
|
15
|
+
comp.kind === "barcode" ||
|
|
16
|
+
comp.kind === "qrcode" ||
|
|
17
|
+
comp.kind === "chart") &&
|
|
18
|
+
!!comp.imageData);
|
|
19
|
+
}
|
|
20
|
+
export function isRichText(comp) {
|
|
21
|
+
return (comp.kind === "text" &&
|
|
22
|
+
Array.isArray(comp.richParagraphs) &&
|
|
23
|
+
comp.richParagraphs.length > 0);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=render-plan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-plan.js","sourceRoot":"","sources":["../../src/schema/render-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA8GlD,MAAM,CAAC,MAAM,0BAA0B,GAAG,mBAAmB,CAAC;AAE9D,MAAM,UAAU,YAAY,CAAC,QAA0B;IACrD,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,IAAI,IAAI,IAAI,QAAQ,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,IAAI,UAAU,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC;IACpC,OAAO;QACL,aAAa,EAAE,0BAA0B;QACzC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;KACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,IAAmB;IACxD,OAAO,CACL,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO;QACpB,IAAI,CAAC,IAAI,KAAK,SAAS;QACvB,IAAI,CAAC,IAAI,KAAK,QAAQ;QACtB,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;QACxB,CAAC,CAAC,IAAI,CAAC,SAAS,CACjB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAmB;IAC5C,OAAO,CACL,IAAI,CAAC,IAAI,KAAK,MAAM;QACpB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC;QAClC,IAAI,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAC/B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PlanComponent, PlanPage } from "./render-plan";
|
|
2
|
+
export interface ValidationError {
|
|
3
|
+
path: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ValidationResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
errors: ValidationError[];
|
|
9
|
+
warnings: ValidationError[];
|
|
10
|
+
}
|
|
11
|
+
export declare function validateComponent(comp: unknown, path: string, errors: ValidationError[], warnings?: ValidationError[]): void;
|
|
12
|
+
export declare function validatePage(page: unknown, path: string, errors: ValidationError[], warnings?: ValidationError[]): void;
|
|
13
|
+
export declare function validateRenderPlan(input: unknown): ValidationResult;
|
|
14
|
+
export declare function assertRenderPlan(input: unknown): PlanPage[];
|
|
15
|
+
export type { PlanComponent, PlanPage };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const KNOWN_KINDS = new Set([
|
|
2
|
+
"text",
|
|
3
|
+
"image",
|
|
4
|
+
"barcode",
|
|
5
|
+
"qrcode",
|
|
6
|
+
"chart",
|
|
7
|
+
"line",
|
|
8
|
+
"rect",
|
|
9
|
+
"ellipse"
|
|
10
|
+
]);
|
|
11
|
+
function isObj(v) {
|
|
12
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
13
|
+
}
|
|
14
|
+
function checkNumber(obj, key, path, errors) {
|
|
15
|
+
const v = obj[key];
|
|
16
|
+
if (v === undefined || v === null)
|
|
17
|
+
return;
|
|
18
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
19
|
+
errors.push({ path: `${path}.${key}`, message: `应为有限数字,实际 ${JSON.stringify(v)}` });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function validateComponent(comp, path, errors, warnings = []) {
|
|
23
|
+
if (!isObj(comp)) {
|
|
24
|
+
errors.push({ path, message: "组件应为对象" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const kind = comp["kind"];
|
|
28
|
+
if (typeof kind !== "string") {
|
|
29
|
+
errors.push({ path: `${path}.kind`, message: `组件缺少 kind 字符串` });
|
|
30
|
+
}
|
|
31
|
+
else if (!KNOWN_KINDS.has(kind)) {
|
|
32
|
+
warnings.push({
|
|
33
|
+
path: `${path}.kind`,
|
|
34
|
+
message: `未知组件类型 ${JSON.stringify(kind)},将以兜底方式渲染`
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
for (const key of ["leftMm", "topMm", "widthMm", "heightMm"]) {
|
|
38
|
+
checkNumber(comp, key, path, errors);
|
|
39
|
+
}
|
|
40
|
+
if (comp["leftMm"] != null && comp["widthMm"] != null) {
|
|
41
|
+
if (comp["leftMm"] < -100 || comp["widthMm"] < 0) {
|
|
42
|
+
errors.push({ path, message: "几何数值异常(leftMm<-100 或 widthMm<0)" });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const grid = comp["grid"];
|
|
46
|
+
if (grid !== undefined) {
|
|
47
|
+
if (!isObj(grid) || !Array.isArray(grid["cells"])) {
|
|
48
|
+
errors.push({ path: `${path}.grid.cells`, message: "grid.cells 应为二维数组" });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
grid["cells"].forEach((row, ri) => {
|
|
52
|
+
if (!Array.isArray(row)) {
|
|
53
|
+
errors.push({ path: `${path}.grid.cells.${ri}`, message: "行应为数组" });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
row.forEach((cell, ci) => {
|
|
57
|
+
if (cell === undefined || cell === null)
|
|
58
|
+
return;
|
|
59
|
+
if (isObj(cell) && Array.isArray(cell["children"])) {
|
|
60
|
+
cell["children"].forEach((child, ki) => {
|
|
61
|
+
validateComponent(child, `${path}.grid.cells.${ri}.${ci}.children.${ki}`, errors, warnings);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const rich = comp["richParagraphs"];
|
|
69
|
+
if (rich !== undefined) {
|
|
70
|
+
if (!Array.isArray(rich)) {
|
|
71
|
+
errors.push({ path: `${path}.richParagraphs`, message: "richParagraphs 应为数组" });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
rich.forEach((para, i) => {
|
|
75
|
+
if (!isObj(para) || !Array.isArray(para["segments"])) {
|
|
76
|
+
errors.push({ path: `${path}.richParagraphs.${i}`, message: "段落缺少 segments 数组" });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function validatePage(page, path, errors, warnings = []) {
|
|
83
|
+
if (!isObj(page)) {
|
|
84
|
+
errors.push({ path, message: "页应为对象" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const w = page["pageWidthMm"];
|
|
88
|
+
const h = page["pageHeightMm"];
|
|
89
|
+
if (typeof w !== "number" || !(w > 0 && w < 3000)) {
|
|
90
|
+
errors.push({ path: `${path}.pageWidthMm`, message: `页宽非法: ${JSON.stringify(w)}` });
|
|
91
|
+
}
|
|
92
|
+
if (typeof h !== "number" || !(h > 0 && h < 5000)) {
|
|
93
|
+
errors.push({ path: `${path}.pageHeightMm`, message: `页高非法: ${JSON.stringify(h)}` });
|
|
94
|
+
}
|
|
95
|
+
const comps = page["components"];
|
|
96
|
+
if (comps !== undefined && !Array.isArray(comps)) {
|
|
97
|
+
errors.push({ path: `${path}.components`, message: "components 应为数组" });
|
|
98
|
+
}
|
|
99
|
+
else if (Array.isArray(comps)) {
|
|
100
|
+
comps.forEach((comp, i) => validateComponent(comp, `${path}.components.${i}`, errors, warnings));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function validateRenderPlan(input) {
|
|
104
|
+
const errors = [];
|
|
105
|
+
const warnings = [];
|
|
106
|
+
const pages = isObj(input) ? input["pages"] : undefined;
|
|
107
|
+
if (!Array.isArray(pages)) {
|
|
108
|
+
errors.push({
|
|
109
|
+
path: "$.pages",
|
|
110
|
+
message: input == null ? "渲染计划为空" : "渲染计划缺少 pages 数组"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
pages.forEach((page, i) => validatePage(page, `$.pages.${i}`, errors, warnings));
|
|
115
|
+
}
|
|
116
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
117
|
+
}
|
|
118
|
+
export function assertRenderPlan(input) {
|
|
119
|
+
const result = validateRenderPlan(input);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
const head = result.errors
|
|
122
|
+
.slice(0, 3)
|
|
123
|
+
.map((e) => `${e.path}: ${e.message}`)
|
|
124
|
+
.join("; ");
|
|
125
|
+
throw new Error(`渲染计划校验失败(${result.errors.length} 处): ${head}`);
|
|
126
|
+
}
|
|
127
|
+
return input.pages;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/schema/validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface AstCheckResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
/** ast 深度校验是否执行(方言不支持时降级词法) */
|
|
4
|
+
degraded: boolean;
|
|
5
|
+
message?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* AST 深度校验(双保险策略):
|
|
9
|
+
* 1. node-sql-parser(MySQL 方言,支持 ? 占位符)AST 顶层必须为 SELECT/集合运算
|
|
10
|
+
* 2. AST 通过后再叠加词法校验(MySQL 方言把 SELECT INTO 解析为别名,AST 层不可见,
|
|
11
|
+
* 由词法层兜底拦截)
|
|
12
|
+
* AST 解析失败(方言不兼容)自动降级为纯词法校验,返回 degraded=true。
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertSingleSelectAst(sql: string): AstCheckResult;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Parser } from "node-sql-parser";
|
|
2
|
+
import { assertSingleSelect } from "./compile";
|
|
3
|
+
function isSelectAst(ast) {
|
|
4
|
+
if (ast == null || typeof ast !== "object")
|
|
5
|
+
return false;
|
|
6
|
+
const type = ast.type;
|
|
7
|
+
return type === "select" || type === "union" || type === "except" || type === "intersect";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* AST 深度校验(双保险策略):
|
|
11
|
+
* 1. node-sql-parser(MySQL 方言,支持 ? 占位符)AST 顶层必须为 SELECT/集合运算
|
|
12
|
+
* 2. AST 通过后再叠加词法校验(MySQL 方言把 SELECT INTO 解析为别名,AST 层不可见,
|
|
13
|
+
* 由词法层兜底拦截)
|
|
14
|
+
* AST 解析失败(方言不兼容)自动降级为纯词法校验,返回 degraded=true。
|
|
15
|
+
*/
|
|
16
|
+
export function assertSingleSelectAst(sql) {
|
|
17
|
+
const astResult = checkAstTopLevel(sql);
|
|
18
|
+
try {
|
|
19
|
+
assertSingleSelect(sql);
|
|
20
|
+
return { ok: true, degraded: !astResult };
|
|
21
|
+
}
|
|
22
|
+
catch (lexError) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
degraded: false,
|
|
26
|
+
message: lexError instanceof Error ? lexError.message : String(lexError)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** 返回 true=AST 校验通过;false=解析失败或顶层非 SELECT(message 由词法层补充) */
|
|
31
|
+
function checkAstTopLevel(sql) {
|
|
32
|
+
try {
|
|
33
|
+
const parser = new Parser();
|
|
34
|
+
const ast = parser.astify(sql, { database: "MySQL" });
|
|
35
|
+
const list = Array.isArray(ast) ? ast : [ast];
|
|
36
|
+
if (list.length > 1)
|
|
37
|
+
return false;
|
|
38
|
+
return isSelectAst(list[0]);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=ast-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-check.js","sourceRoot":"","sources":["../../src/sql/ast-check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAS/C,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,GAAG,IAAI,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACzD,MAAM,IAAI,GAAI,GAA0B,CAAC,IAAI,CAAC;IAC9C,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,WAAW,CAAC;AAC5F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,CAAC;QACH,kBAAkB,CAAC,GAAG,CAAC,CAAC;QACxB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,SAAS,EAAE,CAAC;IAC5C,CAAC;IAAC,OAAO,QAAQ,EAAE,CAAC;QAClB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,QAAQ,YAAY,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;SACzE,CAAC;IACJ,CAAC;AACH,CAAC;AAED,6DAA6D;AAC7D,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAClC,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { SqlSyntaxError } from "./tokenizer";
|
|
2
|
+
export interface RenderedSql {
|
|
3
|
+
sql: string;
|
|
4
|
+
binds: unknown[];
|
|
5
|
+
warnings: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface CompiledSql {
|
|
8
|
+
original: string;
|
|
9
|
+
/** 参数名集合(bind 与 subst 并集) */
|
|
10
|
+
paramNames: string[];
|
|
11
|
+
render(params: Record<string, unknown>): RenderedSql;
|
|
12
|
+
}
|
|
13
|
+
export declare class SqlValidationError extends Error {
|
|
14
|
+
readonly sql: string;
|
|
15
|
+
constructor(message: string, sql: string);
|
|
16
|
+
}
|
|
17
|
+
export declare function compileDynamicSql(source: string): CompiledSql;
|
|
18
|
+
/**
|
|
19
|
+
* 去注释:-- 行注释、# 行注释(仅行首/空白后,且排除 #{ 绑定占位)、/* 块注释。
|
|
20
|
+
* 注意:此函数用于校验口径,输出会折叠空白。
|
|
21
|
+
*/
|
|
22
|
+
export declare function stripSqlComments(sql: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* 单 SELECT 结构校验(词法层;AST 级校验见 ast-check.ts 双保险):
|
|
25
|
+
* - 去注释后必须以 select 或 with 开头
|
|
26
|
+
* - 语句内除末尾外不允许出现分号
|
|
27
|
+
* - 顶层禁止 DML/DDL/调用关键字(字符串字面量内容不参与匹配)
|
|
28
|
+
*/
|
|
29
|
+
export declare function assertSingleSelect(sql: string): void;
|
|
30
|
+
/** 便捷入口:编译 + 渲染 + 校验一体 */
|
|
31
|
+
export declare function renderDynamicSql(source: string, params: Record<string, unknown>): RenderedSql;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { evalExpr, ExprError, parseIfArgs } from "./expr";
|
|
2
|
+
import { SqlSyntaxError, tokenize } from "./tokenizer";
|
|
3
|
+
export { SqlSyntaxError } from "./tokenizer";
|
|
4
|
+
export class SqlValidationError extends Error {
|
|
5
|
+
constructor(message, sql) {
|
|
6
|
+
super(`SQL 校验失败: ${message}`);
|
|
7
|
+
this.sql = sql;
|
|
8
|
+
this.name = "SqlValidationError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const FORBIDDEN_TOP_LEVEL = /\b(insert|update|delete|merge|create|alter|drop|truncate|grant|revoke|call|exec|execute)\b/i;
|
|
12
|
+
export function compileDynamicSql(source) {
|
|
13
|
+
let tokens;
|
|
14
|
+
try {
|
|
15
|
+
tokens = tokenize(source);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
if (e instanceof SqlSyntaxError)
|
|
19
|
+
throw e;
|
|
20
|
+
throw new SqlSyntaxError(String(e), 0, source);
|
|
21
|
+
}
|
|
22
|
+
const paramNames = new Set();
|
|
23
|
+
for (const t of tokens) {
|
|
24
|
+
if (t.type === "bind" || t.type === "subst")
|
|
25
|
+
paramNames.add(t.value);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
original: source,
|
|
29
|
+
paramNames: [...paramNames],
|
|
30
|
+
render(params) {
|
|
31
|
+
const binds = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
let out = "";
|
|
34
|
+
for (const t of tokens) {
|
|
35
|
+
switch (t.type) {
|
|
36
|
+
case "literal":
|
|
37
|
+
out += t.text;
|
|
38
|
+
break;
|
|
39
|
+
case "bind": {
|
|
40
|
+
const name = t.value;
|
|
41
|
+
if (!(name in params)) {
|
|
42
|
+
warnings.push(`参数 ${name} 未提供,按 NULL 绑定`);
|
|
43
|
+
binds.push(null);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
binds.push(params[name]);
|
|
47
|
+
}
|
|
48
|
+
out += "?";
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case "subst": {
|
|
52
|
+
const name = t.value;
|
|
53
|
+
if (!(name in params)) {
|
|
54
|
+
warnings.push(`参数 ${name} 未提供,文本替换为空串`);
|
|
55
|
+
out += "";
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
out += String(params[name]);
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "if": {
|
|
63
|
+
const { cond, whenTrue, whenFalse } = parseIfArgs(t.value);
|
|
64
|
+
let truthy;
|
|
65
|
+
try {
|
|
66
|
+
const v = evalExpr(cond, { params });
|
|
67
|
+
truthy = v === true || (typeof v === "string" && v !== "") || (typeof v === "number" && v !== 0);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
throw e instanceof ExprError ? e : new ExprError(String(e));
|
|
71
|
+
}
|
|
72
|
+
const branch = truthy ? whenTrue : whenFalse;
|
|
73
|
+
let branchText;
|
|
74
|
+
try {
|
|
75
|
+
branchText = String(evalExpr(branch, { params }));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
branchText = branch;
|
|
79
|
+
}
|
|
80
|
+
const sub = compileDynamicSql(branchText);
|
|
81
|
+
const rendered = sub.render(params);
|
|
82
|
+
binds.push(...rendered.binds);
|
|
83
|
+
warnings.push(...rendered.warnings);
|
|
84
|
+
out += rendered.sql;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "escape":
|
|
88
|
+
out += t.text;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { sql: out, binds, warnings };
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 去注释:-- 行注释、# 行注释(仅行首/空白后,且排除 #{ 绑定占位)、/* 块注释。
|
|
98
|
+
* 注意:此函数用于校验口径,输出会折叠空白。
|
|
99
|
+
*/
|
|
100
|
+
export function stripSqlComments(sql) {
|
|
101
|
+
return sql
|
|
102
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
103
|
+
.replace(/(^|\s)--[^\n]*/g, "$1 ")
|
|
104
|
+
.replace(/(^|\s)#(?!\{)[^\n]*/g, "$1 ")
|
|
105
|
+
.replace(/\s+/g, " ")
|
|
106
|
+
.trim();
|
|
107
|
+
}
|
|
108
|
+
/** 剥离单引号字符串字面量(内容不参与关键字检查,防 'xxx into yyy' 误伤) */
|
|
109
|
+
function stripStringLiterals(sql) {
|
|
110
|
+
return sql.replace(/'(?:[^']|'')*'/g, "''");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 单 SELECT 结构校验(词法层;AST 级校验见 ast-check.ts 双保险):
|
|
114
|
+
* - 去注释后必须以 select 或 with 开头
|
|
115
|
+
* - 语句内除末尾外不允许出现分号
|
|
116
|
+
* - 顶层禁止 DML/DDL/调用关键字(字符串字面量内容不参与匹配)
|
|
117
|
+
*/
|
|
118
|
+
export function assertSingleSelect(sql) {
|
|
119
|
+
const clean = stripSqlComments(sql);
|
|
120
|
+
if (clean === "")
|
|
121
|
+
throw new SqlValidationError("空 SQL", sql);
|
|
122
|
+
if (!/^(select|with)\b/i.test(clean)) {
|
|
123
|
+
throw new SqlValidationError("仅允许 SELECT 查询(需以 select/with 开头)", sql);
|
|
124
|
+
}
|
|
125
|
+
const body = clean.endsWith(";") ? clean.slice(0, -1) : clean;
|
|
126
|
+
if (body.includes(";")) {
|
|
127
|
+
throw new SqlValidationError("检测到多语句(分号),仅允许单条 SELECT", sql);
|
|
128
|
+
}
|
|
129
|
+
const codeOnly = stripStringLiterals(body);
|
|
130
|
+
const m = FORBIDDEN_TOP_LEVEL.exec(codeOnly);
|
|
131
|
+
if (m) {
|
|
132
|
+
throw new SqlValidationError(`禁止的关键字: ${m[1]}`, sql);
|
|
133
|
+
}
|
|
134
|
+
if (/\binto\b/i.test(codeOnly)) {
|
|
135
|
+
throw new SqlValidationError("禁止 SELECT INTO", sql);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** 便捷入口:编译 + 渲染 + 校验一体 */
|
|
139
|
+
export function renderDynamicSql(source, params) {
|
|
140
|
+
const compiled = compileDynamicSql(source);
|
|
141
|
+
const rendered = compiled.render(params);
|
|
142
|
+
assertSingleSelect(rendered.sql);
|
|
143
|
+
return rendered;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/sql/compile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ExprContext {
|
|
2
|
+
params: Record<string, unknown>;
|
|
3
|
+
}
|
|
4
|
+
export declare class ExprError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 求值 jh4j 公式表达式(用于 {if(cond, a, b)} 的 cond 与操作数):
|
|
9
|
+
* - #param 引用(渲染时被替换为参数值;此实现按值求值)
|
|
10
|
+
* - "..." 字符串字面量(仅双引号,与 jh4j 一致)
|
|
11
|
+
* - isEmpty(x)、isNotEmpty(x)
|
|
12
|
+
* - == / != 比较
|
|
13
|
+
* - + 拼接
|
|
14
|
+
*/
|
|
15
|
+
export declare function evalExpr(input: string, ctx: ExprContext): unknown;
|
|
16
|
+
/** {if(cond, whenTrue, whenFalse)} 的参数拆分(2 或 3 段,兼容 jh4j 三段式) */
|
|
17
|
+
export declare function parseIfArgs(inner: string): {
|
|
18
|
+
cond: string;
|
|
19
|
+
whenTrue: string;
|
|
20
|
+
whenFalse: string;
|
|
21
|
+
};
|