@cosmicdrift/kumiko-framework 0.258.0 → 0.259.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 +3 -3
- package/src/__tests__/anonymous-access.integration.test.ts +3 -3
- package/src/__tests__/derived-fields.integration.test.ts +1 -1
- package/src/__tests__/entity-list-limits.integration.test.ts +1 -1
- package/src/__tests__/error-contract.integration.test.ts +1 -1
- package/src/__tests__/ownership-where-write-path.integration.test.ts +1 -1
- package/src/__tests__/ownership.integration.test.ts +7 -3
- package/src/__tests__/schema-cli-rebuild.integration.test.ts +2 -2
- package/src/__tests__/transition-guard.integration.test.ts +3 -3
- package/src/api/__tests__/api.test.ts +21 -3
- package/src/api/__tests__/batch.integration.test.ts +3 -3
- package/src/api/__tests__/body-limit.test.ts +7 -1
- package/src/api/__tests__/dispatcher-live.integration.test.ts +1 -1
- package/src/api/__tests__/http-route-rate-limit.integration.test.ts +7 -1
- package/src/api/__tests__/metrics-endpoint.test.ts +7 -1
- package/src/api/__tests__/nested-write.integration.test.ts +3 -3
- package/src/api/__tests__/server-boot-guards.test.ts +11 -4
- package/src/bun-db/__tests__/write-brand.test.ts +4 -1
- package/src/changes.json +6 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.test.ts +1 -1
- package/src/db/__tests__/assert-exists-in.integration.test.ts +1 -1
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +2 -2
- package/src/db/__tests__/collect-table-metas.test.ts +9 -5
- package/src/db/__tests__/compound-types.test.ts +1 -1
- package/src/db/__tests__/config-seed.integration.test.ts +3 -3
- package/src/db/__tests__/eagerload.integration.test.ts +7 -7
- package/src/db/__tests__/entity-table-from-registry.test.ts +8 -2
- package/src/db/__tests__/entity-table-meta-source.test.ts +7 -3
- package/src/db/__tests__/entity-table-types.test.ts +4 -4
- package/src/db/__tests__/event-store-executor-list.integration.test.ts +9 -2
- package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +4 -4
- package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +3 -3
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +4 -4
- package/src/db/__tests__/event-store-executor.integration.test.ts +11 -6
- package/src/db/__tests__/event-store-system-stream.integration.test.ts +2 -2
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +6 -4
- package/src/db/__tests__/located-timestamp.test.ts +1 -1
- package/src/db/__tests__/money.test.ts +1 -1
- package/src/db/__tests__/multi-row-insert.integration.test.ts +3 -3
- package/src/db/__tests__/schema-migration.integration.test.ts +30 -14
- package/src/db/__tests__/source-shadow-create.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-indexes.test.ts +14 -12
- package/src/db/__tests__/table-builder-required.test.ts +2 -2
- package/src/db/__tests__/tenant-db-where-merge.integration.test.ts +1 -1
- package/src/db/__tests__/tenant-db-where-merge.test.ts +5 -2
- package/src/db/__tests__/tenant-db.integration.test.ts +2 -2
- package/src/db/__tests__/unique-violation-mapping.integration.test.ts +2 -2
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +50 -8
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +138 -1
- package/src/engine/__tests__/boot-validator-detail-for.test.ts +28 -4
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +20 -4
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +9 -3
- package/src/engine/__tests__/boot-validator.test.ts +488 -91
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +2 -2
- package/src/engine/__tests__/engine.test.ts +128 -37
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +4 -4
- package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +1 -1
- package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +1 -1
- package/src/engine/__tests__/extend-entity-projection.test.ts +1 -1
- package/src/engine/__tests__/extends-registrar.test.ts +13 -3
- package/src/engine/__tests__/factories-long-text.test.ts +10 -6
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +1 -1
- package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
- package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/nav.test.ts +1 -1
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +1 -1
- package/src/engine/__tests__/projection-detail-actions.test.ts +6 -1
- package/src/engine/__tests__/projection-detail-singleton.test.ts +6 -1
- package/src/engine/__tests__/projection-detail-tabs.test.ts +6 -1
- package/src/engine/__tests__/projection-list-filter.integration.test.ts +7 -2
- package/src/engine/__tests__/projection.test.ts +1 -1
- package/src/engine/__tests__/registry.test.ts +13 -3
- package/src/engine/__tests__/schema-builder.test.ts +39 -11
- package/src/engine/__tests__/screen.test.ts +21 -15
- package/src/engine/__tests__/search-payload-extension.test.ts +15 -3
- package/src/engine/__tests__/store-table.test.ts +6 -3
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +12 -4
- package/src/engine/__tests__/validation-hooks.test.ts +7 -1
- package/src/engine/boot-validator/__tests__/ownership.test.ts +2 -2
- package/src/engine/boot-validator/query-refs.ts +11 -0
- package/src/engine/boot-validator/screens.ts +45 -2
- package/src/engine/build-config-feature-schema.ts +2 -2
- package/src/engine/index.ts +2 -0
- package/src/engine/types/index.ts +2 -0
- package/src/es-ops/__tests__/runner.integration.test.ts +1 -1
- package/src/event-store/__tests__/upcaster.integration.test.ts +1 -1
- package/src/files/__tests__/file-field-pipeline.integration.test.ts +1 -1
- package/src/files/__tests__/files.integration.test.ts +1 -1
- package/src/files/file-ref-entity.ts +9 -9
- package/src/i18n/__tests__/i18n.test.ts +7 -1
- package/src/migrations/__tests__/kumiko-drift.integration.test.ts +3 -1
- package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +2 -2
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +1 -1
- package/src/pipeline/__tests__/cascade-handler.integration.test.ts +23 -8
- package/src/pipeline/__tests__/cascade-handler.test.ts +18 -3
- package/src/pipeline/__tests__/causation-chain.integration.test.ts +1 -1
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +5 -3
- package/src/pipeline/__tests__/dispatcher.test.ts +42 -6
- package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +2 -2
- package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +1 -1
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +42 -6
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +2 -2
- package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +1 -1
- package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +6 -2
- package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +6 -2
- package/src/pipeline/__tests__/perf-rebuild.integration.test.ts +1 -1
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +3 -3
- package/src/pipeline/__tests__/query-projection.integration.test.ts +1 -1
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +2 -2
- package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +13 -2
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +1 -1
- package/src/search/__tests__/reindex-entity.integration.test.ts +6 -1
- package/src/search/__tests__/search-pii-derived-index.integration.test.ts +7 -1
- package/src/stack/__tests__/ownership-boot-guard.integration.test.ts +1 -1
- package/src/testing/__tests__/e2e-generator.test.ts +15 -5
- package/src/testing/shared-entities.ts +4 -2
- package/src/ui-types/index.ts +2 -0
|
@@ -17,7 +17,7 @@ const testTable = table("test_projection", {
|
|
|
17
17
|
function exampleEntity(name = "unit") {
|
|
18
18
|
return createEntity({
|
|
19
19
|
table: name,
|
|
20
|
-
fields: { name: createTextField() },
|
|
20
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -65,11 +65,15 @@ describe("getAllQueryHandlers", () => {
|
|
|
65
65
|
test("returns every registered query handler, qualified, across multiple features", () => {
|
|
66
66
|
const taskEntity = createEntity({
|
|
67
67
|
table: "registry_test_tasks",
|
|
68
|
-
fields: {
|
|
68
|
+
fields: {
|
|
69
|
+
title: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
70
|
+
},
|
|
69
71
|
});
|
|
70
72
|
const noteEntity = createEntity({
|
|
71
73
|
table: "registry_test_notes",
|
|
72
|
-
fields: {
|
|
74
|
+
fields: {
|
|
75
|
+
body: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
76
|
+
},
|
|
73
77
|
});
|
|
74
78
|
const taskFeature = defineFeature("registry-test-task", (r) => {
|
|
75
79
|
r.crud("task", taskEntity, {
|
|
@@ -283,7 +287,13 @@ describe("extensionSelector boot-validation", () => {
|
|
|
283
287
|
});
|
|
284
288
|
const consumer = defineFeature("money-horse", (r) => {
|
|
285
289
|
r.requires("cap-ext");
|
|
286
|
-
r.entity(
|
|
290
|
+
r.entity(
|
|
291
|
+
"credit",
|
|
292
|
+
createEntity({
|
|
293
|
+
table: "Credits",
|
|
294
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
287
297
|
r.writeHandler(
|
|
288
298
|
"doSomething",
|
|
289
299
|
z.object({}),
|
|
@@ -32,25 +32,34 @@ describe("buildInsertSchema", () => {
|
|
|
32
32
|
test.each<SchemaCase>([
|
|
33
33
|
{
|
|
34
34
|
name: "required text field",
|
|
35
|
-
fields: {
|
|
35
|
+
fields: {
|
|
36
|
+
email: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
37
|
+
},
|
|
36
38
|
valid: { email: "test@test.de" },
|
|
37
39
|
invalid: { email: "" },
|
|
38
40
|
},
|
|
39
41
|
{
|
|
40
42
|
name: "optional text field",
|
|
41
|
-
fields: { name: createTextField() },
|
|
43
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
42
44
|
valid: {},
|
|
43
45
|
invalid: null,
|
|
44
46
|
},
|
|
45
47
|
{
|
|
46
48
|
name: "text field with maxLength",
|
|
47
|
-
fields: { name: createTextField({ maxLength: 5 }) },
|
|
49
|
+
fields: { name: createTextField({ maxLength: 5, personal: false, reason: "test_fixture" }) },
|
|
48
50
|
valid: { name: "hello" },
|
|
49
51
|
invalid: { name: "toolong" },
|
|
50
52
|
},
|
|
51
53
|
{
|
|
52
54
|
name: "email format",
|
|
53
|
-
fields: {
|
|
55
|
+
fields: {
|
|
56
|
+
email: createTextField({
|
|
57
|
+
required: true,
|
|
58
|
+
format: "email",
|
|
59
|
+
personal: false,
|
|
60
|
+
reason: "test_fixture",
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
54
63
|
valid: { email: "a@b.de" },
|
|
55
64
|
invalid: { email: "not-an-email" },
|
|
56
65
|
},
|
|
@@ -166,8 +175,13 @@ describe("buildInsertSchema", () => {
|
|
|
166
175
|
const entity = createEntity({
|
|
167
176
|
table: "Users",
|
|
168
177
|
fields: {
|
|
169
|
-
email: createTextField({
|
|
170
|
-
|
|
178
|
+
email: createTextField({
|
|
179
|
+
required: true,
|
|
180
|
+
format: "email",
|
|
181
|
+
personal: false,
|
|
182
|
+
reason: "test_fixture",
|
|
183
|
+
}),
|
|
184
|
+
firstName: createTextField({ personal: false, reason: "test_fixture" }),
|
|
171
185
|
isEnabled: createBooleanField({ default: true }),
|
|
172
186
|
locale: createSelectField({ options: ["de", "en"] as const }),
|
|
173
187
|
},
|
|
@@ -281,7 +295,9 @@ describe("buildInsertSchema", () => {
|
|
|
281
295
|
test("required text rejects empty string", () => {
|
|
282
296
|
const entity = createEntity({
|
|
283
297
|
table: "Test",
|
|
284
|
-
fields: {
|
|
298
|
+
fields: {
|
|
299
|
+
name: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
300
|
+
},
|
|
285
301
|
});
|
|
286
302
|
const schema = buildInsertSchema(entity);
|
|
287
303
|
expect(schema.safeParse({ name: "" }).success).toBe(false);
|
|
@@ -353,7 +369,7 @@ describe("buildInsertSchema", () => {
|
|
|
353
369
|
const entity = createEntity({
|
|
354
370
|
table: "Test",
|
|
355
371
|
fields: {
|
|
356
|
-
name: createTextField({ required: true }),
|
|
372
|
+
name: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
357
373
|
address: createEmbeddedField({ street: { type: "text" } }),
|
|
358
374
|
},
|
|
359
375
|
});
|
|
@@ -1061,8 +1077,13 @@ describe("buildUpdateSchema", () => {
|
|
|
1061
1077
|
const entity = createEntity({
|
|
1062
1078
|
table: "Users",
|
|
1063
1079
|
fields: {
|
|
1064
|
-
email: createTextField({
|
|
1065
|
-
|
|
1080
|
+
email: createTextField({
|
|
1081
|
+
required: true,
|
|
1082
|
+
format: "email",
|
|
1083
|
+
personal: false,
|
|
1084
|
+
reason: "test_fixture",
|
|
1085
|
+
}),
|
|
1086
|
+
firstName: createTextField({ personal: false, reason: "test_fixture" }),
|
|
1066
1087
|
isEnabled: createBooleanField(),
|
|
1067
1088
|
},
|
|
1068
1089
|
});
|
|
@@ -1082,7 +1103,14 @@ describe("buildUpdateSchema", () => {
|
|
|
1082
1103
|
test("still validates format on provided fields", () => {
|
|
1083
1104
|
const entity = createEntity({
|
|
1084
1105
|
table: "Users",
|
|
1085
|
-
fields: {
|
|
1106
|
+
fields: {
|
|
1107
|
+
email: createTextField({
|
|
1108
|
+
required: true,
|
|
1109
|
+
format: "email",
|
|
1110
|
+
personal: false,
|
|
1111
|
+
reason: "test_fixture",
|
|
1112
|
+
}),
|
|
1113
|
+
},
|
|
1086
1114
|
});
|
|
1087
1115
|
|
|
1088
1116
|
const schema = buildUpdateSchema(entity);
|
|
@@ -16,8 +16,8 @@ function productEntity() {
|
|
|
16
16
|
return createEntity({
|
|
17
17
|
table: "products",
|
|
18
18
|
fields: {
|
|
19
|
-
name: createTextField(),
|
|
20
|
-
sku: createTextField(),
|
|
19
|
+
name: createTextField({ personal: false, reason: "test_fixture" }),
|
|
20
|
+
sku: createTextField({ personal: false, reason: "test_fixture" }),
|
|
21
21
|
},
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -26,7 +26,7 @@ function derivedProductEntity() {
|
|
|
26
26
|
return createEntity({
|
|
27
27
|
table: "derived_products",
|
|
28
28
|
fields: {
|
|
29
|
-
name: createTextField(),
|
|
29
|
+
name: createTextField({ personal: false, reason: "test_fixture" }),
|
|
30
30
|
},
|
|
31
31
|
derivedFields: {
|
|
32
32
|
summary: createDerivedField({
|
|
@@ -306,7 +306,10 @@ describe("r.screen() — registration", () => {
|
|
|
306
306
|
defineFeature("billing", (r) => {
|
|
307
307
|
r.entity(
|
|
308
308
|
"invoice",
|
|
309
|
-
createEntity({
|
|
309
|
+
createEntity({
|
|
310
|
+
table: "invoices",
|
|
311
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
312
|
+
}),
|
|
310
313
|
);
|
|
311
314
|
r.screen({
|
|
312
315
|
id: "invoice-detail",
|
|
@@ -695,7 +698,10 @@ describe("r.screen() — registration", () => {
|
|
|
695
698
|
defineFeature("billing", (r) => {
|
|
696
699
|
r.entity(
|
|
697
700
|
"invoice",
|
|
698
|
-
createEntity({
|
|
701
|
+
createEntity({
|
|
702
|
+
table: "invoices",
|
|
703
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
704
|
+
}),
|
|
699
705
|
);
|
|
700
706
|
r.screen({
|
|
701
707
|
id: "invoice-detail",
|
|
@@ -774,7 +780,7 @@ describe("r.screen() — registration", () => {
|
|
|
774
780
|
{
|
|
775
781
|
kind: "writeForm",
|
|
776
782
|
title: "s",
|
|
777
|
-
fieldDefs: { name: createTextField() },
|
|
783
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
778
784
|
fields: [],
|
|
779
785
|
handler: "app:write:save",
|
|
780
786
|
},
|
|
@@ -798,7 +804,7 @@ describe("r.screen() — registration", () => {
|
|
|
798
804
|
{
|
|
799
805
|
kind: "writeForm",
|
|
800
806
|
title: "s",
|
|
801
|
-
fieldDefs: { name: createTextField() },
|
|
807
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
802
808
|
fields: ["sku"],
|
|
803
809
|
handler: "app:write:save",
|
|
804
810
|
},
|
|
@@ -825,7 +831,7 @@ describe("r.screen() — registration", () => {
|
|
|
825
831
|
{
|
|
826
832
|
kind: "writeForm",
|
|
827
833
|
title: "s",
|
|
828
|
-
fieldDefs: { name: createTextField() },
|
|
834
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
829
835
|
fields: ["name"],
|
|
830
836
|
handler: "app:write:save",
|
|
831
837
|
},
|
|
@@ -849,7 +855,7 @@ describe("r.screen() — registration", () => {
|
|
|
849
855
|
{
|
|
850
856
|
kind: "writeForm",
|
|
851
857
|
title: "s",
|
|
852
|
-
fieldDefs: { name: createTextField() },
|
|
858
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
853
859
|
fields: ["name"],
|
|
854
860
|
handler: "app:write:missing",
|
|
855
861
|
},
|
|
@@ -886,7 +892,7 @@ describe("r.screen() — registration", () => {
|
|
|
886
892
|
{
|
|
887
893
|
kind: "writeForm",
|
|
888
894
|
title: "s",
|
|
889
|
-
fieldDefs: { name: createTextField() },
|
|
895
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
890
896
|
fields: ["name"],
|
|
891
897
|
handler: "app:write:save",
|
|
892
898
|
submitLabel: "actions.save",
|
|
@@ -928,7 +934,7 @@ describe("r.screen() — registration", () => {
|
|
|
928
934
|
{
|
|
929
935
|
kind: "writeForm",
|
|
930
936
|
title: "s",
|
|
931
|
-
fieldDefs: { name: createTextField() },
|
|
937
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
932
938
|
fields: ["name"],
|
|
933
939
|
handler: "app:write:save",
|
|
934
940
|
},
|
|
@@ -970,7 +976,7 @@ describe("r.screen() — registration", () => {
|
|
|
970
976
|
{
|
|
971
977
|
kind: "writeForm",
|
|
972
978
|
title: "s",
|
|
973
|
-
fieldDefs: { name: createTextField() },
|
|
979
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
974
980
|
fields: ["name"],
|
|
975
981
|
handler: "app:write:save",
|
|
976
982
|
},
|
|
@@ -996,7 +1002,7 @@ describe("r.screen() — registration", () => {
|
|
|
996
1002
|
{
|
|
997
1003
|
kind: "writeForm",
|
|
998
1004
|
title: "s",
|
|
999
|
-
fieldDefs: { name: createTextField() },
|
|
1005
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
1000
1006
|
fields: ["name"],
|
|
1001
1007
|
handler: "shop:write:save",
|
|
1002
1008
|
},
|
|
@@ -1027,7 +1033,7 @@ describe("r.screen() — registration", () => {
|
|
|
1027
1033
|
{
|
|
1028
1034
|
kind: "writeForm",
|
|
1029
1035
|
title: "s",
|
|
1030
|
-
fieldDefs: { name: createTextField() },
|
|
1036
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
1031
1037
|
fields: ["name"],
|
|
1032
1038
|
handler: "shop:write:save",
|
|
1033
1039
|
},
|
|
@@ -1060,7 +1066,7 @@ describe("r.screen() — registration", () => {
|
|
|
1060
1066
|
{
|
|
1061
1067
|
kind: "writeForm",
|
|
1062
1068
|
title: "s",
|
|
1063
|
-
fieldDefs: { name: createTextField() },
|
|
1069
|
+
fieldDefs: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
1064
1070
|
fields: ["name"],
|
|
1065
1071
|
handler: "shop:write:save",
|
|
1066
1072
|
},
|
|
@@ -124,7 +124,12 @@ describe("buildSearchDocument — contributor precedence (base fields win)", ()
|
|
|
124
124
|
const feature = defineFeature("test", (r) => {
|
|
125
125
|
const thing = r.entity(
|
|
126
126
|
"thing",
|
|
127
|
-
createEntity({
|
|
127
|
+
createEntity({
|
|
128
|
+
table: "things",
|
|
129
|
+
fields: {
|
|
130
|
+
title: createTextField({ searchable: true, personal: false, reason: "test_fixture" }),
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
128
133
|
);
|
|
129
134
|
r.searchPayloadExtension(thing, contributor);
|
|
130
135
|
});
|
|
@@ -151,7 +156,9 @@ describe("buildSearchDocument — contributor precedence (base fields win)", ()
|
|
|
151
156
|
"thing-dedup",
|
|
152
157
|
createEntity({
|
|
153
158
|
table: "things_dedup",
|
|
154
|
-
fields: {
|
|
159
|
+
fields: {
|
|
160
|
+
title: createTextField({ searchable: true, personal: false, reason: "test_fixture" }),
|
|
161
|
+
},
|
|
155
162
|
}),
|
|
156
163
|
);
|
|
157
164
|
r.searchPayloadExtension(thing, () => ({ title: "from-contributor" }));
|
|
@@ -177,7 +184,12 @@ describe("buildSearchDocument — contributor precedence (base fields win)", ()
|
|
|
177
184
|
const feature = defineFeature("test", (r) => {
|
|
178
185
|
const thing = r.entity(
|
|
179
186
|
"thing",
|
|
180
|
-
createEntity({
|
|
187
|
+
createEntity({
|
|
188
|
+
table: "things",
|
|
189
|
+
fields: {
|
|
190
|
+
title: createTextField({ searchable: true, personal: false, reason: "test_fixture" }),
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
181
193
|
);
|
|
182
194
|
r.searchPayloadExtension(thing, () => ({ overlap: "first" }));
|
|
183
195
|
r.searchPayloadExtension(thing, () => ({ overlap: "second" }));
|
|
@@ -65,7 +65,7 @@ describe("r.storeTable — declaration", () => {
|
|
|
65
65
|
test("rejects an EntityTableMeta whose source is not 'unmanaged' (#1209)", () => {
|
|
66
66
|
const managedEntity = createEntity({
|
|
67
67
|
table: "rt_probe_managed",
|
|
68
|
-
fields: { name: createTextField() },
|
|
68
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
69
69
|
});
|
|
70
70
|
const managedMeta = deriveEntityTableMeta("rt-probe-managed", managedEntity);
|
|
71
71
|
expect(() =>
|
|
@@ -173,7 +173,10 @@ describe("createRegistry — storeTable aggregation", () => {
|
|
|
173
173
|
// default would carry read_, which r.storeTable now rejects outright
|
|
174
174
|
// (#1220), so the collision case needs a table name storeTable can
|
|
175
175
|
// actually register.
|
|
176
|
-
const widget = createEntity({
|
|
176
|
+
const widget = createEntity({
|
|
177
|
+
table: "shop_widgets",
|
|
178
|
+
fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
179
|
+
});
|
|
177
180
|
// resolveTableName mirrors the migrate-runner — pin the exact physical name.
|
|
178
181
|
const physical = resolveTableName("widget", widget, "shop");
|
|
179
182
|
const clashing = defineUnmanagedTable({
|
|
@@ -203,7 +206,7 @@ describe("createRegistry — store tables with PII-annotated fields (#820)", ()
|
|
|
203
206
|
const piiEntity = createEntity({
|
|
204
207
|
table: "rt_pii_probe",
|
|
205
208
|
fields: {
|
|
206
|
-
userId: createTextField({ required: true }),
|
|
209
|
+
userId: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
207
210
|
ip: createTextField({
|
|
208
211
|
personal: { of: "userId" },
|
|
209
212
|
find: "none",
|
|
@@ -52,7 +52,9 @@ describe("validateProjectionAllowlist", () => {
|
|
|
52
52
|
"widget",
|
|
53
53
|
createEntity({
|
|
54
54
|
table: "widgets",
|
|
55
|
-
fields: {
|
|
55
|
+
fields: {
|
|
56
|
+
label: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
57
|
+
},
|
|
56
58
|
}),
|
|
57
59
|
);
|
|
58
60
|
});
|
|
@@ -97,7 +99,9 @@ describe("validateProjectionAllowlist", () => {
|
|
|
97
99
|
"widget",
|
|
98
100
|
createEntity({
|
|
99
101
|
table: "widgets-delete",
|
|
100
|
-
fields: {
|
|
102
|
+
fields: {
|
|
103
|
+
label: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
104
|
+
},
|
|
101
105
|
}),
|
|
102
106
|
);
|
|
103
107
|
});
|
|
@@ -353,7 +357,9 @@ describe("validateProjectionAllowlist", () => {
|
|
|
353
357
|
"thing",
|
|
354
358
|
createEntity({
|
|
355
359
|
table: "shared_table",
|
|
356
|
-
fields: {
|
|
360
|
+
fields: {
|
|
361
|
+
label: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
362
|
+
},
|
|
357
363
|
}),
|
|
358
364
|
);
|
|
359
365
|
});
|
|
@@ -362,7 +368,9 @@ describe("validateProjectionAllowlist", () => {
|
|
|
362
368
|
"thing",
|
|
363
369
|
createEntity({
|
|
364
370
|
table: "shared_table",
|
|
365
|
-
fields: {
|
|
371
|
+
fields: {
|
|
372
|
+
label: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
373
|
+
},
|
|
366
374
|
}),
|
|
367
375
|
);
|
|
368
376
|
});
|
|
@@ -6,7 +6,13 @@ import { runValidation } from "../validation";
|
|
|
6
6
|
describe("validation hooks", () => {
|
|
7
7
|
test("r.hook registers validation hook", () => {
|
|
8
8
|
const feature = defineFeature("test", (r) => {
|
|
9
|
-
r.entity(
|
|
9
|
+
r.entity(
|
|
10
|
+
"user",
|
|
11
|
+
createEntity({
|
|
12
|
+
table: "Users",
|
|
13
|
+
fields: { email: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
10
16
|
r.hook("validation", "user:create", (data) => {
|
|
11
17
|
const errors: ValidationError[] = [];
|
|
12
18
|
if (!data["email"]) errors.push({ field: "email", error: "required" });
|
|
@@ -32,8 +32,8 @@ function featureWith(entityAccess: AccessMaps, fieldAccess?: AccessMaps): Featur
|
|
|
32
32
|
createEntity({
|
|
33
33
|
table: "fw2626_guard_memos",
|
|
34
34
|
fields: {
|
|
35
|
-
ownerId: createTextField({ required: true }),
|
|
36
|
-
title: createTextField({ access: fieldAccess }),
|
|
35
|
+
ownerId: createTextField({ required: true, personal: false, reason: "test_fixture" }),
|
|
36
|
+
title: createTextField({ access: fieldAccess, personal: false, reason: "test_fixture" }),
|
|
37
37
|
},
|
|
38
38
|
access: entityAccess,
|
|
39
39
|
}),
|
|
@@ -56,6 +56,17 @@ function checkDashboardQueryRefs(
|
|
|
56
56
|
): void {
|
|
57
57
|
for (const panel of screen.panels) {
|
|
58
58
|
if (panel.kind === "custom") continue;
|
|
59
|
+
if (panel.kind === "screen") {
|
|
60
|
+
if (panel.visibleWhen !== undefined) {
|
|
61
|
+
checkQueryRef(
|
|
62
|
+
queryHandlers,
|
|
63
|
+
panel.visibleWhen.query,
|
|
64
|
+
() =>
|
|
65
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}" visibleWhen`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
59
70
|
if (panel.kind === "stat-group") {
|
|
60
71
|
for (const stat of panel.stats) {
|
|
61
72
|
checkQueryRef(
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
DashboardFilterDefinition,
|
|
24
24
|
DashboardPanelDefinition,
|
|
25
25
|
DashboardScreenDefinition,
|
|
26
|
+
DashboardScreenPanel,
|
|
26
27
|
DashboardStatGroupPanel,
|
|
27
28
|
EditFieldSpec,
|
|
28
29
|
EditLayout,
|
|
@@ -1234,7 +1235,7 @@ export function validateScreens(
|
|
|
1234
1235
|
}
|
|
1235
1236
|
|
|
1236
1237
|
if (screen.type === "dashboard") {
|
|
1237
|
-
validateDashboardScreen(feature.name, screenId, screen);
|
|
1238
|
+
validateDashboardScreen(feature.name, screenId, screen, featureMap);
|
|
1238
1239
|
continue;
|
|
1239
1240
|
}
|
|
1240
1241
|
|
|
@@ -1807,6 +1808,7 @@ function validateDashboardScreen(
|
|
|
1807
1808
|
featureName: string,
|
|
1808
1809
|
screenId: string,
|
|
1809
1810
|
screen: DashboardScreenDefinition,
|
|
1811
|
+
featureMap: ReadonlyMap<string, FeatureDefinition>,
|
|
1810
1812
|
): void {
|
|
1811
1813
|
if (screen.panels.length === 0) {
|
|
1812
1814
|
throw new Error(
|
|
@@ -1830,6 +1832,8 @@ function validateDashboardScreen(
|
|
|
1830
1832
|
validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId);
|
|
1831
1833
|
} else if (panel.kind === "custom") {
|
|
1832
1834
|
validateDashboardCustomPanel(featureName, screenId, panel);
|
|
1835
|
+
} else if (panel.kind === "screen") {
|
|
1836
|
+
validateDashboardScreenPanel(featureName, screenId, panel, featureMap);
|
|
1833
1837
|
} else {
|
|
1834
1838
|
validateDashboardQueryPanel(featureName, screenId, panel);
|
|
1835
1839
|
}
|
|
@@ -1840,6 +1844,42 @@ function validateDashboardScreen(
|
|
|
1840
1844
|
}
|
|
1841
1845
|
}
|
|
1842
1846
|
|
|
1847
|
+
const EMBEDDABLE_SCREEN_TYPES: ReadonlySet<ScreenDefinition["type"]> = new Set([
|
|
1848
|
+
"projectionList",
|
|
1849
|
+
"actionForm",
|
|
1850
|
+
"secretMint",
|
|
1851
|
+
"configEdit",
|
|
1852
|
+
"secretsEdit",
|
|
1853
|
+
]);
|
|
1854
|
+
|
|
1855
|
+
function validateDashboardScreenPanel(
|
|
1856
|
+
featureName: string,
|
|
1857
|
+
screenId: string,
|
|
1858
|
+
panel: DashboardScreenPanel,
|
|
1859
|
+
featureMap: ReadonlyMap<string, FeatureDefinition>,
|
|
1860
|
+
): void {
|
|
1861
|
+
const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}"`;
|
|
1862
|
+
const targetQn = resolveScreenTargetQn(featureName, panel.screen);
|
|
1863
|
+
const [targetFeatureName = "", , targetShortId = ""] = targetQn.split(":");
|
|
1864
|
+
const target = featureMap.get(targetFeatureName)?.screens[targetShortId];
|
|
1865
|
+
if (target === undefined) {
|
|
1866
|
+
throw new Error(
|
|
1867
|
+
`${context} screen "${panel.screen}" does not resolve to a registered screen (checked "${targetQn}"). ` +
|
|
1868
|
+
`Use a same-feature short id or a cross-feature QN "<feature>:screen:<id>".`,
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
if (!EMBEDDABLE_SCREEN_TYPES.has(target.type)) {
|
|
1872
|
+
throw new Error(
|
|
1873
|
+
`${context} embeds "${targetQn}" of type "${target.type}", which can't be embedded — allowed: ` +
|
|
1874
|
+
`${[...EMBEDDABLE_SCREEN_TYPES].join(", ")}. Use a custom panel for app components.`,
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
const visibleWhen = panel.visibleWhen;
|
|
1878
|
+
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
1879
|
+
throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1843
1883
|
function validateDashboardStatGroupPanel(
|
|
1844
1884
|
featureName: string,
|
|
1845
1885
|
screenId: string,
|
|
@@ -1882,7 +1922,10 @@ function validateDashboardCustomPanel(
|
|
|
1882
1922
|
function validateDashboardQueryPanel(
|
|
1883
1923
|
featureName: string,
|
|
1884
1924
|
screenId: string,
|
|
1885
|
-
panel: Exclude<
|
|
1925
|
+
panel: Exclude<
|
|
1926
|
+
DashboardPanelDefinition,
|
|
1927
|
+
DashboardStatGroupPanel | DashboardCustomPanel | DashboardScreenPanel
|
|
1928
|
+
>,
|
|
1886
1929
|
): void {
|
|
1887
1930
|
if (!panel.query || typeof panel.query !== "string") {
|
|
1888
1931
|
throw new Error(
|
|
@@ -442,9 +442,9 @@ function deriveField(def: ConfigKeyDefinition): FieldDefinition {
|
|
|
442
442
|
case "select":
|
|
443
443
|
return def.options !== undefined && def.options.length > 0
|
|
444
444
|
? createSelectField({ options: def.options })
|
|
445
|
-
: createTextField();
|
|
445
|
+
: createTextField({ personal: false, reason: "system_metadata" });
|
|
446
446
|
default:
|
|
447
|
-
return createTextField();
|
|
447
|
+
return createTextField({ personal: false, reason: "system_metadata" });
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
450
|
|
package/src/engine/index.ts
CHANGED
|
@@ -333,8 +333,10 @@ export type {
|
|
|
333
333
|
DashboardFilterDefinition,
|
|
334
334
|
DashboardListPanel,
|
|
335
335
|
DashboardPanelDefinition,
|
|
336
|
+
DashboardPanelVisibility,
|
|
336
337
|
DashboardProgressListPanel,
|
|
337
338
|
DashboardScreenDefinition,
|
|
339
|
+
DashboardScreenPanel,
|
|
338
340
|
DashboardStatGroupPanel,
|
|
339
341
|
DashboardStatPanel,
|
|
340
342
|
DateFieldDef,
|
|
@@ -270,8 +270,10 @@ export type {
|
|
|
270
270
|
DashboardFilterDefinition,
|
|
271
271
|
DashboardListPanel,
|
|
272
272
|
DashboardPanelDefinition,
|
|
273
|
+
DashboardPanelVisibility,
|
|
273
274
|
DashboardProgressListPanel,
|
|
274
275
|
DashboardScreenDefinition,
|
|
276
|
+
DashboardScreenPanel,
|
|
275
277
|
DashboardStatGroupPanel,
|
|
276
278
|
DashboardStatPanel,
|
|
277
279
|
EditExtensionSection,
|
|
@@ -43,7 +43,7 @@ import { runPendingSeedMigrations } from "../runner";
|
|
|
43
43
|
// is exercised against the production write-pipeline, not a stubbed return.
|
|
44
44
|
const probeEntity = createEntity({
|
|
45
45
|
table: "read_esops_probes",
|
|
46
|
-
fields: { label: createTextField({ required: true }) },
|
|
46
|
+
fields: { label: createTextField({ personal: false, reason: "test_fixture", required: true }) },
|
|
47
47
|
});
|
|
48
48
|
const probeTable = buildEntityTable("esops-probe", probeEntity);
|
|
49
49
|
const probeExecutor = createEventStoreExecutor(probeTable, probeEntity, {
|
|
@@ -33,7 +33,7 @@ import { upcastStoredEvent } from "../upcaster";
|
|
|
33
33
|
const orderEntity = createEntity({
|
|
34
34
|
table: "read_upcast_orders",
|
|
35
35
|
fields: {
|
|
36
|
-
customer: createTextField({ required: true }),
|
|
36
|
+
customer: createTextField({ personal: false, reason: "test_fixture", required: true }),
|
|
37
37
|
},
|
|
38
38
|
});
|
|
39
39
|
const orderTable = buildEntityTable("upcast-order", orderEntity);
|
|
@@ -47,7 +47,7 @@ import { createLocalProvider } from "../local-provider";
|
|
|
47
47
|
const documentEntity = createEntity({
|
|
48
48
|
table: "pipeline_documents",
|
|
49
49
|
fields: {
|
|
50
|
-
title: createTextField({ required: true }),
|
|
50
|
+
title: createTextField({ personal: false, reason: "test_fixture", required: true }),
|
|
51
51
|
attachment: createFileField(),
|
|
52
52
|
cover: createImageField(),
|
|
53
53
|
photos: createImagesField(),
|
|
@@ -57,7 +57,7 @@ const JWT_SECRET = "files-test-secret-at-least-32-characters!!";
|
|
|
57
57
|
const testTenantEntity = createEntity({
|
|
58
58
|
table: "test_tenants",
|
|
59
59
|
fields: {
|
|
60
|
-
name: createTextField({ required: true }),
|
|
60
|
+
name: createTextField({ personal: false, reason: "test_fixture", required: true }),
|
|
61
61
|
logo: createImageField({ maxSize: "2mb", accept: ["png", "jpg"] }),
|
|
62
62
|
},
|
|
63
63
|
});
|
|
@@ -14,20 +14,20 @@ import { createBigIntField, createEntity, createTextField } from "../engine";
|
|
|
14
14
|
// ohne `.default(now()).notNull()` macht inserted_at still nullable.
|
|
15
15
|
//
|
|
16
16
|
// PII-Annotations:
|
|
17
|
-
// - fileName → personal: "self" (
|
|
18
|
-
// "Marc-Lebenslauf.pdf", "Krankheitsattest-Mai.pdf").
|
|
19
|
-
// (storageKey, mimeType,
|
|
20
|
-
//
|
|
17
|
+
// - fileName → personal: "self" (original filename often carries a personal
|
|
18
|
+
// reference: "Marc-Lebenslauf.pdf", "Krankheitsattest-Mai.pdf"). The other
|
|
19
|
+
// fields (storageKey, mimeType, entityType, entityId, fieldName) are
|
|
20
|
+
// technical references or system metadata, not subject content.
|
|
21
21
|
export const fileRefEntity = createEntity({
|
|
22
22
|
table: "file_refs",
|
|
23
23
|
softDelete: true,
|
|
24
24
|
fields: {
|
|
25
|
-
storageKey: createTextField({ required: true }),
|
|
25
|
+
storageKey: createTextField({ required: true, personal: false, reason: "technical_reference" }),
|
|
26
26
|
fileName: createTextField({ required: true, personal: "self", find: "none" }),
|
|
27
|
-
mimeType: createTextField({ required: true }),
|
|
27
|
+
mimeType: createTextField({ required: true, personal: false, reason: "system_metadata" }),
|
|
28
28
|
size: createBigIntField({ required: true }),
|
|
29
|
-
entityType: createTextField(),
|
|
30
|
-
entityId: createTextField(),
|
|
31
|
-
fieldName: createTextField(),
|
|
29
|
+
entityType: createTextField({ personal: false, reason: "technical_reference" }),
|
|
30
|
+
entityId: createTextField({ personal: false, reason: "technical_reference" }),
|
|
31
|
+
fieldName: createTextField({ personal: false, reason: "technical_reference" }),
|
|
32
32
|
},
|
|
33
33
|
});
|
|
@@ -4,7 +4,13 @@ import { createI18n } from "../index";
|
|
|
4
4
|
|
|
5
5
|
describe("createI18n", () => {
|
|
6
6
|
const adminFeature = defineFeature("adminUsers", (r) => {
|
|
7
|
-
r.entity(
|
|
7
|
+
r.entity(
|
|
8
|
+
"user",
|
|
9
|
+
createEntity({
|
|
10
|
+
table: "Users",
|
|
11
|
+
fields: { email: createTextField({ personal: false, reason: "test_fixture" }) },
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
8
14
|
r.translations({
|
|
9
15
|
keys: {
|
|
10
16
|
"nav.title": { de: "Benutzer", en: "Users" },
|
|
@@ -198,7 +198,9 @@ describe("kumiko-drift end-to-end (generate → apply → gate)", () => {
|
|
|
198
198
|
test("generate from entity metas → apply → gate ok (the local-verify proof)", async () => {
|
|
199
199
|
const entity = createEntity({
|
|
200
200
|
table: "kdrift_gen",
|
|
201
|
-
fields: {
|
|
201
|
+
fields: {
|
|
202
|
+
name: createTextField({ personal: false, reason: "test_fixture", required: true }),
|
|
203
|
+
},
|
|
202
204
|
});
|
|
203
205
|
const meta = deriveEntityTableMeta("kdriftGen", entity);
|
|
204
206
|
const result = generateMigration({
|