@cosmicdrift/kumiko-framework 0.257.0 → 0.258.1

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.
Files changed (138) hide show
  1. package/package.json +3 -3
  2. package/src/__tests__/anonymous-access.integration.test.ts +3 -3
  3. package/src/__tests__/derived-fields.integration.test.ts +1 -1
  4. package/src/__tests__/entity-list-limits.integration.test.ts +1 -1
  5. package/src/__tests__/error-contract.integration.test.ts +1 -1
  6. package/src/__tests__/ownership-where-write-path.integration.test.ts +1 -1
  7. package/src/__tests__/ownership.integration.test.ts +7 -3
  8. package/src/__tests__/schema-cli-rebuild.integration.test.ts +2 -2
  9. package/src/__tests__/transition-guard.integration.test.ts +3 -3
  10. package/src/api/__tests__/api.test.ts +21 -3
  11. package/src/api/__tests__/batch.integration.test.ts +3 -3
  12. package/src/api/__tests__/body-limit.test.ts +7 -1
  13. package/src/api/__tests__/dispatcher-live.integration.test.ts +1 -1
  14. package/src/api/__tests__/http-route-rate-limit.integration.test.ts +7 -1
  15. package/src/api/__tests__/metrics-endpoint.test.ts +7 -1
  16. package/src/api/__tests__/nested-write.integration.test.ts +3 -3
  17. package/src/api/__tests__/server-boot-guards.test.ts +11 -4
  18. package/src/bun-db/__tests__/write-brand.test.ts +4 -1
  19. package/src/db/__tests__/apply-entity-event-tenant.integration.test.ts +1 -1
  20. package/src/db/__tests__/assert-exists-in.integration.test.ts +1 -1
  21. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +2 -2
  22. package/src/db/__tests__/collect-table-metas.test.ts +9 -5
  23. package/src/db/__tests__/compound-types.test.ts +1 -1
  24. package/src/db/__tests__/config-seed.integration.test.ts +3 -3
  25. package/src/db/__tests__/eagerload.integration.test.ts +7 -7
  26. package/src/db/__tests__/entity-table-from-registry.test.ts +8 -2
  27. package/src/db/__tests__/entity-table-meta-source.test.ts +7 -3
  28. package/src/db/__tests__/entity-table-types.test.ts +4 -4
  29. package/src/db/__tests__/event-store-executor-list.integration.test.ts +9 -2
  30. package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +4 -4
  31. package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +3 -3
  32. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +4 -4
  33. package/src/db/__tests__/event-store-executor.integration.test.ts +11 -6
  34. package/src/db/__tests__/event-store-system-stream.integration.test.ts +2 -2
  35. package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +6 -4
  36. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  37. package/src/db/__tests__/money.test.ts +1 -1
  38. package/src/db/__tests__/multi-row-insert.integration.test.ts +3 -3
  39. package/src/db/__tests__/parent-ref-clause.test.ts +213 -0
  40. package/src/db/__tests__/schema-migration.integration.test.ts +30 -14
  41. package/src/db/__tests__/source-shadow-create.integration.test.ts +1 -1
  42. package/src/db/__tests__/table-builder-indexes.test.ts +14 -12
  43. package/src/db/__tests__/table-builder-required.test.ts +2 -2
  44. package/src/db/__tests__/tenant-db-where-merge.integration.test.ts +1 -1
  45. package/src/db/__tests__/tenant-db-where-merge.test.ts +5 -2
  46. package/src/db/__tests__/tenant-db.integration.test.ts +2 -2
  47. package/src/db/__tests__/unique-violation-mapping.integration.test.ts +2 -2
  48. package/src/db/event-store-executor-read.ts +97 -19
  49. package/src/db/parent-ref-clause.ts +182 -0
  50. package/src/db/table-builder.ts +7 -0
  51. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +50 -8
  52. package/src/engine/__tests__/boot-validator-detail-for.test.ts +28 -4
  53. package/src/engine/__tests__/boot-validator-entity-list.test.ts +20 -4
  54. package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +9 -3
  55. package/src/engine/__tests__/boot-validator.test.ts +716 -91
  56. package/src/engine/__tests__/define-feature-entity-mapping.test.ts +2 -2
  57. package/src/engine/__tests__/engine.test.ts +128 -37
  58. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  59. package/src/engine/__tests__/entity-presave-wiring.integration.test.ts +4 -4
  60. package/src/engine/__tests__/entity-restore-tenant-isolation.integration.test.ts +1 -1
  61. package/src/engine/__tests__/entity-write-crosstenant.integration.test.ts +1 -1
  62. package/src/engine/__tests__/extend-entity-projection.test.ts +1 -1
  63. package/src/engine/__tests__/extends-registrar.test.ts +13 -3
  64. package/src/engine/__tests__/factories-long-text.test.ts +10 -6
  65. package/src/engine/__tests__/feature-crud-shorthand.test.ts +1 -1
  66. package/src/engine/__tests__/feature-crud-verb-access.integration.test.ts +1 -1
  67. package/src/engine/__tests__/multiselect-filter.integration.test.ts +1 -1
  68. package/src/engine/__tests__/nav.test.ts +1 -1
  69. package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
  70. package/src/engine/__tests__/pipeline-observability.integration.test.ts +1 -1
  71. package/src/engine/__tests__/pipeline-performance.integration.test.ts +1 -1
  72. package/src/engine/__tests__/projection-detail-actions.test.ts +6 -1
  73. package/src/engine/__tests__/projection-detail-singleton.test.ts +6 -1
  74. package/src/engine/__tests__/projection-detail-tabs.test.ts +6 -1
  75. package/src/engine/__tests__/projection-list-filter.integration.test.ts +7 -2
  76. package/src/engine/__tests__/projection.test.ts +1 -1
  77. package/src/engine/__tests__/registry.test.ts +13 -3
  78. package/src/engine/__tests__/schema-builder.test.ts +39 -11
  79. package/src/engine/__tests__/screen.test.ts +21 -15
  80. package/src/engine/__tests__/search-payload-extension.test.ts +15 -3
  81. package/src/engine/__tests__/store-table.test.ts +6 -3
  82. package/src/engine/__tests__/validate-projection-allowlist.test.ts +12 -4
  83. package/src/engine/__tests__/validation-hooks.test.ts +7 -1
  84. package/src/engine/boot-validator/__tests__/ownership.test.ts +2 -2
  85. package/src/engine/boot-validator/__tests__/parent-ref.test.ts +149 -0
  86. package/src/engine/boot-validator/action-wiring.ts +7 -7
  87. package/src/engine/boot-validator/index.ts +2 -0
  88. package/src/engine/boot-validator/parent-ref.ts +96 -0
  89. package/src/engine/boot-validator/projection-list-screens.ts +36 -23
  90. package/src/engine/boot-validator/query-output-columns.ts +2 -1
  91. package/src/engine/boot-validator/query-refs.ts +2 -1
  92. package/src/engine/boot-validator/screens.ts +322 -131
  93. package/src/engine/build-config-feature-schema.ts +2 -2
  94. package/src/engine/define-handler.ts +6 -0
  95. package/src/engine/entity-handlers.ts +6 -1
  96. package/src/engine/factories.ts +2 -0
  97. package/src/engine/feature-entity-handlers.ts +5 -0
  98. package/src/engine/index.ts +6 -0
  99. package/src/engine/ownership.ts +15 -0
  100. package/src/engine/pattern-library/mixed-schemas.ts +1 -0
  101. package/src/engine/screen-helpers.ts +25 -0
  102. package/src/engine/types/index.ts +5 -0
  103. package/src/es-ops/__tests__/runner.integration.test.ts +1 -1
  104. package/src/event-store/__tests__/upcaster.integration.test.ts +1 -1
  105. package/src/files/__tests__/file-field-pipeline.integration.test.ts +1 -1
  106. package/src/files/__tests__/files.integration.test.ts +1 -1
  107. package/src/files/file-ref-entity.ts +9 -9
  108. package/src/i18n/__tests__/i18n.test.ts +7 -1
  109. package/src/i18n/required-surface-keys.ts +48 -0
  110. package/src/migrations/__tests__/kumiko-drift.integration.test.ts +3 -1
  111. package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +2 -2
  112. package/src/pipeline/__tests__/archive-stream.integration.test.ts +1 -1
  113. package/src/pipeline/__tests__/cascade-handler.integration.test.ts +23 -8
  114. package/src/pipeline/__tests__/cascade-handler.test.ts +18 -3
  115. package/src/pipeline/__tests__/causation-chain.integration.test.ts +1 -1
  116. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +5 -3
  117. package/src/pipeline/__tests__/dispatcher.test.ts +42 -6
  118. package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +2 -2
  119. package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +1 -1
  120. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +42 -6
  121. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +2 -2
  122. package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +1 -1
  123. package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +6 -2
  124. package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +6 -2
  125. package/src/pipeline/__tests__/perf-rebuild.integration.test.ts +1 -1
  126. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  127. package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +3 -3
  128. package/src/pipeline/__tests__/query-projection.integration.test.ts +1 -1
  129. package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +2 -2
  130. package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +13 -2
  131. package/src/pipeline/__tests__/try-append-event.integration.test.ts +1 -1
  132. package/src/search/__tests__/reindex-entity.integration.test.ts +6 -1
  133. package/src/search/__tests__/search-pii-derived-index.integration.test.ts +7 -1
  134. package/src/stack/__tests__/ownership-boot-guard.integration.test.ts +1 -1
  135. package/src/testing/__tests__/e2e-generator.test.ts +15 -5
  136. package/src/testing/e2e-generator.ts +2 -0
  137. package/src/testing/shared-entities.ts +4 -2
  138. package/src/ui-types/index.ts +4 -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: { title: createTextField({ required: true }) },
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: { body: createTextField({ required: true }) },
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("credit", createEntity({ table: "Credits", fields: { name: createTextField() } }));
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: { email: createTextField({ required: true }) },
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: { email: createTextField({ required: true, format: "email" }) },
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({ required: true, format: "email" }),
170
- firstName: createTextField(),
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: { name: createTextField({ required: true }) },
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({ required: true, format: "email" }),
1065
- firstName: createTextField(),
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: { email: createTextField({ required: true, format: "email" }) },
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({ table: "invoices", fields: { name: createTextField() } }),
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({ table: "invoices", fields: { name: createTextField() } }),
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({ table: "things", fields: { title: createTextField({ searchable: true }) } }),
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: { title: createTextField({ searchable: true }) },
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({ table: "things", fields: { title: createTextField({ searchable: true }) } }),
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({ table: "shop_widgets", fields: { name: createTextField() } });
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: { label: createTextField({ required: true }) },
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: { label: createTextField({ required: true }) },
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: { label: createTextField({ required: true }) },
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: { label: createTextField({ required: true }) },
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("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
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
  }),
