@cosmicdrift/kumiko-bundled-features 0.40.1 → 0.41.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.40.1",
3
+ "version": "0.41.1",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -76,11 +76,11 @@
76
76
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
77
77
  },
78
78
  "dependencies": {
79
- "@cosmicdrift/kumiko-dispatcher-live": "0.38.0",
80
- "@cosmicdrift/kumiko-framework": "0.38.0",
81
- "@cosmicdrift/kumiko-headless": "0.38.0",
82
- "@cosmicdrift/kumiko-renderer": "0.38.0",
83
- "@cosmicdrift/kumiko-renderer-web": "0.38.0",
79
+ "@cosmicdrift/kumiko-dispatcher-live": "0.40.1",
80
+ "@cosmicdrift/kumiko-framework": "0.40.1",
81
+ "@cosmicdrift/kumiko-headless": "0.40.1",
82
+ "@cosmicdrift/kumiko-renderer": "0.40.1",
83
+ "@cosmicdrift/kumiko-renderer-web": "0.40.1",
84
84
  "@mollie/api-client": "^4.5.0",
85
85
  "@node-rs/argon2": "^2.0.2",
86
86
  "@types/nodemailer": "^8.0.0",
@@ -123,3 +123,47 @@ describe("fieldDefinitionAggregateId determinism", () => {
123
123
  expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
124
124
  });
125
125
  });
126
+
127
+ // Role-Naming-Drift (Wave J): die CustomFieldsFormSection dispatcht die
128
+ // Bundle-QNs hart — Apps mit eigenem Rollen-Vokabular (publicstatus:
129
+ // "Admin"/"Editor") müssen die Access-Rollen der Value-Writes + des
130
+ // fieldDefinition-List-Queries überschreiben können, sonst ist jeder
131
+ // Save/Load für App-User access_denied.
132
+ describe("createCustomFieldsFeature access-options", () => {
133
+ function writeAccess(
134
+ feature: ReturnType<typeof createCustomFieldsFeature>,
135
+ nameMatch: string,
136
+ ): readonly string[] {
137
+ const entry = Object.entries(feature.writeHandlers).find(([qn]) => qn.includes(nameMatch));
138
+ if (!entry) throw new Error(`handler ${nameMatch} not registered`);
139
+ const access = entry[1].access;
140
+ if (!access || !("roles" in access)) throw new Error(`handler ${nameMatch} has no roles`);
141
+ return access.roles;
142
+ }
143
+
144
+ test("ohne Optionen: Singleton mit Default-Rollen", () => {
145
+ const feature = createCustomFieldsFeature();
146
+ expect(feature).toBe(createCustomFieldsFeature());
147
+ expect(writeAccess(feature, "set-custom-field")).toEqual(["TenantAdmin", "TenantMember"]);
148
+ expect(writeAccess(feature, "clear-custom-field")).toEqual(["TenantAdmin", "TenantMember"]);
149
+ });
150
+
151
+ test("valueWriteRoles überschreibt set- UND clear-custom-field", () => {
152
+ const feature = createCustomFieldsFeature({ valueWriteRoles: ["Admin", "Editor"] });
153
+ expect(writeAccess(feature, "set-custom-field")).toEqual(["Admin", "Editor"]);
154
+ expect(writeAccess(feature, "clear-custom-field")).toEqual(["Admin", "Editor"]);
155
+ // Definition-CRUD bleibt unberührt — dafür existieren App-Wrapper.
156
+ expect(writeAccess(feature, "define-tenant-field")).toEqual(["TenantAdmin"]);
157
+ });
158
+
159
+ test("fieldDefinitionListRoles überschreibt den List-Query (FormSection-Lade-Pfad)", () => {
160
+ const feature = createCustomFieldsFeature({ fieldDefinitionListRoles: ["Admin", "Editor"] });
161
+ const entry = Object.entries(feature.queryHandlers).find(([qn]) =>
162
+ qn.includes("field-definition:list"),
163
+ );
164
+ if (!entry) throw new Error("field-definition:list not registered");
165
+ const access = entry[1].access;
166
+ if (!access || !("roles" in access)) throw new Error("list-query has no roles");
167
+ expect(access.roles).toEqual(["Admin", "Editor"]);
168
+ });
169
+ });
@@ -50,6 +50,15 @@ export const CUSTOM_FIELD_CLEARED_EVENT = "custom-field-cleared";
50
50
  // dass eine host-entity Custom-Fields haben darf.
