@cosmicdrift/kumiko-framework 0.205.0 → 0.206.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.206.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>",
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"./package.json": "./package.json"
|
|
191
191
|
},
|
|
192
192
|
"dependencies": {
|
|
193
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
193
|
+
"@cosmicdrift/kumiko-types": "0.206.0",
|
|
194
194
|
"bullmq": "^5.76.7",
|
|
195
195
|
"bun-types": "^1.3.13",
|
|
196
196
|
"hono": "^4.13.1",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"zod": "^4.4.3"
|
|
207
207
|
},
|
|
208
208
|
"devDependencies": {
|
|
209
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
209
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.206.0",
|
|
210
210
|
"bun-types": "^1.3.13",
|
|
211
211
|
"pino-pretty": "^13.1.3"
|
|
212
212
|
},
|
package/src/db/index.ts
CHANGED
|
@@ -108,6 +108,7 @@ export {
|
|
|
108
108
|
isUniqueViolation,
|
|
109
109
|
type PgErrorInfo,
|
|
110
110
|
} from "./pg-error";
|
|
111
|
+
export { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock";
|
|
111
112
|
export type { SelectOptions, WhereObject, WhereValue } from "./query-api";
|
|
112
113
|
export {
|
|
113
114
|
asRawClient,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnyDb } from "../query";
|
|
2
|
+
import { asRawClient } from "../query";
|
|
3
|
+
|
|
4
|
+
/** pg_advisory_xact_lock keyed on namespace+key hash — xact-scoped, auto-released at commit/rollback. */
|
|
5
|
+
export async function acquireNamespacedAdvisoryLock(
|
|
6
|
+
db: AnyDb,
|
|
7
|
+
namespace: number,
|
|
8
|
+
key: string,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
await asRawClient(db).unsafe(`SELECT pg_advisory_xact_lock($1, hashtext($2))`, [namespace, key]);
|
|
11
|
+
}
|
|
@@ -188,4 +188,54 @@ describe("validateBoot — projectionList screens", () => {
|
|
|
188
188
|
});
|
|
189
189
|
expect(() => validateBoot([feature])).toThrow(/paginated is derived/);
|
|
190
190
|
});
|
|
191
|
+
|
|
192
|
+
// fw#2164 Nebenbefund: the "at most one rowClick:true" cap (already
|
|
193
|
+
// enforced for entityList) didn't run for projectionList — a boot-time
|
|
194
|
+
// gap, not a rendering one (the renderer only wires the first match, see
|
|
195
|
+
// ProjectionListBody), but two conflicting rowClick actions on the same
|
|
196
|
+
// screen should still fail loud instead of silently picking one.
|
|
197
|
+
describe("projectionList rowAction rowClick", () => {
|
|
198
|
+
function makeFeature(rowClickCount: number) {
|
|
199
|
+
return defineFeature("ledger", (r) => {
|
|
200
|
+
r.queryHandler(
|
|
201
|
+
"schedule:list",
|
|
202
|
+
z.object({}),
|
|
203
|
+
async () => ({ rows: [], nextCursor: null }),
|
|
204
|
+
{
|
|
205
|
+
access: { openToAll: true },
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
r.screen({ id: "schedule-detail", type: "custom", renderer: { react: "stub" } });
|
|
209
|
+
r.screen({
|
|
210
|
+
id: "schedule-list",
|
|
211
|
+
type: "projectionList",
|
|
212
|
+
query: "ledger:query:schedule:list",
|
|
213
|
+
columns: ["description"],
|
|
214
|
+
rowActions: Array.from({ length: rowClickCount }, (_, i) => ({
|
|
215
|
+
kind: "navigate" as const,
|
|
216
|
+
id: `open-${i}`,
|
|
217
|
+
label: "actions.open",
|
|
218
|
+
screen: "schedule-detail",
|
|
219
|
+
rowClick: true,
|
|
220
|
+
})),
|
|
221
|
+
});
|
|
222
|
+
r.translations({
|
|
223
|
+
keys: {
|
|
224
|
+
"screen:schedule-list.title": { de: "Liste", en: "List" },
|
|
225
|
+
"screen:schedule-detail.title": { de: "Detail", en: "Detail" },
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
test("one rowClick navigate action passes boot", () => {
|
|
232
|
+
expect(() => validateBoot([makeFeature(1)])).not.toThrow();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("more than one rowClick action per list is rejected", () => {
|
|
236
|
+
expect(() => validateBoot([makeFeature(2)])).toThrow(
|
|
237
|
+
/at most one may fire on a row-body click/i,
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
191
241
|
});
|
|
@@ -25,6 +25,24 @@ import type {
|
|
|
25
25
|
ToolbarAction,
|
|
26
26
|
} from "../types/screen";
|
|
27
27
|
|
|
28
|
+
// entityList and projectionList both allow a rowAction to double as the
|
|
29
|
+
// row-body click target (rowClick: true, fw#1708/#2164) — at most one per
|
|
30
|
+
// screen, or the renderer can't tell which one should fire.
|
|
31
|
+
function validateAtMostOneRowClick(
|
|
32
|
+
featureName: string,
|
|
33
|
+
screenId: string,
|
|
34
|
+
screenType: "entityList" | "projectionList",
|
|
35
|
+
rowActions: readonly RowAction[],
|
|
36
|
+
): void {
|
|
37
|
+
const rowClickActions = rowActions.filter((a) => a.kind === "navigate" && a.rowClick === true);
|
|
38
|
+
if (rowClickActions.length > 1) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[Feature ${featureName}] Screen "${screenId}" (${screenType}) has ${rowClickActions.length} ` +
|
|
41
|
+
"rowActions marked rowClick:true — at most one may fire on a row-body click.",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
// A field type in NO_WIDGET_FIELD_TYPES renders read-only on the auto-wired
|
|
29
47
|
// entityEdit path (#1925) — a required field the user can never fill would
|
|
30
48
|
// block every save. Only the statically-resolvable case is caught here: a
|
|
@@ -354,6 +372,7 @@ export function validateScreens(
|
|
|
354
372
|
);
|
|
355
373
|
}
|
|
356
374
|
}
|
|
375
|
+
validateAtMostOneRowClick(feature.name, screenId, "projectionList", screen.rowActions);
|
|
357
376
|
}
|
|
358
377
|
continue;
|
|
359
378
|
}
|
|
@@ -889,15 +908,7 @@ export function validateScreens(
|
|
|
889
908
|
rowMeta,
|
|
890
909
|
);
|
|
891
910
|
}
|
|
892
|
-
|
|
893
|
-
(a) => a.kind === "navigate" && a.rowClick === true,
|
|
894
|
-
);
|
|
895
|
-
if (rowClickActions.length > 1) {
|
|
896
|
-
throw new Error(
|
|
897
|
-
`[Feature ${feature.name}] Screen "${screenId}" (entityList) has ${rowClickActions.length} ` +
|
|
898
|
-
"rowActions marked rowClick:true — at most one may fire on a row-body click.",
|
|
899
|
-
);
|
|
900
|
-
}
|
|
911
|
+
validateAtMostOneRowClick(feature.name, screenId, "entityList", screen.rowActions);
|
|
901
912
|
}
|
|
902
913
|
// Tier 2.7e-2: toolbarActions — analog zu rowActions, aber bisher
|
|
903
914
|
// ohne Validator. Typo'd navigate-targets und unregistrierte
|