@happyvertical/smrt-reports 0.37.2 → 0.37.3
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/aggregate.js +1 -5
- package/dist/compiler.js +138 -185
- package/dist/compiler.js.map +1 -1
- package/dist/decorators.js +53 -68
- package/dist/decorators.js.map +1 -1
- package/dist/index.js +80 -152
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/refresh.js +423 -667
- package/dist/refresh.js.map +1 -1
- package/dist/scheduler.js +330 -395
- package/dist/scheduler.js.map +1 -1
- package/dist/smrt-knowledge.json +7 -7
- package/dist/state.js +305 -305
- package/dist/state.js.map +1 -1
- package/dist/types.js +0 -2
- package/package.json +9 -9
- package/dist/aggregate.js.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/refresh.js
CHANGED
|
@@ -1,428 +1,344 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildReportDefinition, compileReportDefinition, getReportGroupingColumns } from "./compiler.js";
|
|
2
|
+
import { REPORT_LOCKS_TABLE, REPORT_RUNS_TABLE, REPORT_WATERMARKS_TABLE, assertReportTablesReady, scopeKeyForTenant } from "./state.js";
|
|
3
|
+
import { buildAggregate, buildWhere, tableExists, validateColumnName } from "@happyvertical/sql";
|
|
2
4
|
import { ObjectRegistry } from "@happyvertical/smrt-core";
|
|
3
5
|
import { toSnakeCase } from "@happyvertical/smrt-core/utils";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import { assertReportTablesReady, REPORT_RUNS_TABLE, REPORT_WATERMARKS_TABLE, REPORT_LOCKS_TABLE, scopeKeyForTenant } from "./state.js";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { getTenantId, withTenant } from "@happyvertical/smrt-tenancy";
|
|
8
|
+
//#region src/refresh.ts
|
|
8
9
|
function isSqlAdapterType(value) {
|
|
9
|
-
|
|
10
|
+
return value === "sqlite" || value === "postgres" || value === "duckdb" || value === "json";
|
|
10
11
|
}
|
|
11
12
|
function adapterTypeFromDb(db, fallback) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return "duckdb";
|
|
19
|
-
}
|
|
20
|
-
if (url === ":memory:" || url.endsWith(".sqlite") || url.endsWith(".db")) {
|
|
21
|
-
return "sqlite";
|
|
22
|
-
}
|
|
23
|
-
throw new Error(
|
|
24
|
-
"Report refresh requires a database adapter type. Pass { adapterType }."
|
|
25
|
-
);
|
|
13
|
+
if (isSqlAdapterType(fallback)) return fallback;
|
|
14
|
+
const url = String(db.url ?? "");
|
|
15
|
+
if (url.startsWith("postgres://") || url.startsWith("postgresql://")) return "postgres";
|
|
16
|
+
if (url.endsWith(".duckdb")) return "duckdb";
|
|
17
|
+
if (url === ":memory:" || url.endsWith(".sqlite") || url.endsWith(".db")) return "sqlite";
|
|
18
|
+
throw new Error("Report refresh requires a database adapter type. Pass { adapterType }.");
|
|
26
19
|
}
|
|
27
20
|
function stableReportId(values) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
const hash = createHash("sha256").update(JSON.stringify(values)).digest("hex");
|
|
22
|
+
const variant = (Number.parseInt(hash[16], 16) & 3 | 8).toString(16);
|
|
23
|
+
return [
|
|
24
|
+
hash.slice(0, 8),
|
|
25
|
+
hash.slice(8, 12),
|
|
26
|
+
`4${hash.slice(13, 16)}`,
|
|
27
|
+
`${variant}${hash.slice(17, 20)}`,
|
|
28
|
+
hash.slice(20, 32)
|
|
29
|
+
].join("-");
|
|
37
30
|
}
|
|
38
31
|
function getReportTableName(reportCtor) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
return tableName;
|
|
32
|
+
const registered = ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
|
|
33
|
+
const tableName = registered ? ObjectRegistry.getTableName(registered.qualifiedName ?? registered.name) : ObjectRegistry.getTableName(reportCtor.name);
|
|
34
|
+
if (!tableName) throw new Error(`No report table registered for ${reportCtor.name}`);
|
|
35
|
+
return tableName;
|
|
45
36
|
}
|
|
46
37
|
function getActualGroupingColumns(definition) {
|
|
47
|
-
|
|
48
|
-
(field) => field.report?.kind === "group" || field.report?.kind === "bucket"
|
|
49
|
-
).map((field) => field.columnName ?? toSnakeCase(field.fieldName));
|
|
38
|
+
return definition.fields.filter((field) => field.report?.kind === "group" || field.report?.kind === "bucket").map((field) => field.columnName ?? toSnakeCase(field.fieldName));
|
|
50
39
|
}
|
|
51
40
|
function materializeRows(rows, definition, refreshedAt, scope) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
41
|
+
const groupingColumns = getActualGroupingColumns(definition);
|
|
42
|
+
const now = refreshedAt.toISOString();
|
|
43
|
+
return rows.map((row, index) => {
|
|
44
|
+
const id = stableReportId(groupingColumns.length === 0 ? [
|
|
45
|
+
definition.reportClassName,
|
|
46
|
+
scope.scopeKey,
|
|
47
|
+
index
|
|
48
|
+
] : [
|
|
49
|
+
definition.reportClassName,
|
|
50
|
+
scope.scopeKey,
|
|
51
|
+
...groupingColumns.map((column) => row[column])
|
|
52
|
+
]);
|
|
53
|
+
const tenantFields = scope.reportTenantColumn ? { [scope.reportTenantColumn]: scope.tenantId } : {};
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
slug: id,
|
|
57
|
+
context: scope.scopeKey,
|
|
58
|
+
...tenantFields,
|
|
59
|
+
...row,
|
|
60
|
+
refreshed_at: now,
|
|
61
|
+
created_at: now,
|
|
62
|
+
updated_at: now
|
|
63
|
+
};
|
|
64
|
+
});
|
|
73
65
|
}
|
|
74
66
|
function sourceClassName(source) {
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
if (typeof source === "string") return source;
|
|
68
|
+
return source.name;
|
|
77
69
|
}
|
|
78
70
|
async function fieldColumn(className, requested) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return requestedColumn;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
71
|
+
const fields = await ObjectRegistry.getAllFields(className);
|
|
72
|
+
const requestedColumn = toSnakeCase(requested);
|
|
73
|
+
for (const [fieldName] of fields.entries()) if (fieldName === requested || toSnakeCase(fieldName) === requestedColumn) return requestedColumn;
|
|
74
|
+
return null;
|
|
87
75
|
}
|
|
88
76
|
async function tenantColumn(className) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const fields = await ObjectRegistry.getAllFields(className);
|
|
95
|
-
for (const [fieldName, field] of fields.entries()) {
|
|
96
|
-
if (field?._meta?.__tenancy?.isTenantIdField) {
|
|
97
|
-
return toSnakeCase(fieldName);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const tenantIdField = await fieldColumn(className, "tenantId");
|
|
101
|
-
return tenantIdField;
|
|
77
|
+
const configuredField = ObjectRegistry.getClass(className)?.tenantScopedConfig?.field;
|
|
78
|
+
if (configuredField) return toSnakeCase(configuredField);
|
|
79
|
+
const fields = await ObjectRegistry.getAllFields(className);
|
|
80
|
+
for (const [fieldName, field] of fields.entries()) if (field?._meta?.__tenancy?.isTenantIdField) return toSnakeCase(fieldName);
|
|
81
|
+
return await fieldColumn(className, "tenantId");
|
|
102
82
|
}
|
|
103
83
|
async function resolveScope(definition, options) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
return {
|
|
118
|
-
tenantId,
|
|
119
|
-
scopeKey: scopeKeyForTenant(tenantId),
|
|
120
|
-
sourceTenantColumn,
|
|
121
|
-
reportTenantColumn
|
|
122
|
-
};
|
|
84
|
+
const tenantId = options.tenantId !== void 0 ? options.tenantId : getTenantId() ?? null;
|
|
85
|
+
const sourceTenantColumn = await tenantColumn(definition.sourceClassName);
|
|
86
|
+
const reportTenantColumn = await tenantColumn(definition.reportClassName);
|
|
87
|
+
if (tenantId && !sourceTenantColumn) throw new Error(`Tenant-scoped refresh for ${definition.reportClassName} requires source ${definition.sourceClassName} to expose a tenantId field.`);
|
|
88
|
+
if (tenantId && !reportTenantColumn) throw new Error(`Tenant-scoped refresh for ${definition.reportClassName} requires the report table to expose a tenantId field.`);
|
|
89
|
+
return {
|
|
90
|
+
tenantId,
|
|
91
|
+
scopeKey: scopeKeyForTenant(tenantId),
|
|
92
|
+
sourceTenantColumn,
|
|
93
|
+
reportTenantColumn
|
|
94
|
+
};
|
|
123
95
|
}
|
|
124
96
|
function tenantWhere(scope) {
|
|
125
|
-
|
|
126
|
-
|
|
97
|
+
if (!scope.sourceTenantColumn) return {};
|
|
98
|
+
return { [scope.sourceTenantColumn]: scope.tenantId };
|
|
127
99
|
}
|
|
128
100
|
function andWhere(base, extra) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
101
|
+
if (Object.keys(extra).length === 0) return base;
|
|
102
|
+
if (!base) return extra;
|
|
103
|
+
if (Array.isArray(base)) return base.map((andGroup) => [...andGroup, extra]);
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
...extra
|
|
107
|
+
};
|
|
135
108
|
}
|
|
136
109
|
function andHaving(base, extra) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
110
|
+
if (Object.keys(extra).length === 0) return base;
|
|
111
|
+
if (!base) return extra;
|
|
112
|
+
if (Array.isArray(base)) return base.map((andGroup) => [...andGroup, extra]);
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
...extra
|
|
116
|
+
};
|
|
143
117
|
}
|
|
144
118
|
function withRefreshFilters(spec, scope, extraWhere = {}) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
119
|
+
return {
|
|
120
|
+
...spec,
|
|
121
|
+
where: andWhere(spec.where, {
|
|
122
|
+
...tenantWhere(scope),
|
|
123
|
+
...extraWhere
|
|
124
|
+
})
|
|
125
|
+
};
|
|
152
126
|
}
|
|
153
127
|
async function resolveWatermarkConfig(definition) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!watermarkColumn) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`Incremental report ${definition.reportClassName} requires source ${definition.sourceClassName} to define watermark column '${watermarkField}'.`
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
if (!softDeleteColumn) {
|
|
171
|
-
throw new Error(
|
|
172
|
-
`Incremental report ${definition.reportClassName} requires source ${definition.sourceClassName} to define soft-delete column '${softDeleteField}'.`
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
return { watermarkColumn, softDeleteColumn };
|
|
128
|
+
const configured = definition.refresh ?? {};
|
|
129
|
+
const watermarkField = configured.watermarkColumn ?? "updatedAt";
|
|
130
|
+
const softDeleteField = configured.softDeleteColumn ?? "deletedAt";
|
|
131
|
+
const watermarkColumn = await fieldColumn(definition.sourceClassName, watermarkField);
|
|
132
|
+
const softDeleteColumn = await fieldColumn(definition.sourceClassName, softDeleteField);
|
|
133
|
+
if (!watermarkColumn) throw new Error(`Incremental report ${definition.reportClassName} requires source ${definition.sourceClassName} to define watermark column '${watermarkField}'.`);
|
|
134
|
+
if (!softDeleteColumn) throw new Error(`Incremental report ${definition.reportClassName} requires source ${definition.sourceClassName} to define soft-delete column '${softDeleteField}'.`);
|
|
135
|
+
return {
|
|
136
|
+
watermarkColumn,
|
|
137
|
+
softDeleteColumn
|
|
138
|
+
};
|
|
176
139
|
}
|
|
177
140
|
function compileChangedGroupSpec(definition, scope, watermark, watermarkBefore) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
having: void 0
|
|
191
|
-
},
|
|
192
|
-
scope,
|
|
193
|
-
{ [`${watermark.watermarkColumn} >`]: watermarkBefore }
|
|
194
|
-
);
|
|
141
|
+
const spec = compileReportDefinition(definition);
|
|
142
|
+
const select = spec.select.filter((expr) => !("fn" in expr));
|
|
143
|
+
return withRefreshFilters({
|
|
144
|
+
...spec,
|
|
145
|
+
where: void 0,
|
|
146
|
+
select: select.length > 0 ? select : [{
|
|
147
|
+
fn: "count",
|
|
148
|
+
as: "__smrt_changed_count"
|
|
149
|
+
}],
|
|
150
|
+
groupBy: select.length > 0 ? spec.groupBy : [],
|
|
151
|
+
having: void 0
|
|
152
|
+
}, scope, { [`${watermark.watermarkColumn} >`]: watermarkBefore });
|
|
195
153
|
}
|
|
196
154
|
function compileAffectedGroupSpec(definition, scope, watermark, groupRow) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return {
|
|
205
|
-
...spec,
|
|
206
|
-
having: andHaving(spec.having, groupHaving)
|
|
207
|
-
};
|
|
155
|
+
const groupingColumns = getActualGroupingColumns(definition);
|
|
156
|
+
const groupHaving = Object.fromEntries(groupingColumns.map((column) => [column, groupRow[column]]));
|
|
157
|
+
const spec = withRefreshFilters(compileReportDefinition(definition), scope, { [watermark.softDeleteColumn]: null });
|
|
158
|
+
return {
|
|
159
|
+
...spec,
|
|
160
|
+
having: andHaving(spec.having, groupHaving)
|
|
161
|
+
};
|
|
208
162
|
}
|
|
209
163
|
function conflictColumns(definition, scope) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
...scope.reportTenantColumn ? [scope.reportTenantColumn] : [],
|
|
214
|
-
...groupingColumns
|
|
215
|
-
];
|
|
164
|
+
const groupingColumns = getActualGroupingColumns(definition);
|
|
165
|
+
if (groupingColumns.length === 0) return ["id"];
|
|
166
|
+
return [...scope.reportTenantColumn ? [scope.reportTenantColumn] : [], ...groupingColumns];
|
|
216
167
|
}
|
|
217
168
|
function predicateSql(tableName, predicate) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
169
|
+
validateColumnName(tableName);
|
|
170
|
+
const clauses = [];
|
|
171
|
+
const values = [];
|
|
172
|
+
for (const [column, value] of Object.entries(predicate)) {
|
|
173
|
+
validateColumnName(column);
|
|
174
|
+
if (value === null || value === void 0) {
|
|
175
|
+
clauses.push(`${column} IS NULL`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
clauses.push(`${column} = ?`);
|
|
179
|
+
values.push(value);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
sql: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
|
|
183
|
+
values
|
|
184
|
+
};
|
|
234
185
|
}
|
|
235
186
|
function dedupeGroupRows(definition, rows) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
groups.set(key, groupRow);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return [...groups.values()];
|
|
187
|
+
const groupingColumns = getActualGroupingColumns(definition);
|
|
188
|
+
const groups = /* @__PURE__ */ new Map();
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
if (!row) continue;
|
|
191
|
+
const groupRow = groupingColumns.length === 0 ? {} : Object.fromEntries(groupingColumns.map((column) => [column, row[column] ?? null]));
|
|
192
|
+
const key = groupingColumns.length === 0 ? "__smrt_global_group__" : JSON.stringify(groupingColumns.map((column) => groupRow[column]));
|
|
193
|
+
if (!groups.has(key)) groups.set(key, groupRow);
|
|
194
|
+
}
|
|
195
|
+
return [...groups.values()];
|
|
249
196
|
}
|
|
250
197
|
async function readMaterializedGroups(db, tableName, definition, scope) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
...where.values
|
|
260
|
-
);
|
|
261
|
-
return result.rows.map((row) => parseMaybeJson(row)).filter((row) => Boolean(row));
|
|
198
|
+
const groupingColumns = getActualGroupingColumns(definition);
|
|
199
|
+
if (groupingColumns.length === 0) return [{}];
|
|
200
|
+
const where = scope.reportTenantColumn ? predicateSql(tableName, { [scope.reportTenantColumn]: scope.tenantId }) : {
|
|
201
|
+
sql: "",
|
|
202
|
+
values: []
|
|
203
|
+
};
|
|
204
|
+
const columns = groupingColumns.map((column) => validateColumnName(column));
|
|
205
|
+
return (await db.query(`SELECT ${columns.join(", ")} FROM ${validateColumnName(tableName)} ${where.sql}`, ...where.values)).rows.map((row) => parseMaybeJson(row)).filter((row) => Boolean(row));
|
|
262
206
|
}
|
|
263
207
|
async function deleteMaterializedGroup(db, tableName, definition, scope, groupRow) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
predicate[column] = groupRow[column];
|
|
270
|
-
}
|
|
271
|
-
const where = predicateSql(tableName, predicate);
|
|
272
|
-
await db.query(
|
|
273
|
-
`DELETE FROM ${validateColumnName(tableName)} ${where.sql}`,
|
|
274
|
-
...where.values
|
|
275
|
-
);
|
|
208
|
+
const predicate = {};
|
|
209
|
+
if (scope.reportTenantColumn) predicate[scope.reportTenantColumn] = scope.tenantId;
|
|
210
|
+
for (const column of getActualGroupingColumns(definition)) predicate[column] = groupRow[column];
|
|
211
|
+
const where = predicateSql(tableName, predicate);
|
|
212
|
+
await db.query(`DELETE FROM ${validateColumnName(tableName)} ${where.sql}`, ...where.values);
|
|
276
213
|
}
|
|
277
214
|
async function replaceRows(db, tableName, rows, scope) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
};
|
|
292
|
-
if (db.transaction) {
|
|
293
|
-
await db.transaction(run);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
await run(db);
|
|
215
|
+
validateColumnName(tableName);
|
|
216
|
+
const run = async (tx) => {
|
|
217
|
+
if (scope.reportTenantColumn) {
|
|
218
|
+
const where = predicateSql(tableName, { [scope.reportTenantColumn]: scope.tenantId });
|
|
219
|
+
await tx.query(`DELETE FROM ${tableName} ${where.sql}`, ...where.values);
|
|
220
|
+
} else await tx.query(`DELETE FROM ${tableName}`);
|
|
221
|
+
if (rows.length > 0) await tx.insert(tableName, rows);
|
|
222
|
+
};
|
|
223
|
+
if (db.transaction) {
|
|
224
|
+
await db.transaction(run);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await run(db);
|
|
297
228
|
}
|
|
298
229
|
function parseMaybeJson(value) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
230
|
+
if (value == null) return null;
|
|
231
|
+
if (typeof value !== "string") return value;
|
|
232
|
+
try {
|
|
233
|
+
return JSON.parse(value);
|
|
234
|
+
} catch {
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
306
237
|
}
|
|
307
238
|
function watermarkValue(value) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
239
|
+
if (value === null || value === void 0) return null;
|
|
240
|
+
if (value instanceof Date) return value.toISOString();
|
|
241
|
+
return String(value);
|
|
311
242
|
}
|
|
312
243
|
async function readWatermark(db, definition, scope, watermarkColumn) {
|
|
313
|
-
|
|
314
|
-
`SELECT watermark_value FROM ${REPORT_WATERMARKS_TABLE}
|
|
244
|
+
const row = (await db.query(`SELECT watermark_value FROM ${REPORT_WATERMARKS_TABLE}
|
|
315
245
|
WHERE report_class = ?
|
|
316
246
|
AND scope_key = ?
|
|
317
247
|
AND source_class = ?
|
|
318
248
|
AND watermark_column = ?
|
|
319
|
-
LIMIT 1`,
|
|
320
|
-
|
|
321
|
-
scope.scopeKey,
|
|
322
|
-
definition.sourceClassName,
|
|
323
|
-
watermarkColumn
|
|
324
|
-
);
|
|
325
|
-
const row = result.rows[0];
|
|
326
|
-
return watermarkValue(row?.watermark_value);
|
|
249
|
+
LIMIT 1`, definition.reportClassName, scope.scopeKey, definition.sourceClassName, watermarkColumn)).rows[0];
|
|
250
|
+
return watermarkValue(row?.watermark_value);
|
|
327
251
|
}
|
|
328
252
|
async function writeWatermark(db, definition, scope, watermarkColumn, value, runId) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
253
|
+
if (value === null) return;
|
|
254
|
+
const id = stableReportId([
|
|
255
|
+
"watermark",
|
|
256
|
+
definition.reportClassName,
|
|
257
|
+
scope.scopeKey,
|
|
258
|
+
definition.sourceClassName,
|
|
259
|
+
watermarkColumn
|
|
260
|
+
]);
|
|
261
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
262
|
+
await db.upsert(REPORT_WATERMARKS_TABLE, [
|
|
263
|
+
"report_class",
|
|
264
|
+
"scope_key",
|
|
265
|
+
"source_class",
|
|
266
|
+
"watermark_column"
|
|
267
|
+
], {
|
|
268
|
+
id,
|
|
269
|
+
slug: id,
|
|
270
|
+
context: scope.scopeKey,
|
|
271
|
+
tenant_id: scope.tenantId,
|
|
272
|
+
scope_key: scope.scopeKey,
|
|
273
|
+
report_class: definition.reportClassName,
|
|
274
|
+
source_class: definition.sourceClassName,
|
|
275
|
+
watermark_column: watermarkColumn,
|
|
276
|
+
watermark_value: value,
|
|
277
|
+
last_run_id: runId ?? null,
|
|
278
|
+
created_at: now,
|
|
279
|
+
updated_at: now
|
|
280
|
+
});
|
|
356
281
|
}
|
|
357
282
|
async function maxWatermark(db, definition, scope, watermarkColumn, adapterType) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return watermarkValue(row?.watermark);
|
|
283
|
+
const where = andWhere(void 0, tenantWhere(scope));
|
|
284
|
+
const builtWhere = where ? buildWhere(where, 1, adapterType) : {
|
|
285
|
+
sql: "",
|
|
286
|
+
values: []
|
|
287
|
+
};
|
|
288
|
+
const row = (await db.query(`SELECT MAX(${validateColumnName(watermarkColumn)}) AS watermark FROM ${validateColumnName(definition.sourceTable)} ${builtWhere.sql}`, ...builtWhere.values)).rows[0];
|
|
289
|
+
return watermarkValue(row?.watermark);
|
|
366
290
|
}
|
|
367
291
|
async function startRun(db, definition, scope, options, mode, watermarkBefore) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
});
|
|
394
|
-
return id;
|
|
292
|
+
if (options.trackRuns === false) return void 0;
|
|
293
|
+
const id = randomUUID();
|
|
294
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
295
|
+
await db.insert(REPORT_RUNS_TABLE, {
|
|
296
|
+
id,
|
|
297
|
+
slug: id,
|
|
298
|
+
context: scope.scopeKey,
|
|
299
|
+
tenant_id: scope.tenantId,
|
|
300
|
+
scope_key: scope.scopeKey,
|
|
301
|
+
report_class: definition.reportClassName,
|
|
302
|
+
source_class: definition.sourceClassName,
|
|
303
|
+
mode,
|
|
304
|
+
trigger: options.trigger ?? "manual",
|
|
305
|
+
status: "running",
|
|
306
|
+
started_at: now,
|
|
307
|
+
row_count: 0,
|
|
308
|
+
changed_group_count: 0,
|
|
309
|
+
watermark_before: watermarkBefore ?? null,
|
|
310
|
+
watermark_after: null,
|
|
311
|
+
error: null,
|
|
312
|
+
metadata: { scheduleId: options.scheduleId },
|
|
313
|
+
created_at: now,
|
|
314
|
+
updated_at: now
|
|
315
|
+
});
|
|
316
|
+
return id;
|
|
395
317
|
}
|
|
396
318
|
async function completeRun(db, runId, status, data = {}) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
error,
|
|
409
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
410
|
-
}
|
|
411
|
-
);
|
|
319
|
+
if (!runId) return;
|
|
320
|
+
const error = data.error instanceof Error ? data.error.message : data.error ? String(data.error) : null;
|
|
321
|
+
await db.update(REPORT_RUNS_TABLE, { id: runId }, {
|
|
322
|
+
status,
|
|
323
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
324
|
+
row_count: data.rowCount ?? 0,
|
|
325
|
+
changed_group_count: data.changedGroupCount ?? 0,
|
|
326
|
+
watermark_after: data.watermarkAfter ?? null,
|
|
327
|
+
error,
|
|
328
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
329
|
+
});
|
|
412
330
|
}
|
|
413
331
|
async function acquireLock(db, definition, scope, ttlMs) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
await db.query(
|
|
425
|
-
`INSERT INTO ${REPORT_LOCKS_TABLE} (
|
|
332
|
+
const now = /* @__PURE__ */ new Date();
|
|
333
|
+
const nowIso = now.toISOString();
|
|
334
|
+
const ownerId = randomUUID();
|
|
335
|
+
const expiresAtIso = new Date(now.getTime() + ttlMs).toISOString();
|
|
336
|
+
const id = stableReportId([
|
|
337
|
+
"lock",
|
|
338
|
+
definition.reportClassName,
|
|
339
|
+
scope.scopeKey
|
|
340
|
+
]);
|
|
341
|
+
await db.query(`INSERT INTO ${REPORT_LOCKS_TABLE} (
|
|
426
342
|
id,
|
|
427
343
|
slug,
|
|
428
344
|
context,
|
|
@@ -446,333 +362,173 @@ async function acquireLock(db, definition, scope, ttlMs) {
|
|
|
446
362
|
updated_at = excluded.updated_at
|
|
447
363
|
WHERE ${REPORT_LOCKS_TABLE}.owner_id IS NULL
|
|
448
364
|
OR ${REPORT_LOCKS_TABLE}.expires_at IS NULL
|
|
449
|
-
OR ${REPORT_LOCKS_TABLE}.expires_at <= ?`,
|
|
450
|
-
|
|
451
|
-
id,
|
|
452
|
-
scope.scopeKey,
|
|
453
|
-
scope.tenantId,
|
|
454
|
-
scope.scopeKey,
|
|
455
|
-
definition.reportClassName,
|
|
456
|
-
ownerId,
|
|
457
|
-
nowIso,
|
|
458
|
-
nowIso,
|
|
459
|
-
expiresAtIso,
|
|
460
|
-
nowIso,
|
|
461
|
-
nowIso,
|
|
462
|
-
nowIso
|
|
463
|
-
);
|
|
464
|
-
const claimed = await db.query(
|
|
465
|
-
`SELECT owner_id FROM ${REPORT_LOCKS_TABLE}
|
|
365
|
+
OR ${REPORT_LOCKS_TABLE}.expires_at <= ?`, id, id, scope.scopeKey, scope.tenantId, scope.scopeKey, definition.reportClassName, ownerId, nowIso, nowIso, expiresAtIso, nowIso, nowIso, nowIso);
|
|
366
|
+
if ((await db.query(`SELECT owner_id FROM _smrt_report_locks
|
|
466
367
|
WHERE report_class = ? AND scope_key = ?
|
|
467
|
-
LIMIT 1`,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (row?.owner_id !== ownerId) {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
return {
|
|
476
|
-
ownerId,
|
|
477
|
-
async release() {
|
|
478
|
-
await db.query(
|
|
479
|
-
`UPDATE ${REPORT_LOCKS_TABLE}
|
|
368
|
+
LIMIT 1`, definition.reportClassName, scope.scopeKey)).rows[0]?.owner_id !== ownerId) return null;
|
|
369
|
+
return {
|
|
370
|
+
ownerId,
|
|
371
|
+
async release() {
|
|
372
|
+
await db.query(`UPDATE ${REPORT_LOCKS_TABLE}
|
|
480
373
|
SET owner_id = NULL,
|
|
481
374
|
heartbeat_at = NULL,
|
|
482
375
|
expires_at = NULL,
|
|
483
376
|
updated_at = ?
|
|
484
377
|
WHERE report_class = ?
|
|
485
378
|
AND scope_key = ?
|
|
486
|
-
AND owner_id = ?`,
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
scope.scopeKey,
|
|
490
|
-
ownerId
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
};
|
|
379
|
+
AND owner_id = ?`, (/* @__PURE__ */ new Date()).toISOString(), definition.reportClassName, scope.scopeKey, ownerId);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
494
382
|
}
|
|
495
383
|
async function rebuildReport(db, definition, reportTable, scope, adapterType, runId, watermark) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const refreshedAt = /* @__PURE__ */ new Date();
|
|
515
|
-
const rows = materializeRows(result.rows, definition, refreshedAt, scope);
|
|
516
|
-
await replaceRows(db, reportTable, rows, scope);
|
|
517
|
-
const watermarkAfter = watermarkColumn ? await maxWatermark(db, definition, scope, watermarkColumn, adapterType) : null;
|
|
518
|
-
if (watermarkColumn) {
|
|
519
|
-
await writeWatermark(
|
|
520
|
-
db,
|
|
521
|
-
definition,
|
|
522
|
-
scope,
|
|
523
|
-
watermarkColumn,
|
|
524
|
-
watermarkAfter,
|
|
525
|
-
runId
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
return {
|
|
529
|
-
rowCount: rows.length,
|
|
530
|
-
changedGroupCount: rows.length,
|
|
531
|
-
watermarkAfter,
|
|
532
|
-
mode: "rebuild"
|
|
533
|
-
};
|
|
384
|
+
const explicitWatermarkColumn = definition.refresh?.watermarkColumn && await fieldColumn(definition.sourceClassName, definition.refresh.watermarkColumn);
|
|
385
|
+
const explicitSoftDeleteColumn = definition.refresh?.softDeleteColumn && await fieldColumn(definition.sourceClassName, definition.refresh.softDeleteColumn);
|
|
386
|
+
const watermarkColumn = watermark?.watermarkColumn ?? explicitWatermarkColumn ?? null;
|
|
387
|
+
const softDeleteColumn = watermark?.softDeleteColumn ?? explicitSoftDeleteColumn ?? null;
|
|
388
|
+
const deletedFilter = softDeleteColumn ? { [softDeleteColumn]: null } : {};
|
|
389
|
+
const aggregate = buildAggregate(withRefreshFilters(compileReportDefinition(definition), scope, deletedFilter), 1, adapterType);
|
|
390
|
+
const result = await db.query(aggregate.sql, ...aggregate.values);
|
|
391
|
+
const refreshedAt = /* @__PURE__ */ new Date();
|
|
392
|
+
const rows = materializeRows(result.rows, definition, refreshedAt, scope);
|
|
393
|
+
await replaceRows(db, reportTable, rows, scope);
|
|
394
|
+
const watermarkAfter = watermarkColumn ? await maxWatermark(db, definition, scope, watermarkColumn, adapterType) : null;
|
|
395
|
+
if (watermarkColumn) await writeWatermark(db, definition, scope, watermarkColumn, watermarkAfter, runId);
|
|
396
|
+
return {
|
|
397
|
+
rowCount: rows.length,
|
|
398
|
+
changedGroupCount: rows.length,
|
|
399
|
+
watermarkAfter,
|
|
400
|
+
mode: "rebuild"
|
|
401
|
+
};
|
|
534
402
|
}
|
|
535
403
|
async function incrementalReport(db, definition, reportTable, scope, adapterType, runId, changedRows = []) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return {
|
|
579
|
-
rowCount: 0,
|
|
580
|
-
changedGroupCount: 0,
|
|
581
|
-
watermarkBefore,
|
|
582
|
-
watermarkAfter: watermarkBefore,
|
|
583
|
-
mode: "incremental"
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
let rowCount = 0;
|
|
587
|
-
const conflicts = conflictColumns(definition, scope);
|
|
588
|
-
for (const groupRow of affectedGroups) {
|
|
589
|
-
const affectedSpec = compileAffectedGroupSpec(
|
|
590
|
-
definition,
|
|
591
|
-
scope,
|
|
592
|
-
watermark,
|
|
593
|
-
groupRow
|
|
594
|
-
);
|
|
595
|
-
const aggregate = buildAggregate(affectedSpec, 1, adapterType);
|
|
596
|
-
const result = await db.query(aggregate.sql, ...aggregate.values);
|
|
597
|
-
const refreshedAt = /* @__PURE__ */ new Date();
|
|
598
|
-
const rows = materializeRows(result.rows, definition, refreshedAt, scope);
|
|
599
|
-
if (rows.length === 0) {
|
|
600
|
-
await deleteMaterializedGroup(
|
|
601
|
-
db,
|
|
602
|
-
reportTable,
|
|
603
|
-
definition,
|
|
604
|
-
scope,
|
|
605
|
-
groupRow
|
|
606
|
-
);
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
for (const row of rows) {
|
|
610
|
-
await db.upsert(reportTable, conflicts, row);
|
|
611
|
-
rowCount += 1;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
const watermarkAfter = await maxWatermark(
|
|
615
|
-
db,
|
|
616
|
-
definition,
|
|
617
|
-
scope,
|
|
618
|
-
watermark.watermarkColumn,
|
|
619
|
-
adapterType
|
|
620
|
-
);
|
|
621
|
-
await writeWatermark(
|
|
622
|
-
db,
|
|
623
|
-
definition,
|
|
624
|
-
scope,
|
|
625
|
-
watermark.watermarkColumn,
|
|
626
|
-
watermarkAfter,
|
|
627
|
-
runId
|
|
628
|
-
);
|
|
629
|
-
return {
|
|
630
|
-
rowCount,
|
|
631
|
-
changedGroupCount: affectedGroups.length,
|
|
632
|
-
watermarkBefore,
|
|
633
|
-
watermarkAfter,
|
|
634
|
-
mode: "incremental"
|
|
635
|
-
};
|
|
404
|
+
const watermark = await resolveWatermarkConfig(definition);
|
|
405
|
+
const watermarkBefore = await readWatermark(db, definition, scope, watermark.watermarkColumn);
|
|
406
|
+
if (!watermarkBefore) return {
|
|
407
|
+
...await rebuildReport(db, definition, reportTable, scope, adapterType, runId, watermark),
|
|
408
|
+
watermarkBefore
|
|
409
|
+
};
|
|
410
|
+
const changedAggregate = buildAggregate(compileChangedGroupSpec(definition, scope, watermark, watermarkBefore), 1, adapterType);
|
|
411
|
+
const changedGroups = (await db.query(changedAggregate.sql, ...changedAggregate.values)).rows.map((row) => parseMaybeJson(row));
|
|
412
|
+
const materializedGroups = changedGroups.length > 0 || changedRows.length > 0 ? await readMaterializedGroups(db, reportTable, definition, scope) : [];
|
|
413
|
+
const affectedGroups = dedupeGroupRows(definition, [...changedGroups, ...materializedGroups]);
|
|
414
|
+
if (affectedGroups.length === 0) return {
|
|
415
|
+
rowCount: 0,
|
|
416
|
+
changedGroupCount: 0,
|
|
417
|
+
watermarkBefore,
|
|
418
|
+
watermarkAfter: watermarkBefore,
|
|
419
|
+
mode: "incremental"
|
|
420
|
+
};
|
|
421
|
+
let rowCount = 0;
|
|
422
|
+
const conflicts = conflictColumns(definition, scope);
|
|
423
|
+
for (const groupRow of affectedGroups) {
|
|
424
|
+
const aggregate = buildAggregate(compileAffectedGroupSpec(definition, scope, watermark, groupRow), 1, adapterType);
|
|
425
|
+
const result = await db.query(aggregate.sql, ...aggregate.values);
|
|
426
|
+
const refreshedAt = /* @__PURE__ */ new Date();
|
|
427
|
+
const rows = materializeRows(result.rows, definition, refreshedAt, scope);
|
|
428
|
+
if (rows.length === 0) {
|
|
429
|
+
await deleteMaterializedGroup(db, reportTable, definition, scope, groupRow);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
for (const row of rows) {
|
|
433
|
+
await db.upsert(reportTable, conflicts, row);
|
|
434
|
+
rowCount += 1;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const watermarkAfter = await maxWatermark(db, definition, scope, watermark.watermarkColumn, adapterType);
|
|
438
|
+
await writeWatermark(db, definition, scope, watermark.watermarkColumn, watermarkAfter, runId);
|
|
439
|
+
return {
|
|
440
|
+
rowCount,
|
|
441
|
+
changedGroupCount: affectedGroups.length,
|
|
442
|
+
watermarkBefore,
|
|
443
|
+
watermarkAfter,
|
|
444
|
+
mode: "incremental"
|
|
445
|
+
};
|
|
636
446
|
}
|
|
637
447
|
function requiredReportRuntimeTables(mode, options) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
448
|
+
const tables = /* @__PURE__ */ new Set();
|
|
449
|
+
if (options.trackRuns !== false) tables.add(REPORT_RUNS_TABLE);
|
|
450
|
+
if (mode === "incremental") tables.add(REPORT_WATERMARKS_TABLE);
|
|
451
|
+
if (options.lock !== false) tables.add(REPORT_LOCKS_TABLE);
|
|
452
|
+
return [...tables];
|
|
643
453
|
}
|
|
644
454
|
async function refreshReportOnce(reportCtor, options) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
options.db,
|
|
692
|
-
definition,
|
|
693
|
-
scope,
|
|
694
|
-
options,
|
|
695
|
-
requestedMode
|
|
696
|
-
);
|
|
697
|
-
const work = requestedMode === "incremental" ? await incrementalReport(
|
|
698
|
-
options.db,
|
|
699
|
-
definition,
|
|
700
|
-
reportTable,
|
|
701
|
-
scope,
|
|
702
|
-
adapterType,
|
|
703
|
-
runId,
|
|
704
|
-
options.changedRows
|
|
705
|
-
) : await rebuildReport(
|
|
706
|
-
options.db,
|
|
707
|
-
definition,
|
|
708
|
-
reportTable,
|
|
709
|
-
scope,
|
|
710
|
-
adapterType,
|
|
711
|
-
runId
|
|
712
|
-
);
|
|
713
|
-
await completeRun(options.db, runId, "success", {
|
|
714
|
-
rowCount: work.rowCount,
|
|
715
|
-
changedGroupCount: work.changedGroupCount,
|
|
716
|
-
watermarkAfter: work.watermarkAfter
|
|
717
|
-
});
|
|
718
|
-
return {
|
|
719
|
-
rowCount: work.rowCount,
|
|
720
|
-
refreshedAt: /* @__PURE__ */ new Date(),
|
|
721
|
-
mode: work.mode,
|
|
722
|
-
tenantId: scope.tenantId,
|
|
723
|
-
runId,
|
|
724
|
-
changedGroupCount: work.changedGroupCount
|
|
725
|
-
};
|
|
726
|
-
} catch (error) {
|
|
727
|
-
await completeRun(options.db, runId, "failed", { error });
|
|
728
|
-
throw error;
|
|
729
|
-
} finally {
|
|
730
|
-
await lock?.release();
|
|
731
|
-
}
|
|
455
|
+
if (!options.db) throw new Error("refreshReport requires a database handle");
|
|
456
|
+
const definition = await buildReportDefinition(reportCtor);
|
|
457
|
+
const reportTable = getReportTableName(reportCtor);
|
|
458
|
+
const requestedMode = options.mode ?? definition.refresh?.mode ?? "rebuild";
|
|
459
|
+
if (!await tableExists(options.db, reportTable)) throw new Error(`Report table '${reportTable}' does not exist for ${reportCtor.name}. Run smrt db:migrate before refreshing reports.`);
|
|
460
|
+
const requiredTables = requiredReportRuntimeTables(requestedMode, options);
|
|
461
|
+
if (requiredTables.length > 0) await assertReportTablesReady(options.db, requiredTables);
|
|
462
|
+
const adapterType = adapterTypeFromDb(options.db, options.adapterType);
|
|
463
|
+
const scope = await resolveScope(definition, options);
|
|
464
|
+
const lock = options.lock === false ? null : await acquireLock(options.db, definition, scope, options.lockTtlMs ?? 900 * 1e3);
|
|
465
|
+
if (options.lock !== false && !lock) {
|
|
466
|
+
const runId2 = await startRun(options.db, definition, scope, options, requestedMode);
|
|
467
|
+
await completeRun(options.db, runId2, "skipped");
|
|
468
|
+
return {
|
|
469
|
+
rowCount: 0,
|
|
470
|
+
refreshedAt: /* @__PURE__ */ new Date(),
|
|
471
|
+
mode: requestedMode,
|
|
472
|
+
tenantId: scope.tenantId,
|
|
473
|
+
runId: runId2,
|
|
474
|
+
changedGroupCount: 0,
|
|
475
|
+
skipped: true
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
let runId;
|
|
479
|
+
try {
|
|
480
|
+
runId = await startRun(options.db, definition, scope, options, requestedMode);
|
|
481
|
+
const work = requestedMode === "incremental" ? await incrementalReport(options.db, definition, reportTable, scope, adapterType, runId, options.changedRows) : await rebuildReport(options.db, definition, reportTable, scope, adapterType, runId);
|
|
482
|
+
await completeRun(options.db, runId, "success", {
|
|
483
|
+
rowCount: work.rowCount,
|
|
484
|
+
changedGroupCount: work.changedGroupCount,
|
|
485
|
+
watermarkAfter: work.watermarkAfter
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
rowCount: work.rowCount,
|
|
489
|
+
refreshedAt: /* @__PURE__ */ new Date(),
|
|
490
|
+
mode: work.mode,
|
|
491
|
+
tenantId: scope.tenantId,
|
|
492
|
+
runId,
|
|
493
|
+
changedGroupCount: work.changedGroupCount
|
|
494
|
+
};
|
|
495
|
+
} catch (error) {
|
|
496
|
+
await completeRun(options.db, runId, "failed", { error });
|
|
497
|
+
throw error;
|
|
498
|
+
} finally {
|
|
499
|
+
await lock?.release();
|
|
500
|
+
}
|
|
732
501
|
}
|
|
733
502
|
async function refreshReport(reportCtor, options = {}) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
changedGroupCount: results.reduce(
|
|
756
|
-
(sum, result) => sum + (result.changedGroupCount ?? 0),
|
|
757
|
-
0
|
|
758
|
-
),
|
|
759
|
-
tenantResults: results
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
return refreshReportOnce(reportCtor, options);
|
|
503
|
+
if (options.tenantIds && options.tenantIds.length > 0) {
|
|
504
|
+
if (!options.db) throw new Error("refreshReport requires a database handle");
|
|
505
|
+
const results = [];
|
|
506
|
+
for (const tenantId of options.tenantIds) {
|
|
507
|
+
const result = await withTenant({ tenantId }, () => refreshReportOnce(reportCtor, {
|
|
508
|
+
...options,
|
|
509
|
+
tenantId,
|
|
510
|
+
tenantIds: void 0
|
|
511
|
+
}));
|
|
512
|
+
results.push(result);
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
rowCount: results.reduce((sum, result) => sum + result.rowCount, 0),
|
|
516
|
+
refreshedAt: /* @__PURE__ */ new Date(),
|
|
517
|
+
mode: results[0]?.mode ?? options.mode ?? "rebuild",
|
|
518
|
+
tenantId: null,
|
|
519
|
+
changedGroupCount: results.reduce((sum, result) => sum + (result.changedGroupCount ?? 0), 0),
|
|
520
|
+
tenantResults: results
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return refreshReportOnce(reportCtor, options);
|
|
763
524
|
}
|
|
764
525
|
function reportRowIdentity(row, definition) {
|
|
765
|
-
|
|
766
|
-
return stableReportId(
|
|
767
|
-
groupingColumns.map((column) => row[toSnakeCase(column)])
|
|
768
|
-
);
|
|
526
|
+
return stableReportId(getReportGroupingColumns(definition).map((column) => row[toSnakeCase(column)]));
|
|
769
527
|
}
|
|
770
528
|
function reportSourceClassName(source) {
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
};
|
|
778
|
-
//# sourceMappingURL=refresh.js.map
|
|
529
|
+
return sourceClassName(source);
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
export { refreshReport, reportRowIdentity, reportSourceClassName };
|
|
533
|
+
|
|
534
|
+
//# sourceMappingURL=refresh.js.map
|