51
51
  export const CUSTOM_FIELDS_EXTENSION = "customFields";
52
52
 
53
+ // Default-RBAC der Value-Write- und List-Pfade. Apps mit eigenem Rollen-
54
+ // Vokabular (publicstatus: "Admin"/"Editor" statt "TenantAdmin"/
55
+ // "TenantMember") überschreiben via createCustomFieldsFeature({
56
+ // valueWriteRoles, fieldDefinitionListRoles }) — sonst sind die hart
57
+ // verdrahteten Bundle-QNs, die die CustomFieldsFormSection dispatcht,
58
+ // für jeden App-User access_denied.
59
+ export const DEFAULT_VALUE_WRITE_ROLES = ["TenantAdmin", "TenantMember"] as const;
60
+ export const DEFAULT_FIELD_DEFINITION_LIST_ROLES = ["TenantAdmin"] as const;
61
+
53
62
  // Field-type union — identisch zu Stammfeld-Field-Type-System (Spec Z.59-73:
54
63
  // `Identisch zu Entity-Feld-Typen`). Builder-Reuse-Promise: was `r.field.X()`
55
64
  // kann, kann eine Custom-Field-Definition auch.
@@ -50,11 +50,15 @@ import {
50
50
  CUSTOM_FIELD_SET_EVENT,
51
51
  CUSTOM_FIELDS_EXTENSION,
52
52
  CUSTOM_FIELDS_FEATURE_NAME,
53
+ DEFAULT_FIELD_DEFINITION_LIST_ROLES,
53
54
  FIELD_DEFINITION_DELETED_EVENT,
54
55
  } from "./constants";
55
56
  import { fieldDefinitionEntity } from "./entity";
56
57
  import { customFieldClearedSchema, customFieldSetSchema } from "./events";
57
- import { clearCustomFieldHandler } from "./handlers/clear-custom-field.write";
58
+ import {
59
+ clearCustomFieldHandler,
60
+ createClearCustomFieldHandler,
61
+ } from "./handlers/clear-custom-field.write";
58
62
  import { defineSystemFieldHandler } from "./handlers/define-system-field.write";