@@ -0,0 +1,149 @@
1
+ // fw#2766 — boot guard for join-row entities. The read gate needs the registry,
2
+ // which only exists at request time, so the raw executor stays ungated when a
3
+ // caller doesn't wire `parentVisibility`. That keeps framework-internal cascade
4
+ // and GDPR paths working, and moves enforcement here: a hand-written list/detail
5
+ // handler on a parentRef entity must fail boot rather than leak silently.
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { z } from "zod";
9
+ import { defineFeature } from "../../define-feature";
10
+ import { defineEntityDetailHandler, defineEntityListHandler } from "../../entity-handlers";
11
+ import { createEntity, createTextField } from "../../factories";
12
+ import type { FeatureDefinition } from "../../types";
13
+ import { validateParentRefs } from "../parent-ref";
14
+
15
+ const textField = () =>
16
+ createTextField({
17
+ required: true,
18
+ maxLength: 64,
19
+ personal: false,
20
+ reason: "technical_reference",
21
+ });
22
+
23
+ const hostEntity = createEntity({
24
+ table: "read_hosts",
25
+ fields: { name: textField() },
26
+ });
27
+
28
+ function joinEntity(parentRef: {
29
+ entityTypeField: string;
30
+ entityIdField: string;
31
+ allowedTypes?: readonly string[];
32
+ }) {
33
+ return createEntity({
34
+ table: "read_joins",
35
+ fields: { entityType: textField(), entityId: textField() },
36
+ parentRef,
37
+ });
38
+ }
39
+
40
+ const validRef = { entityTypeField: "entityType", entityIdField: "entityId" } as const;
41
+
42
+ function featureMapOf(...features: readonly FeatureDefinition[]) {
43
+ return new Map(features.map((f) => [f.name, f]));
44
+ }
45
+
46
+ function validate(feature: FeatureDefinition, extra: readonly FeatureDefinition[] = []): void {
47
+ validateParentRefs(feature, featureMapOf(feature, ...extra));
48
+ }
49
+
50
+ describe("validateParentRefs", () => {
51
+ test("accepts a parentRef whose fields exist and whose list/detail are convention handlers", () => {
52
+ const entity = joinEntity(validRef);
53
+ const feature = defineFeature("joins", (r) => {
54
+ r.entity("join", entity);
55
+ r.queryHandler(defineEntityListHandler("join", entity, { access: { openToAll: true } }));
56
+ r.queryHandler(defineEntityDetailHandler("join", entity, { access: { openToAll: true } }));
57
+ });
58
+ expect(() => validate(feature)).not.toThrow();
59
+ });
60
+
61
+ test("an entity with no parentRef is not checked at all", () => {
62
+ const feature = defineFeature("plain", (r) => {
63
+ r.entity("host", hostEntity);
64
+ r.queryHandler({
65
+ name: "host:list",
66
+ schema: z.object({}),
67
+ handler: async () => ({ rows: [], nextCursor: null }),
68
+ access: { openToAll: true },
69
+ });
70
+ });
71
+ expect(() => validate(feature)).not.toThrow();
72
+ });
73
+
74
+ test("rejects an entityTypeField that is not a declared field", () => {
75
+ const feature = defineFeature("joins", (r) => {
76
+ r.entity("join", joinEntity({ ...validRef, entityTypeField: "nope" }));
77
+ });
78
+ expect(() => validate(feature)).toThrow(/entityTypeField references "nope"/);
79
+ });
80
+
81
+ test("rejects an entityIdField that is not a declared field", () => {
82
+ const feature = defineFeature("joins", (r) => {
83
+ r.entity("join", joinEntity({ ...validRef, entityIdField: "nope" }));
84
+ });
85
+ expect(() => validate(feature)).toThrow(/entityIdField references "nope"/);
86
+ });
87
+
88
+ test("rejects an allowedTypes entry that names no registered entity", () => {
89
+ const feature = defineFeature("joins", (r) => {
90
+ r.entity("join", joinEntity({ ...validRef, allowedTypes: ["ghost"] }));
91
+ });
92
+ expect(() => validate(feature)).toThrow(/names "ghost"/);
93
+ });
94
+
95
+ test("accepts an allowedTypes entry registered by another feature", () => {
96
+ const hosts = defineFeature("hosts", (r) => {
97
+ r.entity("host", hostEntity);
98
+ });
99
+ const entity = joinEntity({ ...validRef, allowedTypes: ["host"] });
100
+ const feature = defineFeature("joins", (r) => {
101
+ r.entity("join", entity);
102
+ r.queryHandler(defineEntityListHandler("join", entity, { access: { openToAll: true } }));
103
+ });
104
+ expect(() => validate(feature, [hosts])).not.toThrow();
105
+ });
106
+
107
+ test("rejects an allowedTypes host that is itself a join row — no recursive gating", () => {
108
+ const others = defineFeature("others", (r) => {
109
+ r.entity("other", joinEntity(validRef));
110
+ });
111
+ const feature = defineFeature("joins", (r) => {
112
+ r.entity("join", joinEntity({ ...validRef, allowedTypes: ["other"] }));
113
+ });
114
+ expect(() => validate(feature, [others])).toThrow(/itself declares a parentRef/);
115
+ });
116
+
117
+ test("rejects a hand-written list handler, which would bypass the read gate", () => {
118
+ const feature = defineFeature("joins", (r) => {
119
+ r.entity("join", joinEntity(validRef));
120
+ r.queryHandler({
121
+ name: "join:list",
122
+ schema: z.object({}),
123
+ handler: async () => ({ rows: [], nextCursor: null }),
124
+ access: { openToAll: true },
125
+ });
126
+ });
127
+ expect(() => validate(feature)).toThrow(/join:list/);
128
+ });
129
+
130
+ test("rejects a hand-written detail handler too", () => {
131
+ const feature = defineFeature("joins", (r) => {
132
+ r.entity("join", joinEntity(validRef));
133
+ r.queryHandler({
134
+ name: "join:detail",
135
+ schema: z.object({ id: z.uuid() }),
136
+ handler: async () => null,
137
+ access: { openToAll: true },
138
+ });
139
+ });
140
+ expect(() => validate(feature)).toThrow(/join:detail/);
141
+ });
142
+
143
+ test("registering no list/detail handler at all is allowed", () => {
144
+ const feature = defineFeature("joins", (r) => {
145
+ r.entity("join", joinEntity(validRef));
146
+ });
147
+ expect(() => validate(feature)).not.toThrow();
148
+ });
149
+ });
@@ -1,4 +1,9 @@
1
- import { isFieldsEditSection, normalizeEditField, normalizeListColumn } from "../screen-helpers";
1
+ import {
2
+ isEditLayoutScreen,
3
+ isFieldsEditSection,
4
+ normalizeEditField,
5
+ normalizeListColumn,
6
+ } from "../screen-helpers";
2
7
  import type {
3
8
  EditFieldSpec,
4
9
  EditLayout,
@@ -138,12 +143,7 @@ export function validateFieldWiring(feature: FeatureDefinition): void {
138
143
  validateColumnsNoFunctions(feature.name, screen.id, screen.type, screen.columns);
139
144
  continue;
140
145
  }
141
- if (
142
- screen.type === "entityEdit" ||
143
- screen.type === "actionForm" ||
144
- screen.type === "configEdit" ||
145
- screen.type === "projectionDetail"
146
- ) {
146
+ if (isEditLayoutScreen(screen)) {
147
147
  validateEditLayoutNoFunctions(feature.name, screen.id, screen.type, screen.layout);
148
148
  continue;
149
149
  }
@@ -45,6 +45,7 @@ import {
45
45
  warnOnNavAccessInversion,
46
46
  } from "./nav";
47
47
  import { collectClaimKeys, validateOwnershipRules } from "./ownership";
48
+ import { validateParentRefs } from "./parent-ref";
48
49
  import { validatePiiAndRetention } from "./pii-retention";
49
50
  import {
50
51
  validateProjectionListScreens,
@@ -195,6 +196,7 @@ export function validateBoot(
195
196
  validateConfigKeyBacking(feature);
196
197
  validateConfigKeyPiiEncrypted(feature);
197
198
  validateOwnershipRules(feature, allClaimKeys, knownRoles);
199
+ validateParentRefs(feature, featureMap);
198
200
  validateMultiStreamProjections(feature);
199
201
  // Vor validateScreens: dessen visible/entityId-Feldref-Checks werfen für
200
202
  // einen Function-Wert bereits (mit verwirrender "unknown field undefined"-