@cosmicdrift/kumiko-framework 0.319.0 → 0.320.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 +8 -4
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +6 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +11 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +8 -4
- package/src/api/__tests__/client-ip.test.ts +139 -0
- package/src/api/__tests__/extra-routes.integration.test.ts +8 -1
- package/src/api/__tests__/http-route-rate-limit.integration.test.ts +73 -2
- package/src/api/__tests__/pat-route-guard.integration.test.ts +126 -0
- package/src/api/__tests__/post-auth-landing.test.ts +100 -0
- package/src/api/__tests__/request-id-middleware.test.ts +35 -2
- package/src/api/__tests__/schema-route.integration.test.ts +157 -0
- package/src/api/api-constants.ts +2 -0
- package/src/api/auth-routes.ts +92 -95
- package/src/api/client-ip.ts +91 -0
- package/src/api/index.ts +15 -0
- package/src/api/pat-route-guard.ts +51 -0
- package/src/api/post-auth-landing.ts +105 -0
- package/src/api/request-context.ts +4 -2
- package/src/api/request-id-middleware.ts +44 -14
- package/src/api/server.ts +128 -40
- package/src/changes.json +73 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +47 -0
- package/src/engine/__tests__/metric-navigate-tab.test.ts +167 -0
- package/src/engine/__tests__/project-app-schema-for-roles.test.ts +500 -0
- package/src/engine/__tests__/toolbar-navigate-tab.test.ts +233 -0
- package/src/engine/boot-validator/pii-retention.ts +18 -2
- package/src/engine/boot-validator/screens.ts +75 -18
- package/src/engine/index.ts +1 -0
- package/src/engine/project-app-schema-for-roles.ts +950 -0
- package/src/engine/types/handlers.ts +1 -1
- package/src/engine/types/index.ts +5 -1
- package/src/entrypoint/index.ts +14 -1
- package/src/files/__tests__/files.integration.test.ts +105 -1
- package/src/files/file-routes.ts +54 -2
- package/src/files/index.ts +1 -1
- package/src/http/index.ts +10 -2
- package/src/jobs/__tests__/jobs.integration.test.ts +105 -3
- package/src/jobs/job-runner.ts +6 -1
- package/src/rate-limit/__tests__/middleware.integration.test.ts +24 -7
- package/src/rate-limit/middleware.ts +32 -8
- package/src/stack/__tests__/push-entity-projection-tables-column-sync.integration.test.ts +107 -0
- package/src/stack/push-entity-projection-tables.ts +52 -7
- package/src/stack/table-helpers.ts +17 -9
- package/src/stack/test-stack.ts +28 -1
- package/src/testing/real-providers.ts +2 -0
- package/src/ui-types/index.ts +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { withBootValidatorFixture } from "../../testing/boot-validator-fixture";
|
|
4
|
+
import { validateBoot as validateBootRaw } from "../boot-validator";
|
|
5
|
+
import { defineFeature } from "../define-feature";
|
|
6
|
+
import { createEntity, createTextField } from "../factories";
|
|
7
|
+
|
|
8
|
+
function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
|
|
9
|
+
validateBootRaw(withBootValidatorFixture(features));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("validateBoot — ToolbarAction.tab", () => {
|
|
13
|
+
test("entityList toolbar navigate + unknown tab throws with the action id", () => {
|
|
14
|
+
const feature = defineFeature("app", (r) => {
|
|
15
|
+
r.queryHandler("vehicle:detail", z.object({}), async () => ({ name: "x" }), {
|
|
16
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
17
|
+
});
|
|
18
|
+
r.screen({
|
|
19
|
+
id: "vehicle-detail",
|
|
20
|
+
type: "projectionDetail",
|
|
21
|
+
query: "app:query:vehicle:detail",
|
|
22
|
+
layout: {
|
|
23
|
+
mode: "tabs",
|
|
24
|
+
sections: [
|
|
25
|
+
{ id: "overview", title: "Overview", fields: ["name"] },
|
|
26
|
+
{ id: "channels", title: "Channels", fields: ["name"] },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
r.entity(
|
|
31
|
+
"car",
|
|
32
|
+
createEntity({
|
|
33
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
r.screen({
|
|
37
|
+
id: "car-list",
|
|
38
|
+
type: "entityList",
|
|
39
|
+
entity: "car",
|
|
40
|
+
columns: ["name"],
|
|
41
|
+
toolbarActions: [
|
|
42
|
+
{
|
|
43
|
+
kind: "navigate",
|
|
44
|
+
id: "view-channels",
|
|
45
|
+
label: "app.actions.viewChannels",
|
|
46
|
+
screen: "vehicle-detail",
|
|
47
|
+
tab: "unknown",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
expect(() => validateBoot([feature])).toThrow(
|
|
53
|
+
/toolbarAction "view-channels".*navigates to tab "unknown", which is not a section id on target screen/,
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("entityList toolbar navigate + known tab boots cleanly", () => {
|
|
58
|
+
const feature = defineFeature("app", (r) => {
|
|
59
|
+
r.queryHandler("vehicle:detail", z.object({}), async () => ({ name: "x" }), {
|
|
60
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
61
|
+
});
|
|
62
|
+
r.screen({
|
|
63
|
+
id: "vehicle-detail",
|
|
64
|
+
type: "projectionDetail",
|
|
65
|
+
query: "app:query:vehicle:detail",
|
|
66
|
+
layout: {
|
|
67
|
+
mode: "tabs",
|
|
68
|
+
sections: [
|
|
69
|
+
{ id: "overview", title: "Overview", fields: ["name"] },
|
|
70
|
+
{ id: "channels", title: "Channels", fields: ["name"] },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
r.entity(
|
|
75
|
+
"car",
|
|
76
|
+
createEntity({
|
|
77
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
r.screen({
|
|
81
|
+
id: "car-list",
|
|
82
|
+
type: "entityList",
|
|
83
|
+
entity: "car",
|
|
84
|
+
columns: ["name"],
|
|
85
|
+
toolbarActions: [
|
|
86
|
+
{
|
|
87
|
+
kind: "navigate",
|
|
88
|
+
id: "view-channels",
|
|
89
|
+
label: "app.actions.viewChannels",
|
|
90
|
+
screen: "vehicle-detail",
|
|
91
|
+
tab: "channels",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("entityList toolbar navigate targeting a non-tabs projectionDetail throws", () => {
|
|
100
|
+
const feature = defineFeature("app", (r) => {
|
|
101
|
+
r.queryHandler("vehicle:detail", z.object({}), async () => ({ name: "x" }), {
|
|
102
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
103
|
+
});
|
|
104
|
+
r.screen({
|
|
105
|
+
id: "vehicle-detail",
|
|
106
|
+
type: "projectionDetail",
|
|
107
|
+
query: "app:query:vehicle:detail",
|
|
108
|
+
layout: { sections: [{ title: "Overview", fields: ["name"] }] },
|
|
109
|
+
});
|
|
110
|
+
r.entity(
|
|
111
|
+
"car",
|
|
112
|
+
createEntity({
|
|
113
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
r.screen({
|
|
117
|
+
id: "car-list",
|
|
118
|
+
type: "entityList",
|
|
119
|
+
entity: "car",
|
|
120
|
+
columns: ["name"],
|
|
121
|
+
toolbarActions: [
|
|
122
|
+
{
|
|
123
|
+
kind: "navigate",
|
|
124
|
+
id: "view-channels",
|
|
125
|
+
label: "app.actions.viewChannels",
|
|
126
|
+
screen: "vehicle-detail",
|
|
127
|
+
tab: "overview",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
expect(() => validateBoot([feature])).toThrow(
|
|
133
|
+
/is not a projectionDetail with layout.mode "tabs"/,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("projectionList toolbar navigate + unknown tab throws", () => {
|
|
138
|
+
const feature = defineFeature("app", (r) => {
|
|
139
|
+
r.queryHandler("vehicle:detail", z.object({}), async () => ({ name: "x" }), {
|
|
140
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
141
|
+
});
|
|
142
|
+
r.screen({
|
|
143
|
+
id: "vehicle-detail",
|
|
144
|
+
type: "projectionDetail",
|
|
145
|
+
query: "app:query:vehicle:detail",
|
|
146
|
+
layout: {
|
|
147
|
+
mode: "tabs",
|
|
148
|
+
sections: [
|
|
149
|
+
{ id: "overview", title: "Overview", fields: ["name"] },
|
|
150
|
+
{ id: "channels", title: "Channels", fields: ["name"] },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
r.queryHandler("car:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
155
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
156
|
+
});
|
|
157
|
+
r.screen({
|
|
158
|
+
id: "car-projection-list",
|
|
159
|
+
type: "projectionList",
|
|
160
|
+
query: "app:query:car:list",
|
|
161
|
+
columns: ["name"],
|
|
162
|
+
toolbarActions: [
|
|
163
|
+
{
|
|
164
|
+
kind: "navigate",
|
|
165
|
+
id: "view-channels",
|
|
166
|
+
label: "app.actions.viewChannels",
|
|
167
|
+
screen: "vehicle-detail",
|
|
168
|
+
tab: "unknown",
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
expect(() => validateBoot([feature])).toThrow(
|
|
174
|
+
/toolbarAction "view-channels".*navigates to tab "unknown", which is not a section id on target screen/,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("relatedList section toolbar navigate + unknown tab throws", () => {
|
|
179
|
+
const feature = defineFeature("app", (r) => {
|
|
180
|
+
r.queryHandler("vehicle:detail", z.object({}), async () => ({ name: "x" }), {
|
|
181
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
182
|
+
});
|
|
183
|
+
r.screen({
|
|
184
|
+
id: "vehicle-detail",
|
|
185
|
+
type: "projectionDetail",
|
|
186
|
+
query: "app:query:vehicle:detail",
|
|
187
|
+
layout: {
|
|
188
|
+
mode: "tabs",
|
|
189
|
+
sections: [
|
|
190
|
+
{ id: "overview", title: "Overview", fields: ["name"] },
|
|
191
|
+
{ id: "channels", title: "Channels", fields: ["name"] },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
r.queryHandler("lease:detail", z.object({}), async () => ({ id: "1" }), {
|
|
196
|
+
access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
|
|
197
|
+
});
|
|
198
|
+
r.queryHandler(
|
|
199
|
+
"lease:positions",
|
|
200
|
+
z.object({}),
|
|
201
|
+
async () => ({ rows: [], nextCursor: null }),
|
|
202
|
+
{ access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
|
|
203
|
+
);
|
|
204
|
+
r.screen({
|
|
205
|
+
id: "lease-detail",
|
|
206
|
+
type: "projectionDetail",
|
|
207
|
+
query: "app:query:lease:detail",
|
|
208
|
+
layout: {
|
|
209
|
+
sections: [
|
|
210
|
+
{
|
|
211
|
+
kind: "relatedList",
|
|
212
|
+
title: "Positions",
|
|
213
|
+
query: "app:query:lease:positions",
|
|
214
|
+
columns: ["id"],
|
|
215
|
+
toolbarActions: [
|
|
216
|
+
{
|
|
217
|
+
kind: "navigate",
|
|
218
|
+
id: "view-channels",
|
|
219
|
+
label: "app.actions.viewChannels",
|
|
220
|
+
screen: "vehicle-detail",
|
|
221
|
+
tab: "unknown",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
expect(() => validateBoot([feature])).toThrow(
|
|
230
|
+
/toolbarAction "view-channels".*navigates to tab "unknown", which is not a section id on target screen/,
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FeatureDefinition } from "../types";
|
|
2
|
-
import type { ResolvedPiiFlags } from "../types/fields";
|
|
2
|
+
import type { EntityDefinition, ResolvedPiiFlags } from "../types/fields";
|
|
3
3
|
import {
|
|
4
4
|
PII_DIRECT_NAME_HINTS,
|
|
5
5
|
PII_USER_OWNED_NAME_HINTS,
|
|
@@ -27,6 +27,17 @@ function hasAnonymizableSubjectField(annot: ResolvedPiiFlags): boolean {
|
|
|
27
27
|
return Boolean(annot.pii || annot.userOwned || annot.tenantOwned || annot.recordOwned);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function ownerFieldNamesReferencedByPersonalOf(
|
|
31
|
+
fieldsByName: EntityDefinition["fields"],
|
|
32
|
+
): ReadonlySet<string> {
|
|
33
|
+
const ownerFieldNames = new Set<string>();
|
|
34
|
+
for (const field of Object.values(fieldsByName)) {
|
|
35
|
+
const ownerFieldName = (field as ResolvedPiiFlags).userOwned?.ownerField; // @cast-boundary schema-walk
|
|
36
|
+
if (typeof ownerFieldName === "string") ownerFieldNames.add(ownerFieldName);
|
|
37
|
+
}
|
|
38
|
+
return ownerFieldNames;
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
// --- PII / Subject-Key Annotations + Retention validation ---
|
|
31
42
|
//
|
|
32
43
|
// Drei Klassen von Checks:
|
|
@@ -54,6 +65,7 @@ function hasAnonymizableSubjectField(annot: ResolvedPiiFlags): boolean {
|
|
|
54
65
|
export function validatePiiAndRetention(feature: FeatureDefinition): void {
|
|
55
66
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
56
67
|
const fieldsByName = entity.fields;
|
|
68
|
+
const personalOfOwnerFieldNames = ownerFieldNamesReferencedByPersonalOf(fieldsByName);
|
|
57
69
|
|
|
58
70
|
for (const [fieldName, field] of Object.entries(fieldsByName)) {
|
|
59
71
|
// ResolvedPiiFlags properties are type-level optional. On field defs
|
|
@@ -192,7 +204,11 @@ export function validatePiiAndRetention(feature: FeatureDefinition): void {
|
|
|
192
204
|
console.warn(
|
|
193
205
|
`[kumiko:boot] [Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" has a user-content-typical name but no personal annotation. If this contains user-generated content, mark it { personal: { of: "<authorIdField>" }, find: ... }. If business data, set { personal: false, reason: "..." } to silence.`,
|
|
194
206
|
);
|
|
195
|
-
} else if (
|
|
207
|
+
} else if (
|
|
208
|
+
PII_USER_REFERENCE_NAME_HINTS.has(lower) &&
|
|
209
|
+
!annot.subjectRef &&
|
|
210
|
+
!personalOfOwnerFieldNames.has(fieldName)
|
|
211
|
+
) {
|
|
196
212
|
// biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
|
|
197
213
|
console.warn(
|
|
198
214
|
`[kumiko:boot] [Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" has a user-reference-typical name but no personal annotation — a foreign key into \`user\` carries Art.17 obligations even with no annotated content on the entity. Mark it { personal: "ref" } AND register r.useExtension(EXT_USER_DATA, "${entityName}", …) for Art.17 coverage — this warning is the only boot-time check for that, registering the hook is not enforced. Or { personal: { of: "${fieldName}" } } on the field it owns. If business data, set { personal: false, reason: "..." } to silence.`,
|
|
@@ -450,7 +450,7 @@ export function validateScreenShortIdCollisions(
|
|
|
450
450
|
// cross-feature screen QN (`<feature>:screen:<id>`) given verbatim — a
|
|
451
451
|
// short id can never itself be a valid QN (QN_SEGMENT forbids colons), so
|
|
452
452
|
// the two forms don't collide.
|
|
453
|
-
function resolveScreenTargetQn(featureName: string, target: string): string {
|
|
453
|
+
export function resolveScreenTargetQn(featureName: string, target: string): string {
|
|
454
454
|
return isValidQn(target) ? target : qualifyEntityName(featureName, "screen", target);
|
|
455
455
|
}
|
|
456
456
|
|
|
@@ -1158,9 +1158,9 @@ export function validateScreens(
|
|
|
1158
1158
|
}
|
|
1159
1159
|
validateAtMostOneRowClick(feature.name, screenId, "projectionList", screen.rowActions);
|
|
1160
1160
|
}
|
|
1161
|
-
// Only drawer-kind
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1161
|
+
// Only drawer-kind and navigate actions that set tab are validated
|
|
1162
|
+
// here — plain navigate (no tab) and writeHandler toolbarActions on
|
|
1163
|
+
// projectionList have no boot check yet (pre-existing gap).
|
|
1164
1164
|
if (screen.toolbarActions !== undefined) {
|
|
1165
1165
|
for (const action of screen.toolbarActions) {
|
|
1166
1166
|
if (action.kind === "drawer") {
|
|
@@ -1173,6 +1173,19 @@ export function validateScreens(
|
|
|
1173
1173
|
feature.screens,
|
|
1174
1174
|
);
|
|
1175
1175
|
}
|
|
1176
|
+
if (action.kind === "navigate" && action.tab !== undefined) {
|
|
1177
|
+
resolveRowActionNavigateTarget(
|
|
1178
|
+
feature.name,
|
|
1179
|
+
screenId,
|
|
1180
|
+
"projectionList",
|
|
1181
|
+
"toolbarAction",
|
|
1182
|
+
action,
|
|
1183
|
+
allScreenQns,
|
|
1184
|
+
navTargetShortIds,
|
|
1185
|
+
screensByShortId,
|
|
1186
|
+
detailForScreens,
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1176
1189
|
}
|
|
1177
1190
|
}
|
|
1178
1191
|
continue;
|
|
@@ -1239,18 +1252,35 @@ export function validateScreens(
|
|
|
1239
1252
|
);
|
|
1240
1253
|
}
|
|
1241
1254
|
const navigate = typeof metric === "string" ? undefined : metric.navigate;
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1255
|
+
if (navigate?.tab !== undefined) {
|
|
1256
|
+
if (navigate.screen === undefined && navigate.entity === undefined) {
|
|
1257
|
+
if (!screen.layout.sections.some((section) => section.id === navigate.tab)) {
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) metric "${field}" ` +
|
|
1260
|
+
`navigates to tab "${navigate.tab}", which is not a section id on this screen.`,
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
} else {
|
|
1264
|
+
resolveRowActionNavigateTarget(
|
|
1265
|
+
feature.name,
|
|
1266
|
+
screenId,
|
|
1267
|
+
"projectionDetail",
|
|
1268
|
+
"metric",
|
|
1269
|
+
{
|
|
1270
|
+
kind: "navigate",
|
|
1271
|
+
id: field,
|
|
1272
|
+
label: field,
|
|
1273
|
+
...(navigate.screen !== undefined ? { screen: navigate.screen } : {}),
|
|
1274
|
+
...(navigate.entity !== undefined ? { entity: navigate.entity } : {}),
|
|
1275
|
+
...(navigate.entityId !== undefined ? { entityId: navigate.entityId } : {}),
|
|
1276
|
+
tab: navigate.tab,
|
|
1277
|
+
},
|
|
1278
|
+
allScreenQns,
|
|
1279
|
+
navTargetShortIds,
|
|
1280
|
+
screensByShortId,
|
|
1281
|
+
detailForScreens,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1254
1284
|
}
|
|
1255
1285
|
}
|
|
1256
1286
|
}
|
|
@@ -1459,8 +1489,9 @@ export function validateScreens(
|
|
|
1459
1489
|
featureMap,
|
|
1460
1490
|
);
|
|
1461
1491
|
}
|
|
1462
|
-
// Only drawer-kind
|
|
1463
|
-
//
|
|
1492
|
+
// Only drawer-kind and navigate actions that set tab are validated
|
|
1493
|
+
// here — plain navigate (no tab) and writeHandler toolbarActions
|
|
1494
|
+
// have no boot check yet, same gap as above.
|
|
1464
1495
|
if (section.toolbarActions !== undefined) {
|
|
1465
1496
|
for (const action of section.toolbarActions) {
|
|
1466
1497
|
if (action.kind === "drawer") {
|
|
@@ -1473,6 +1504,19 @@ export function validateScreens(
|
|
|
1473
1504
|
feature.screens,
|
|
1474
1505
|
);
|
|
1475
1506
|
}
|
|
1507
|
+
if (action.kind === "navigate" && action.tab !== undefined) {
|
|
1508
|
+
resolveRowActionNavigateTarget(
|
|
1509
|
+
feature.name,
|
|
1510
|
+
screenId,
|
|
1511
|
+
"projectionDetail",
|
|
1512
|
+
"toolbarAction",
|
|
1513
|
+
action,
|
|
1514
|
+
allScreenQns,
|
|
1515
|
+
navTargetShortIds,
|
|
1516
|
+
screensByShortId,
|
|
1517
|
+
detailForScreens,
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1476
1520
|
}
|
|
1477
1521
|
}
|
|
1478
1522
|
continue;
|
|
@@ -2002,6 +2046,19 @@ export function validateScreens(
|
|
|
2002
2046
|
`navigate-target "${action.screen}" does not resolve to a registered screen in any feature.`,
|
|
2003
2047
|
);
|
|
2004
2048
|
}
|
|
2049
|
+
if (action.tab !== undefined) {
|
|
2050
|
+
resolveRowActionNavigateTarget(
|
|
2051
|
+
feature.name,
|
|
2052
|
+
screenId,
|
|
2053
|
+
"entityList",
|
|
2054
|
+
"toolbarAction",
|
|
2055
|
+
action,
|
|
2056
|
+
allScreenQns,
|
|
2057
|
+
navTargetShortIds,
|
|
2058
|
+
screensByShortId,
|
|
2059
|
+
detailForScreens,
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2005
2062
|
} else if (action.kind === "drawer") {
|
|
2006
2063
|
validateDrawerTargetAction(
|
|
2007
2064
|
feature.name,
|
package/src/engine/index.ts
CHANGED
|
@@ -267,6 +267,7 @@ export {
|
|
|
267
267
|
export type { ParsedRefTarget } from "./parse-ref-target";
|
|
268
268
|
export { parseRefTarget, parseRefTargetEntityName } from "./parse-ref-target";
|
|
269
269
|
export { buildPipelineSteps, stepsPipeline } from "./pipeline";
|
|
270
|
+
export { projectAppSchemaForRoles } from "./project-app-schema-for-roles";
|
|
270
271
|
export { defineApply, defineMspApply, setFields } from "./projection-helpers";
|
|
271
272
|
export type { BuiltinQnType, ParsedQn, QnType } from "./qualified-name";
|
|
272
273
|
export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name";
|