59
63
  import {
60
64
  createDefineTenantFieldHandler,
@@ -62,11 +66,12 @@ import {
62
66
  } from "./handlers/define-tenant-field.write";
63
67
  import { deleteSystemFieldHandler } from "./handlers/delete-system-field.write";
64
68
  import { deleteTenantFieldHandler } from "./handlers/delete-tenant-field.write";
65
- import { setCustomFieldHandler } from "./handlers/set-custom-field.write";
69
+ import {
70
+ createSetCustomFieldHandler,
71
+ setCustomFieldHandler,
72
+ } from "./handlers/set-custom-field.write";
66
73
  import { updateTenantFieldHandler } from "./handlers/update-tenant-field.write";
67
74
 
68
- const tenantAdminAccess = { access: { roles: ["TenantAdmin"] } } as const;
69
-
70
75
  const fieldDefinitionDeletedSchema = z.object({
71
76
  entityName: z.string(),
72
77
  fieldKey: z.string(),
@@ -78,13 +83,21 @@ const fieldDefinitionDeletedSchema = z.object({
78
83
  tenantId: z.string().optional(),
79
84
  });
80
85
 
81
- // Shared registration body for both the singleton and the quota-variant.
82
- // Only the define-tenant-field handler differs (quota baked in or not); every
83
- // other entity/event/handler is registered identically here so a new
84
- // event/handler can never silently miss the quota variant.
86
+ // Handler-/Access-Varianten die zwischen Singleton und Options-Variante
87
+ // differieren können. Alles andere registriert registerCustomFields für
88
+ // beide identisch, damit ein neues Event/Handler nie still die
89
+ // Options-Variante verfehlt.
90
+ type RegisterVariant = {
91
+ readonly defineTenantHandler: WriteHandlerDef;
92
+ readonly setHandler: WriteHandlerDef;
93
+ readonly clearHandler: WriteHandlerDef;
94
+ readonly fieldDefinitionListRoles: readonly string[];
95
+ };
96
+
97
+ // Shared registration body for both the singleton and the options-variant.
85
98
  function registerCustomFields(
86
99
  r: FeatureRegistrar<typeof CUSTOM_FIELDS_FEATURE_NAME>,
87
- defineTenantHandler: WriteHandlerDef,
100
+ variant: RegisterVariant,
88
101
  ) {
89
102
  r.describe(
90
103
  "Tenant- and system-scoped custom field definitions with generic value storage on any host entity. Registers the `field-definition` entity (event-sourced CRUD via `define-tenant-field`, `define-system-field`, `update-tenant-field`, `delete-tenant-field`, `delete-system-field`) and two value write-handlers (`set-custom-field`, `clear-custom-field`) that emit `custom-fields:event:custom-field-set` / `custom-fields:event:custom-field-cleared` events on the host aggregate's stream. To attach custom fields to your own entity, call `wireCustomFieldsFor(r, entityName, entityTable)` in the host feature — this wires the JSONB projection, `postQuery` flattening hook, and search-payload extension. The host entity must declare a `customFieldsField()` JSONB column.",
@@ -113,19 +126,23 @@ function registerCustomFields(
113
126
  r.extendsRegistrar(CUSTOM_FIELDS_EXTENSION, {});
114
127
 
115
128
  // Definition-CRUD handlers (B1; update kam mit Bug-Bash D2 2026-06-08).
116
- r.writeHandler(defineTenantHandler);
129
+ r.writeHandler(variant.defineTenantHandler);
117
130
  r.writeHandler(defineSystemFieldHandler);
118
131
  r.writeHandler(updateTenantFieldHandler);
119
132
  r.writeHandler(deleteTenantFieldHandler);
120
133
  r.writeHandler(deleteSystemFieldHandler);
121
134
 
122
135
  // Value-write handlers (B2). Emittieren events auf host-aggregate-stream.
123
- r.writeHandler(setCustomFieldHandler);
124
- r.writeHandler(clearCustomFieldHandler);
136
+ r.writeHandler(variant.setHandler);
137
+ r.writeHandler(variant.clearHandler);
125
138
 
126
- // List-Query — tenant-scoped (B1-limit).
139
+ // List-Query — tenant-scoped (B1-limit). Die CustomFieldsFormSection
140
+ // dispatcht diesen QN hart — Apps mit eigenem Rollen-Vokabular
141
+ // überschreiben die Rollen via fieldDefinitionListRoles.
127
142
  r.queryHandler(
128
- defineEntityListHandler("field-definition", fieldDefinitionEntity, tenantAdminAccess),
143
+ defineEntityListHandler("field-definition", fieldDefinitionEntity, {
144
+ access: { roles: variant.fieldDefinitionListRoles },
145
+ }),
129
146
  );
130
147
 
131
148
  return { setEvent, clearedEvent, fieldDefinitionDeletedEvent };
@@ -137,29 +154,54 @@ function registerCustomFields(
137
154
  // (feature.ts -> handlers/*.write.ts -> feature.ts) löst sich auf weil
138
155
  // kein top-level-access stattfindet.
139
156
  export const customFieldsFeature = defineFeature(CUSTOM_FIELDS_FEATURE_NAME, (r) =>
140
- registerCustomFields(r, defineTenantFieldHandler),
157
+ registerCustomFields(r, {
158
+ defineTenantHandler: defineTenantFieldHandler,
159
+ setHandler: setCustomFieldHandler,
160
+ clearHandler: clearCustomFieldHandler,
161
+ fieldDefinitionListRoles: DEFAULT_FIELD_DEFINITION_LIST_ROLES,
162
+ }),
141
163
  );
142
164
 
165
+ export type CustomFieldsFeatureOptions = {
166
+ /** T1.5e: Quota für define-tenant-field. */
167
+ readonly fieldDefinitionLimitPerTenant?: number;
168
+ /** Rollen für set-/clear-custom-field — die Save-Pfade der
169
+ * CustomFieldsFormSection. Default ["TenantAdmin","TenantMember"];
170
+ * Apps mit eigenem Rollen-Vokabular (z.B. ["Admin","Editor"]) MÜSSEN
171
+ * das setzen, sonst ist der Value-Save für jeden App-User
172
+ * access_denied (Role-Naming-Drift). */
173
+ readonly valueWriteRoles?: readonly string[];
174
+ /** Rollen für custom-fields:query:field-definition:list — der
175
+ * Lade-Pfad der CustomFieldsFormSection. Default ["TenantAdmin"]. */
176
+ readonly fieldDefinitionListRoles?: readonly string[];
177
+ };
178
+
143
179
  // Backwards-compat-wrapper. Bestehende Caller (z.B. integration-tests,
144
180
  // host-apps) nutzen weiterhin `createCustomFieldsFeature()`. Returnt den
145
181
  // module-level-Singleton — kein neuer build pro Aufruf, was für consumer
146
- // nicht erkennbar ist (read-only inspection).
147
- //
148
- // **T1.5e options**: when `fieldDefinitionLimitPerTenant` is set, the
149
- // returned feature carries a fresh `define-tenant-field` handler with the
150
- // quota baked in. Callers that don't pass options get the singleton — no
151
- // change in behavior.
182
+ // nicht erkennbar ist (read-only inspection). Jede gesetzte Option baut
183
+ // eine frische Feature-Definition mit den Varianten-Handlern.
152
184
  export function createCustomFieldsFeature(
153
- opts: { readonly fieldDefinitionLimitPerTenant?: number } = {},
185
+ opts: CustomFieldsFeatureOptions = {},
154
186
  ): typeof customFieldsFeature {
155
- if (opts.fieldDefinitionLimitPerTenant === undefined) {
187
+ const hasOptions =
188
+ opts.fieldDefinitionLimitPerTenant !== undefined ||
189
+ opts.valueWriteRoles !== undefined ||
190
+ opts.fieldDefinitionListRoles !== undefined;
191
+ if (!hasOptions) {
156
192
  return customFieldsFeature;
157
193
  }
158
194
  const limit = opts.fieldDefinitionLimitPerTenant;
159
195
  return defineFeature(CUSTOM_FIELDS_FEATURE_NAME, (r) =>
160
- registerCustomFields(
161
- r,
162
- createDefineTenantFieldHandler({ fieldDefinitionLimitPerTenant: limit }),
163
- ),
196
+ registerCustomFields(r, {
197
+ defineTenantHandler:
198
+ limit !== undefined
199
+ ? createDefineTenantFieldHandler({ fieldDefinitionLimitPerTenant: limit })
200
+ : defineTenantFieldHandler,
201
+ setHandler: createSetCustomFieldHandler(opts.valueWriteRoles),
202
+ clearHandler: createClearCustomFieldHandler(opts.valueWriteRoles),
203
+ fieldDefinitionListRoles:
204
+ opts.fieldDefinitionListRoles ?? DEFAULT_FIELD_DEFINITION_LIST_ROLES,
205
+ }),
164
206
  );
165
207
  }
@@ -1,6 +1,7 @@
1
1
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import { z } from "zod";
4
+ import { DEFAULT_VALUE_WRITE_ROLES } from "../constants";
4
5
  import { customFieldsFeature } from "../feature";
5
6
  import { checkFieldAccessForWrite } from "../lib/field-access";
6
7
 
@@ -25,7 +26,7 @@ export type ClearCustomFieldPayload = z.infer<typeof clearCustomFieldPayloadSche
25
26
  export const clearCustomFieldHandler: WriteHandlerDef = {
26
27
  name: "clear-custom-field",
27
28
  schema: clearCustomFieldPayloadSchema,
28
- access: { roles: ["TenantAdmin", "TenantMember"] },
29
+ access: { roles: DEFAULT_VALUE_WRITE_ROLES },
29
30
  handler: async (event, ctx) => {
30
31
  const payload = event.payload as ClearCustomFieldPayload; // @cast-boundary engine-payload
31
32
 
@@ -62,3 +63,11 @@ export const clearCustomFieldHandler: WriteHandlerDef = {
62
63
  };
63
64
  },
64
65
  };
66
+
67
+ /** Pendant zu createSetCustomFieldHandler — gleiche Rollen für set + clear,
68
+ * ein Wert ohne Clear-Recht wäre nur halb beherrschbar. */
69
+ export function createClearCustomFieldHandler(
70
+ roles: readonly string[] = DEFAULT_VALUE_WRITE_ROLES,
71
+ ): WriteHandlerDef {
72
+ return { ...clearCustomFieldHandler, access: { roles } };
73
+ }
@@ -1,6 +1,7 @@
1
1
  import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import { z } from "zod";
4
+ import { DEFAULT_VALUE_WRITE_ROLES } from "../constants";
4
5
  import { customFieldsFeature } from "../feature";
5
6
  import { fieldWriteAccessDeniedRoles, loadFieldDefinition } from "../lib/field-access";
6
7
  import { buildCustomFieldValueSchema } from "../lib/value-schema";
@@ -48,7 +49,7 @@ export type SetCustomFieldPayload = z.infer<typeof setCustomFieldPayloadSchema>;
48
49
  export const setCustomFieldHandler: WriteHandlerDef = {
49
50
  name: "set-custom-field",
50
51
  schema: setCustomFieldPayloadSchema,
51
- access: { roles: ["TenantAdmin", "TenantMember"] },
52
+ access: { roles: DEFAULT_VALUE_WRITE_ROLES },
52
53
  handler: async (event, ctx) => {
53
54
  const payload = event.payload as SetCustomFieldPayload; // @cast-boundary engine-payload
54
55
 
@@ -102,3 +103,12 @@ export const setCustomFieldHandler: WriteHandlerDef = {
102
103
  };
103
104
  },
104
105
  };
106
+
107
+ /** Value-Write mit App-Rollen statt der Bundle-Defaults — Apps mit eigenem
108
+ * Rollen-Vokabular (z.B. "Admin"/"Editor") reichen ihre Rollen über
109
+ * createCustomFieldsFeature({ valueWriteRoles }) hierher durch. */
110
+ export function createSetCustomFieldHandler(
111
+ roles: readonly string[] = DEFAULT_VALUE_WRITE_ROLES,
112
+ ): WriteHandlerDef {
113
+ return { ...setCustomFieldHandler, access: { roles } };
114
+ }
@@ -14,7 +14,11 @@ export {
14
14
  customFieldClearedSchema,
15
15
  customFieldSetSchema,
16
16
  } from "./events";
17
- export { createCustomFieldsFeature, customFieldsFeature } from "./feature";
17
+ export {
18
+ type CustomFieldsFeatureOptions,
19
+ createCustomFieldsFeature,
20
+ customFieldsFeature,
21
+ } from "./feature";
18
22
  export {
19
23
  type ClearCustomFieldPayload,
20
24
  clearCustomFieldPayloadSchema,