@cosmicdrift/kumiko-framework 0.328.1 → 0.330.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/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/websocket-route.d.ts +1 -0
- package/dist/api/websocket-route.js +37 -6
- package/dist/bun-db/connection.d.ts +1 -0
- package/dist/bun-db/connection.js +1 -0
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +3 -0
- package/dist/bun-db/query.js +99 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/query-api.d.ts +1 -1
- package/dist/db/query-api.js +1 -1
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/table-builder.d.ts +1 -0
- package/dist/db/tenant-db.js +5 -1
- package/dist/engine/boot-validator/screens.js +44 -0
- package/dist/engine/build-app-schema.js +3 -0
- package/dist/event-store/admin-api.d.ts +1 -0
- package/dist/event-store/admin-api.js +1 -0
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +1 -0
- package/dist/event-store/row-to-stored-event.d.ts +1 -0
- package/dist/event-store/snapshot.d.ts +1 -0
- package/dist/event-store/snapshot.js +1 -0
- package/dist/i18n/required-surface-keys.js +13 -2
- package/dist/jobs/job-runner.d.ts +1 -0
- package/dist/pipeline/event-dispatcher-admin.d.ts +1 -0
- package/dist/pipeline/event-dispatcher-delivery.d.ts +1 -0
- package/dist/pipeline/event-dispatcher-delivery.js +1 -0
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/projection-rebuild.d.ts +1 -0
- package/dist/time/polyfill.d.ts +1 -0
- package/dist/time/polyfill.js +1 -0
- package/dist/ui-types/index.d.ts +1 -1
- package/package.json +4 -4
- package/src/changes.json +13 -0
package/dist/api/index.d.ts
CHANGED
|
@@ -29,4 +29,4 @@ export { createSseBroker } from "./sse-broker.js";
|
|
|
29
29
|
export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
|
|
30
30
|
export { generateToken } from "./tokens.js";
|
|
31
31
|
export type { KumikoServeEnv, KumikoServerWebSocket, KumikoWebSocketData, WebSocketUpgradeServer, } from "./websocket-route.js";
|
|
32
|
-
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
|
32
|
+
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
package/dist/api/index.js
CHANGED
|
@@ -17,4 +17,4 @@ export { buildServer, makeDispatchSystemQuery, makeDispatchSystemWrite } from ".
|
|
|
17
17
|
export { createSseBroker } from "./sse-broker.js";
|
|
18
18
|
export { createSseRoute, SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
|
|
19
19
|
export { generateToken } from "./tokens.js";
|
|
20
|
-
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
|
20
|
+
export { createKumikoWebSocketHandler, isWebSocketUpgradeRequest, kumikoWebSocketHandler, WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_HEARTBEAT_INTERVAL_MS, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES, WEBSOCKET_REVALIDATION_FAILURE_LIMIT, WEBSOCKET_ROUTE_PATH_PREFIX, } from "./websocket-route.js";
|
|
@@ -4,6 +4,7 @@ import { type AuthSessionChecker, type TenantLifecycleStatusResolver } from "./a
|
|
|
4
4
|
import { type RequestContextData } from "./request-context.js";
|
|
5
5
|
export { WEBSOCKET_BACKPRESSURE_LIMIT_BYTES, WEBSOCKET_DEFAULT_MAX_CONNECTIONS_PER_USER, WEBSOCKET_DEFAULT_MAX_MESSAGE_BYTES, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_ROUTE_PATH_PREFIX, };
|
|
6
6
|
export declare const WEBSOCKET_HEARTBEAT_INTERVAL_MS = 25000;
|
|
7
|
+
export declare const WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES: number;
|
|
7
8
|
export declare const WEBSOCKET_REVALIDATION_FAILURE_LIMIT = 3;
|
|
8
9
|
export type KumikoWebSocketData = {
|
|
9
10
|
readonly handlers: WebSocketSessionHandlers;
|
|
@@ -9,6 +9,9 @@ const CLOSE_POLICY_VIOLATION = 1008;
|
|
|
9
9
|
const CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
10
10
|
const CLOSE_INTERNAL_ERROR = 1011;
|
|
11
11
|
const CLOSE_TRY_AGAIN_LATER = 1013;
|
|
12
|
+
// Frames queue behind a slow onOpen or onMessage; without a byte cap a client
|
|
13
|
+
// could grow that queue unbounded.
|
|
14
|
+
export const WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES = 1024 * 1024;
|
|
12
15
|
// Consecutive failed session-store checks before a socket is closed: rides out a
|
|
13
16
|
// short store blip, but a socket must not stay unverified forever.
|
|
14
17
|
export const WEBSOCKET_REVALIDATION_FAILURE_LIMIT = 3;
|
|
@@ -87,6 +90,7 @@ function stateOf(ws) {
|
|
|
87
90
|
revalidating: false,
|
|
88
91
|
chain: Promise.resolve(),
|
|
89
92
|
closed: false,
|
|
93
|
+
pendingMessageBytes: 0,
|
|
90
94
|
controller: new AbortController(),
|
|
91
95
|
};
|
|
92
96
|
connectionStates.set(ws.data, state);
|
|
@@ -112,22 +116,25 @@ function runInRequestContext(ws, run) {
|
|
|
112
116
|
const { requestContextData } = ws.data;
|
|
113
117
|
return requestContextData ? requestContext.run(requestContextData, run) : run();
|
|
114
118
|
}
|
|
115
|
-
function enqueue(ws, run) {
|
|
119
|
+
function enqueue(ws, run, onSettled) {
|
|
116
120
|
const state = stateOf(ws);
|
|
117
121
|
// skip: the socket is gone, nothing may start after close
|
|
118
122
|
if (state.closed)
|
|
119
123
|
return;
|
|
120
124
|
const step = async () => {
|
|
121
|
-
// skip: queued behind a slower handler and the socket closed meanwhile
|
|
122
|
-
if (state.closed)
|
|
123
|
-
return;
|
|
124
125
|
try {
|
|
126
|
+
// skip: queued behind a slower handler and the socket closed meanwhile
|
|
127
|
+
if (state.closed)
|
|
128
|
+
return;
|
|
125
129
|
await run();
|
|
126
130
|
}
|
|
127
131
|
catch (error) {
|
|
128
132
|
reportHandlerFailure(error);
|
|
129
133
|
ws.close(CLOSE_INTERNAL_ERROR, "internal error");
|
|
130
134
|
}
|
|
135
|
+
finally {
|
|
136
|
+
onSettled?.();
|
|
137
|
+
}
|
|
131
138
|
};
|
|
132
139
|
state.chain = state.chain.then(() => runInRequestContext(ws, step));
|
|
133
140
|
}
|
|
@@ -208,9 +215,33 @@ export function createKumikoWebSocketHandler(options = {}) {
|
|
|
208
215
|
});
|
|
209
216
|
return;
|
|
210
217
|
}
|
|
218
|
+
const state = stateOf(ws);
|
|
219
|
+
// skip: overflowed or closed socket, late frames are dropped
|
|
220
|
+
if (state.closed)
|
|
221
|
+
return;
|
|
222
|
+
state.pendingMessageBytes += byteLength;
|
|
223
|
+
const pendingLimit = Math.max(WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES, ws.data.maxMessageBytes);
|
|
224
|
+
if (state.pendingMessageBytes > pendingLimit) {
|
|
225
|
+
// Marks the socket closed right away so already-queued frames never start.
|
|
226
|
+
state.closed = true;
|
|
227
|
+
ws.close(CLOSE_TRY_AGAIN_LATER, "try again later");
|
|
228
|
+
log.warn("websocket pending buffer over cap", {
|
|
229
|
+
pendingBytes: state.pendingMessageBytes,
|
|
230
|
+
limitBytes: pendingLimit,
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
211
234
|
const { onMessage } = ws.data.handlers;
|
|
212
|
-
if (onMessage)
|
|
213
|
-
|
|
235
|
+
if (onMessage) {
|
|
236
|
+
// Copy now: queued behind a slow handler, Bun may have reused the buffer by then.
|
|
237
|
+
const data = toMessageData(message);
|
|
238
|
+
enqueue(ws, () => onMessage(data, toConnection(ws)), () => {
|
|
239
|
+
state.pendingMessageBytes -= byteLength;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
state.pendingMessageBytes -= byteLength;
|
|
244
|
+
}
|
|
214
245
|
},
|
|
215
246
|
close(ws, code, reason) {
|
|
216
247
|
const state = stateOf(ws);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="bun-types" preserve="true" />
|
|
1
2
|
// Bun.SQL-basierter Connection-Layer (KEIN drizzle). Production-Pfad
|
|
2
3
|
// nach drizzle-removal. Erstellt parallel zum legacy `db/connection.ts`
|
|
3
4
|
// — apps migrieren schritt-für-schritt, alte Datei weg sobald 0 Konsumenten.
|
package/dist/bun-db/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { BunDbConnection, BunDbConnectionOptions, BunDbRunner, BunDbTx, PgListenClient, } from "./connection.js";
|
|
2
2
|
export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection.js";
|
|
3
3
|
export type { SelectOptions, TableInfo, WhereObject, WhereOperator, WhereValue } from "./query.js";
|
|
4
|
-
export { asEntityTableMeta, asRawClient, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOne, runInSavepoint, runInSavepointIfSupported, selectMany, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
|
4
|
+
export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOne, isTimestamptzType, runInSavepoint, runInSavepointIfSupported, selectMany, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
package/dist/bun-db/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// bun-db: Bun.sql-basierte DB-API ohne drizzle.
|
|
2
2
|
// Production-Pfad nach drizzle-removal.
|
|
3
3
|
export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection.js";
|
|
4
|
-
export { asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOne, runInSavepoint, runInSavepointIfSupported, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
|
4
|
+
export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOne, isTimestamptzType, runInSavepoint, runInSavepointIfSupported, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
|
package/dist/bun-db/query.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AggregateRow, AggregateSpec } from "@cosmicdrift/kumiko-types/aggregate-types";
|
|
1
2
|
import type { SelectOptions, WhereObject } from "@cosmicdrift/kumiko-types/where-clause-types";
|
|
2
3
|
import type { EntityTableMeta } from "../db/entity-table-meta.js";
|
|
3
4
|
import { type NotExecutorOnly } from "../db/table-builder.js";
|
|
@@ -51,6 +52,8 @@ export type DeleteManyBatchedResult = {
|
|
|
51
52
|
readonly batches: number;
|
|
52
53
|
};
|
|
53
54
|
export declare function countWhere(db: AnyDb, table: TableLike, where?: WhereObject): Promise<number>;
|
|
55
|
+
export declare function isTimestamptzType(pgType: string | undefined): boolean;
|
|
56
|
+
export declare function aggregateWhere(db: AnyDb, table: TableLike, spec: AggregateSpec, where?: WhereObject): Promise<readonly AggregateRow[]>;
|
|
54
57
|
export declare function upsertOnConflict<TRow = any>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, options: UpsertOnConflictOptions): Promise<TRow | undefined>;
|
|
55
58
|
export declare function upsertByPk<TRow = any>(db: AnyDb, table: WritableTable, values: Record<string, unknown>, updateOnConflict?: Record<string, unknown>): Promise<TRow | undefined>;
|
|
56
59
|
export type IncrementCounterOptions = {
|
package/dist/bun-db/query.js
CHANGED
|
@@ -989,6 +989,105 @@ export async function countWhere(db, table, where = {}) {
|
|
|
989
989
|
const rows = (await unsafeReadRetrying(db, sqlText, values));
|
|
990
990
|
return rows[0]?.count ?? 0;
|
|
991
991
|
}
|
|
992
|
+
const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
|
|
993
|
+
const AGGREGATE_ORDER_DIRECTIONS = ["asc", "desc"];
|
|
994
|
+
function requireAggregateColumn(info, field, role) {
|
|
995
|
+
if (!info.hasColumn(field)) {
|
|
996
|
+
throw new Error(`aggregateWhere: ${role} "${field}" is not a column of ${info.name}`);
|
|
997
|
+
}
|
|
998
|
+
return quoteIdent(info.columnOf(field));
|
|
999
|
+
}
|
|
1000
|
+
export function isTimestamptzType(pgType) {
|
|
1001
|
+
return pgType === "timestamptz" || pgType === "timestamptz(3)";
|
|
1002
|
+
}
|
|
1003
|
+
function aggregateKeyOf(raw) {
|
|
1004
|
+
if (raw === null || raw === undefined)
|
|
1005
|
+
return null;
|
|
1006
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean")
|
|
1007
|
+
return raw;
|
|
1008
|
+
if (typeof raw === "bigint")
|
|
1009
|
+
return Number(raw);
|
|
1010
|
+
if (raw instanceof Date)
|
|
1011
|
+
return raw.toISOString();
|
|
1012
|
+
return String(raw);
|
|
1013
|
+
}
|
|
1014
|
+
function aggregateMeasureSql(info, measure) {
|
|
1015
|
+
switch (measure.fn) {
|
|
1016
|
+
case "count":
|
|
1017
|
+
return "COUNT(*)";
|
|
1018
|
+
case "countDistinct":
|
|
1019
|
+
return `COUNT(DISTINCT ${requireAggregateColumn(info, measure.field, "measure field")})`;
|
|
1020
|
+
case "sum":
|
|
1021
|
+
return `COALESCE(SUM(${requireAggregateColumn(info, measure.field, "measure field")}), 0)`;
|
|
1022
|
+
case "avg":
|
|
1023
|
+
return `AVG(${requireAggregateColumn(info, measure.field, "measure field")})`;
|
|
1024
|
+
default:
|
|
1025
|
+
throw new Error(`aggregateWhere: unknown measure fn "${String(measure.fn)}"`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function aggregateDimensionSql(info, dimension, index, params) {
|
|
1029
|
+
const column = requireAggregateColumn(info, dimension.field, "groupBy field");
|
|
1030
|
+
if (!("bucket" in dimension))
|
|
1031
|
+
return `${column} AS k${index}`;
|
|
1032
|
+
if (!AGGREGATE_BUCKET_UNITS.includes(dimension.bucket)) {
|
|
1033
|
+
throw new Error(`aggregateWhere: unknown bucket "${String(dimension.bucket)}"`);
|
|
1034
|
+
}
|
|
1035
|
+
if (!isTimestamptzType(info.pgTypeOf(info.columnOf(dimension.field)))) {
|
|
1036
|
+
throw new Error(`aggregateWhere: bucket on "${dimension.field}" requires a timestamptz column`);
|
|
1037
|
+
}
|
|
1038
|
+
params.push(dimension.timeZone);
|
|
1039
|
+
return `(EXTRACT(EPOCH FROM date_trunc('${dimension.bucket}', ${column}, $${params.length}::text)) * 1000)::float8 AS k${index}`;
|
|
1040
|
+
}
|
|
1041
|
+
function assertValidAggregateLimits(spec) {
|
|
1042
|
+
if (spec.limit !== undefined && (!Number.isInteger(spec.limit) || spec.limit < 0)) {
|
|
1043
|
+
throw new Error(`aggregateWhere: limit must be a non-negative integer, got ${spec.limit}`);
|
|
1044
|
+
}
|
|
1045
|
+
if (spec.orderByValue !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByValue)) {
|
|
1046
|
+
throw new Error(`aggregateWhere: unknown orderByValue "${String(spec.orderByValue)}"`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
function aggregateGroupOrderLimitSql(spec, dimensionCount) {
|
|
1050
|
+
let sqlText = "";
|
|
1051
|
+
if (dimensionCount > 0) {
|
|
1052
|
+
const positions = Array.from({ length: dimensionCount }, (_, index) => String(index + 1));
|
|
1053
|
+
sqlText += ` GROUP BY ${positions.join(", ")}`;
|
|
1054
|
+
const orderParts = spec.orderByValue === undefined
|
|
1055
|
+
? positions
|
|
1056
|
+
: [`${dimensionCount + 1} ${spec.orderByValue === "desc" ? "DESC" : "ASC"}`, ...positions];
|
|
1057
|
+
sqlText += ` ORDER BY ${orderParts.join(", ")}`;
|
|
1058
|
+
}
|
|
1059
|
+
if (spec.limit !== undefined)
|
|
1060
|
+
sqlText += ` LIMIT ${spec.limit}`;
|
|
1061
|
+
return sqlText;
|
|
1062
|
+
}
|
|
1063
|
+
export async function aggregateWhere(db, table, spec, where = {}) {
|
|
1064
|
+
const scoped = tenantDbDelegate(db);
|
|
1065
|
+
if (scoped) {
|
|
1066
|
+
return scoped.aggregate(table, spec, where);
|
|
1067
|
+
}
|
|
1068
|
+
const info = extractTableInfo(table);
|
|
1069
|
+
const params = [];
|
|
1070
|
+
const measureSql = aggregateMeasureSql(info, spec.measure);
|
|
1071
|
+
const dimensions = spec.groupBy ?? [];
|
|
1072
|
+
const dimensionSql = dimensions.map((dimension, index) => aggregateDimensionSql(info, dimension, index, params));
|
|
1073
|
+
assertValidAggregateLimits(spec);
|
|
1074
|
+
const selectList = [...dimensionSql, `(${measureSql})::float8 AS value`].join(", ");
|
|
1075
|
+
let sqlText = `SELECT ${selectList} FROM ${quoteIdent(info.name)}`;
|
|
1076
|
+
if (Object.keys(where).length > 0) {
|
|
1077
|
+
const w = buildWhereClause(info, where, params.length + 1);
|
|
1078
|
+
sqlText += ` WHERE ${w.sqlText}`;
|
|
1079
|
+
params.push(...w.values);
|
|
1080
|
+
}
|
|
1081
|
+
sqlText += aggregateGroupOrderLimitSql(spec, dimensions.length);
|
|
1082
|
+
const raw = (await unsafeReadRetrying(db, sqlText, params));
|
|
1083
|
+
return raw.map((row) => {
|
|
1084
|
+
const value = row["value"];
|
|
1085
|
+
return {
|
|
1086
|
+
keys: dimensions.map((_, index) => aggregateKeyOf(row[`k${index}`])),
|
|
1087
|
+
value: value === null || value === undefined ? null : Number(value),
|
|
1088
|
+
};
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
992
1091
|
// biome-ignore lint/suspicious/noExplicitAny: see selectMany default
|
|
993
1092
|
export async function upsertOnConflict(db, table, values, options) {
|
|
994
1093
|
assertNotTenantScoped(db, "upsertOnConflict");
|
package/dist/db/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { constraintOf, extractPgError, isTableAlreadyExists, isUniqueViolation,
|
|
|
26
26
|
export { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
|
|
27
27
|
export { executeRawQuery, executeRawQueryRead } from "./queries/raw-sql.js";
|
|
28
28
|
export type { SelectOptions, WhereObject, WhereValue } from "./query-api.js";
|
|
29
|
-
export { asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "./query-api.js";
|
|
29
|
+
export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "./query-api.js";
|
|
30
30
|
export { readRebuildMarker, rebuildTablesFromDiff, writeRebuildMarker, } from "./rebuild-marker.js";
|
|
31
31
|
export { seedReferenceData } from "./reference-data.js";
|
|
32
32
|
export { renderTableDdl, renderTablesDdl } from "./render-ddl.js";
|
package/dist/db/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export { flattenMoney, moneyPayloadToMinorUnits, rehydrateMoney } from "./money.
|
|
|
19
19
|
export { constraintOf, extractPgError, isTableAlreadyExists, isUniqueViolation, } from "./pg-error.js";
|
|
20
20
|
export { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
|
|
21
21
|
export { executeRawQuery, executeRawQueryRead } from "./queries/raw-sql.js";
|
|
22
|
-
export { asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "./query-api.js";
|
|
22
|
+
export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "./query-api.js";
|
|
23
23
|
export { readRebuildMarker, rebuildTablesFromDiff, writeRebuildMarker, } from "./rebuild-marker.js";
|
|
24
24
|
export { seedReferenceData } from "./reference-data.js";
|
|
25
25
|
export { renderTableDdl, renderTablesDdl } from "./render-ddl.js";
|
package/dist/db/query-api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { SelectOptions, WhereObject, WhereOperator, WhereValue, } from "../db/query.js";
|
|
2
|
-
export { asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
|
|
2
|
+
export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
|
package/dist/db/query-api.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// damit existing imports `@cosmicdrift/kumiko-framework/db` weiterhin
|
|
4
4
|
// funktionieren während wir die Callers schrittweise auf den direkten
|
|
5
5
|
// bun-db-Import migrieren.
|
|
6
|
-
export { asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
|
|
6
|
+
export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "../db/query.js";
|
package/dist/db/query.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { type AnyDb, asEntityTableMeta, asRawClient, coerceRow, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOne, type OrderByClause, runInNewTransaction, runInSavepoint, runInSavepointIfSupported, type SelectOptions, selectMany, type TableInfo, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, type WhereObject, type WhereOperator, type WhereValue, } from "../bun-db/query.js";
|
|
1
|
+
export { type AnyDb, aggregateWhere, asEntityTableMeta, asRawClient, coerceRow, countWhere, type DeleteManyBatchedOptions, type DeleteManyBatchedResult, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, type IncrementCounterOptions, incrementCounter, insertMany, insertOne, type OrderByClause, runInNewTransaction, runInSavepoint, runInSavepointIfSupported, type SelectOptions, selectMany, type TableInfo, transaction, type UpsertOnConflictOptions, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, type WhereObject, type WhereOperator, type WhereValue, } from "../bun-db/query.js";
|
package/dist/db/query.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Provider-neutrale Query-API. Re-exported aus bun-db/query.
|
|
2
2
|
// Alle Consumer importieren von hier, nicht direkt aus bun-db/.
|
|
3
|
-
export { asEntityTableMeta, asRawClient, coerceRow, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOne, runInNewTransaction, runInSavepoint, runInSavepointIfSupported, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "../bun-db/query.js";
|
|
3
|
+
export { aggregateWhere, asEntityTableMeta, asRawClient, coerceRow, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOne, runInNewTransaction, runInSavepoint, runInSavepointIfSupported, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "../bun-db/query.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
2
3
|
import type { TenancyBrand } from "@cosmicdrift/kumiko-types/tenancy-brand";
|
|
3
4
|
import type { EntityDefinition, EntityRelations, FieldDefinition, FieldsMap } from "../engine/types/index.js";
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KUMIKO_NAME_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
2
|
import { SYSTEM_SCOPE_CHECK_BRAND, } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
3
|
-
import { asEntityTableMeta, asRawClient, countWhere as bunCountWhere, deleteMany as bunDeleteMany, fetchOne as bunFetchOne, insertOne as bunInsertOne, selectMany as bunSelectMany, updateMany as bunUpdateMany, runInNewTransaction, } from "../db/query.js";
|
|
3
|
+
import { asEntityTableMeta, asRawClient, aggregateWhere as bunAggregateWhere, countWhere as bunCountWhere, deleteMany as bunDeleteMany, fetchOne as bunFetchOne, insertOne as bunInsertOne, selectMany as bunSelectMany, updateMany as bunUpdateMany, runInNewTransaction, } from "../db/query.js";
|
|
4
4
|
import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
|
|
5
5
|
import { AccessDeniedError, InternalError, memberResolutionReadOnlyDenied, } from "../errors/index.js";
|
|
6
6
|
import { fallbackEscapeHatchReporter } from "../observability/escape-hatch-report.js";
|
|
@@ -443,6 +443,10 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
|
|
|
443
443
|
const filter = readWhere(table, where);
|
|
444
444
|
return withDbSpan("select", table, async () => bunCountWhere(db, table, filter));
|
|
445
445
|
},
|
|
446
|
+
aggregate(table, spec, where) {
|
|
447
|
+
const filter = readWhere(table, where);
|
|
448
|
+
return withDbSpan("select", table, async () => bunAggregateWhere(db, table, spec, filter));
|
|
449
|
+
},
|
|
446
450
|
insertOne(table, values) {
|
|
447
451
|
if (mode === "tenant" &&
|
|
448
452
|
hasTenantColumn(table) &&
|
|
@@ -1210,6 +1210,12 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1210
1210
|
`is not sortable. Set sortable: true on the field definition or pick another field.`);
|
|
1211
1211
|
}
|
|
1212
1212
|
}
|
|
1213
|
+
if (screen.createScreen !== undefined &&
|
|
1214
|
+
!Object.hasOwn(feature.screens, screen.createScreen)) {
|
|
1215
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityList) createScreen ` +
|
|
1216
|
+
`"${screen.createScreen}" does not resolve to a registered screen in this feature. ` +
|
|
1217
|
+
`Known screens in this feature: ${Object.keys(feature.screens).sort().join(", ") || "(none)"}.`);
|
|
1218
|
+
}
|
|
1213
1219
|
// Screen-Filter (Tier 2.7c) — drei Layer Author-Code-Check:
|
|
1214
1220
|
// 1) Field existiert auf der Entity (Tippfehler = leere Liste
|
|
1215
1221
|
// statt Crash; Boot-Fail ist deutlich besser).
|
|
@@ -1402,6 +1408,11 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
|
|
|
1402
1408
|
}
|
|
1403
1409
|
}
|
|
1404
1410
|
}
|
|
1411
|
+
if (screen.recordTitleField !== undefined && !fieldNames.has(screen.recordTitleField)) {
|
|
1412
|
+
throw new Error(`[Feature ${feature.name}] Screen "${screenId}" (entityEdit) recordTitleField ` +
|
|
1413
|
+
`"${screen.recordTitleField}" is not a field of entity "${screen.entity}" ` +
|
|
1414
|
+
`(known: ${[...fieldNames].sort().join(", ")}).`);
|
|
1415
|
+
}
|
|
1405
1416
|
if (screen.redirect !== undefined) {
|
|
1406
1417
|
validateRedirectTarget(feature, screenId, "entityEdit", screen.redirect, allScreenQns);
|
|
1407
1418
|
}
|
|
@@ -1441,6 +1452,33 @@ function validateDashboardScreen(featureName, screenId, screen, featureMap) {
|
|
|
1441
1452
|
if (screen.filter !== undefined) {
|
|
1442
1453
|
validateDashboardFilterDefinition(featureName, screenId, screen.filter);
|
|
1443
1454
|
}
|
|
1455
|
+
if (screen.timeRange !== undefined) {
|
|
1456
|
+
validateDashboardTimeRange(featureName, screenId, screen.timeRange, screen.filter?.id);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function validateDashboardTimeRange(featureName, screenId, timeRange, filterId) {
|
|
1460
|
+
const prefix = `[Feature ${featureName}] Screen "${screenId}" (dashboard) timeRange`;
|
|
1461
|
+
if (timeRange.id.length === 0)
|
|
1462
|
+
throw new Error(`${prefix} has an empty id.`);
|
|
1463
|
+
if (timeRange.id === filterId) {
|
|
1464
|
+
throw new Error(`${prefix} id "${timeRange.id}" collides with the filter id — both live in the URL search params.`);
|
|
1465
|
+
}
|
|
1466
|
+
if (timeRange.options.length === 0) {
|
|
1467
|
+
throw new Error(`${prefix}.options is empty — declare at least one option.`);
|
|
1468
|
+
}
|
|
1469
|
+
const values = new Set();
|
|
1470
|
+
for (const option of timeRange.options) {
|
|
1471
|
+
if (option.value.length === 0 || option.label.length === 0) {
|
|
1472
|
+
throw new Error(`${prefix} option needs a non-empty value and label.`);
|
|
1473
|
+
}
|
|
1474
|
+
if (values.has(option.value)) {
|
|
1475
|
+
throw new Error(`${prefix}.options has duplicate value "${option.value}".`);
|
|
1476
|
+
}
|
|
1477
|
+
values.add(option.value);
|
|
1478
|
+
}
|
|
1479
|
+
if (!values.has(timeRange.default)) {
|
|
1480
|
+
throw new Error(`${prefix}.default "${timeRange.default}" is not among the options (${[...values].join(", ")}).`);
|
|
1481
|
+
}
|
|
1444
1482
|
}
|
|
1445
1483
|
const EMBEDDABLE_SCREEN_TYPES = new Set([
|
|
1446
1484
|
"projectionList",
|
|
@@ -1491,6 +1529,12 @@ function validateDashboardQueryPanel(featureName, screenId, panel) {
|
|
|
1491
1529
|
if (!panel.query || typeof panel.query !== "string") {
|
|
1492
1530
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query.`);
|
|
1493
1531
|
}
|
|
1532
|
+
if ("span" in panel &&
|
|
1533
|
+
panel.span !== undefined &&
|
|
1534
|
+
panel.span !== "half" &&
|
|
1535
|
+
panel.span !== "full") {
|
|
1536
|
+
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has span "${String(panel.span)}" — expected "half" or "full".`);
|
|
1537
|
+
}
|
|
1494
1538
|
if (panel.kind === "stat" && panel.valueField.length === 0) {
|
|
1495
1539
|
throw new Error(`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`);
|
|
1496
1540
|
}
|
|
@@ -426,6 +426,9 @@ function projectField(fieldDef) {
|
|
|
426
426
|
const unit = def["unit"];
|
|
427
427
|
if (typeof unit === "string" || (isPlainObject(unit) && typeof unit["field"] === "string"))
|
|
428
428
|
out["unit"] = unit;
|
|
429
|
+
// number/decimal/bigInt: `false` drops thousands separators (e.g. a year).
|
|
430
|
+
if (typeof def["grouping"] === "boolean")
|
|
431
|
+
out["grouping"] = def["grouping"];
|
|
429
432
|
// text: "password" masks the input and blocks URL prefill in the renderer.
|
|
430
433
|
if (typeof def["format"] === "string")
|
|
431
434
|
out["format"] = def["format"];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
import { type EventMetadata, type StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
2
3
|
import type { DbRunner } from "../db/index.js";
|
|
3
4
|
import type { TenantId } from "../engine/types/index.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
import { isPersonalDataGated, UNATTRIBUTED_ORIGIN, } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
2
3
|
// Value-only import, aliased to avoid shadowing the ambient global
|
|
3
4
|
// `Temporal` TYPE this file's other Temporal.Instant annotations resolve
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
import type { LoadAggregateWithSnapshotOptions, LoadAggregateWithSnapshotResult, SnapshotReducer } from "@cosmicdrift/kumiko-types/snapshot-types";
|
|
2
3
|
import type { DbConnection, DbRunner } from "../db/connection.js";
|
|
3
4
|
import type { TenantId } from "../engine/types/index.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
// sql now comes from native dialect
|
|
2
3
|
import { index, instant, integer, jsonb, table as pgTable, primaryKey, sql, text, uuid, } from "../db/dialect.js";
|
|
3
4
|
import { ensureSnapshotVersionColumn, upsertSnapshot } from "../db/queries/event-store.js";
|
|
@@ -57,12 +57,25 @@ function pushDashboardScreenKeys(out, dashboard) {
|
|
|
57
57
|
pushDashboardPanelKeys(out, panel);
|
|
58
58
|
if (dashboard.filter !== undefined)
|
|
59
59
|
pushDashboardFilterKeys(out, dashboard.filter);
|
|
60
|
+
for (const option of dashboard.timeRange?.options ?? [])
|
|
61
|
+
pushKey(out, option.label);
|
|
62
|
+
pushKey(out, dashboard.scope?.badge);
|
|
63
|
+
pushKey(out, dashboard.scope?.notice);
|
|
60
64
|
}
|
|
61
65
|
function pushDashboardPanelKeys(out, panel) {
|
|
62
66
|
// skip: custom-Panel übersetzt sich selbst, kein Key hier
|
|
63
67
|
if (panel.kind === "custom")
|
|
64
68
|
return;
|
|
65
69
|
pushKey(out, panel.label);
|
|
70
|
+
if (panel.kind === "chart")
|
|
71
|
+
pushKey(out, panel.subtitle);
|
|
72
|
+
if (panel.kind === "chart" ||
|
|
73
|
+
panel.kind === "list" ||
|
|
74
|
+
panel.kind === "feed" ||
|
|
75
|
+
panel.kind === "progress-list") {
|
|
76
|
+
pushKey(out, panel.emptyLabel);
|
|
77
|
+
pushKey(out, panel.emptyHint);
|
|
78
|
+
}
|
|
66
79
|
if (panel.kind === "stat-group") {
|
|
67
80
|
for (const stat of panel.stats)
|
|
68
81
|
pushKey(out, stat.label);
|
|
@@ -74,8 +87,6 @@ function pushDashboardPanelKeys(out, panel) {
|
|
|
74
87
|
pushKey(out, normalized.label);
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
|
-
if (panel.kind === "feed" && panel.emptyLabel !== undefined)
|
|
78
|
-
pushKey(out, panel.emptyLabel);
|
|
79
90
|
}
|
|
80
91
|
function pushDashboardFilterKeys(out, filter) {
|
|
81
92
|
pushKey(out, filter.label);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
import { type AppContext, type DispatchWriteRef, type JobRunIn, type JobTriggerEvent, type Registry, type SessionUser, type TenantId } from "../engine/types/index.js";
|
|
2
3
|
/**
|
|
3
4
|
* BullMQ job ids are `repeat:<schedulerId>:<millis>`. Colons inside the
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
1
2
|
// Value-only import, aliased to avoid shadowing the ambient global
|
|
2
3
|
// `Temporal` TYPE that ConsumerStateRow.updatedAt/StoredEventRow.createdAt
|
|
3
4
|
// resolve against (same #1438 dual-package-hazard pattern as event-store.ts).
|
package/dist/time/polyfill.d.ts
CHANGED
package/dist/time/polyfill.js
CHANGED
package/dist/ui-types/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { BooleanFieldDef, DateFieldDef, EmbeddedDerivedCellDef, EntityDefin
|
|
|
9
9
|
export type { AccessRule, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, RoleAccessPersonalData, RoleAccessRule, } from "../engine/types/handlers.js";
|
|
10
10
|
export { isOpenToAllGranted, isUiAccessGranted } from "../engine/types/handlers.js";
|
|
11
11
|
export type { IconKey, NavDefinition, NavIconKey } from "../engine/types/nav.js";
|
|
12
|
-
export type { ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
|
|
12
|
+
export type { ActionFormRedirect, ActionFormScreenDefinition, ConfigEditScreenDefinition, CustomScreenDefinition, CustomScreenRoute, DashboardChartKind, DashboardChartPanel, DashboardChartTone, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelEmptyState, DashboardPanelQueryOptions, DashboardPanelSpan, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScopeDefinition, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardTimeRangeDefinition, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EditWriteFormSection, EntityEditScreenDefinition, EntityListScreenDefinition, FieldCondition, FieldIconKey, FieldRenderer, FormWidth, ListBadgeTone, ListColumnDisplay, ListColumnSpec, ListFacetSpec, ListSortSpec, MetricNavigate, MetricSpec, PlatformComponent, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, RelatedListToolbarAction, RowAction, RowActionDrawer, RowActionNavigate, RowActionWriteHandler, RowFieldExtractor, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenSlots, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, ToolbarAction, } from "../engine/types/screen.js";
|
|
13
13
|
export { metricField } from "../engine/types/screen.js";
|
|
14
14
|
export type { TargetRef } from "../engine/types/target-ref.js";
|
|
15
15
|
export type { TreeAction, TreeNode, TreeNodeState } from "../engine/types/tree-node.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.330.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -206,8 +206,8 @@
|
|
|
206
206
|
"./package.json": "./package.json"
|
|
207
207
|
},
|
|
208
208
|
"dependencies": {
|
|
209
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
210
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
209
|
+
"@cosmicdrift/kumiko-http": "0.330.0",
|
|
210
|
+
"@cosmicdrift/kumiko-types": "0.330.0",
|
|
211
211
|
"bullmq": "^5.76.7",
|
|
212
212
|
"bun-types": "^1.3.13",
|
|
213
213
|
"hono": "^4.13.1",
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
"zod": "^4.4.3"
|
|
224
224
|
},
|
|
225
225
|
"devDependencies": {
|
|
226
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
226
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.330.0",
|
|
227
227
|
"bun-types": "^1.3.13",
|
|
228
228
|
"pino-pretty": "^13.1.3"
|
|
229
229
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.330.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "TenantDb.aggregate and aggregateWhere: grouped count, countDistinct, sum and avg with time buckets (fw#3396)",
|
|
6
|
+
"detail": "`tenantDb.aggregate(table, { measure, groupBy?, orderByValue?, limit? }, where?)` and the standalone `aggregateWhere(db, table, spec, where?)` run one grouped aggregate with the same tenant scoping as `read`/`count`. A dimension can be a plain column or a time bucket (`{ field, bucket: 'hour' | 'day' | ..., timeZone }`) on a timestamptz column; bucket keys come back as epoch milliseconds. Types live in `@cosmicdrift/kumiko-types/aggregate-types`. Unknown fields, non-timestamptz bucket columns and bad limits throw before any SQL runs.",
|
|
7
|
+
"migration": "Only code that implements the `TenantDb` interface itself (for example a hand-written mock) must add an `aggregate` method; `createTenantDb` already provides it."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.329.0",
|
|
11
|
+
"type": "improvement",
|
|
12
|
+
"title": "WebSocket routes cap the bytes queued behind slow handlers",
|
|
13
|
+
"detail": "Frames that arrive while `onOpen` or an earlier `onMessage` is still running are queued in order. Their summed size is now limited to 1 MiB (`WEBSOCKET_PENDING_BUFFER_LIMIT_BYTES`, or the route's `maxMessageBytes` if larger); beyond that the socket closes with code 1013 and the queued frames are dropped."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.328.1",
|
|
4
17
|
"type": "improvement",
|