@cosmicdrift/kumiko-bundled-features 0.358.0 → 0.359.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.
@@ -4,13 +4,14 @@ import { fieldDefinitionAggregateId } from "../aggregate-id.js";
4
4
  import { DEFAULT_FIELD_DEFINITION_WRITE_ROLES } from "../constants.js";
5
5
  import { fieldDefinitionExecutor } from "../executor.js";
6
6
  import { buildFieldDefinitionColumns } from "../lib/field-definition-row.js";
7
- import { updateFieldPayloadSchema } from "../schemas.js";
7
+ import { fieldTypeSchema, updateFieldPayloadSchema } from "../schemas.js";
8
8
  export function createUpdateTenantFieldHandler(opts = {}) {
9
9
  return {
10
10
  name: "update-tenant-field",
11
11
  schema: updateFieldPayloadSchema,
12
12
  access: { roles: opts.roles ?? DEFAULT_FIELD_DEFINITION_WRITE_ROLES },
13
- description: "Replaces a tenant-owned custom-field definition with a complete new spec while refusing any change to the field's type; use it to relabel or re-constrain an existing field rather than to create or remove one.",
13
+ description: "Replaces a tenant-owned custom-field definition with a complete new spec; serializedField.type may be omitted, in which case the stored type applies, and a different type is refused. Use it to relabel or re-constrain an existing field rather than to create or remove one.",
14
+ excludedFields: ["type"],
14
15
  handler: async (event, ctx) => {
15
16
  const payload = event.payload; // @cast-boundary engine-payload
16
17
  const tenantId = event.user.tenantId;
@@ -22,16 +23,24 @@ export function createUpdateTenantFieldHandler(opts = {}) {
22
23
  if (!existing) {
23
24
  return failNotFound("field-definition", aggregateId);
24
25
  }
25
- if (existing["type"] !== payload.serializedField.type) {
26
+ const storedType = fieldTypeSchema.safeParse(existing["type"]);
27
+ if (!storedType.success) {
28
+ throw new Error(`update-tenant-field: stored definition ${aggregateId} has an invalid type`);
29
+ }
30
+ const requestedType = payload.serializedField.type;
31
+ if (requestedType !== undefined && requestedType !== storedType.data) {
26
32
  return failUnprocessable("field_type_immutable", {
27
33
  entityName: payload.entityName,
28
34
  fieldKey: payload.fieldKey,
29
- currentType: existing["type"],
30
- requestedType: payload.serializedField.type,
35
+ currentType: storedType.data,
36
+ requestedType,
31
37
  });
32
38
  }
33
39
  // entityName/fieldKey sind die Identität — nicht Teil der changes.
34
- const { entityName: _entityName, fieldKey: _fieldKey, ...changes } = buildFieldDefinitionColumns(payload);
40
+ const { entityName: _entityName, fieldKey: _fieldKey, ...changes } = buildFieldDefinitionColumns({
41
+ ...payload,
42
+ serializedField: { ...payload.serializedField, type: storedType.data },
43
+ });
35
44
  return fieldDefinitionExecutor.update({ id: aggregateId, changes }, event.user, ctx.db, {
36
45
  skipOptimisticLock: true,
37
46
  });
@@ -1,4 +1,13 @@
1
1
  import * as z from "zod";
2
+ export declare const fieldTypeSchema: z.ZodEnum<{
3
+ boolean: "boolean";
4
+ date: "date";
5
+ embedded: "embedded";
6
+ enum: "enum";
7
+ money: "money";
8
+ number: "number";
9
+ text: "text";
10
+ }>;
2
11
  export declare const customFieldAccessSchema: z.ZodOptional<z.ZodObject<{
3
12
  read: z.ZodOptional<z.ZodArray<z.ZodString>>;
4
13
  write: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -8,15 +17,6 @@ export declare const defineFieldPayloadSchema: z.ZodObject<{
8
17
  entityName: z.ZodString;
9
18
  fieldKey: z.ZodString;
10
19
  serializedField: z.ZodObject<{
11
- type: z.ZodEnum<{
12
- boolean: "boolean";
13
- date: "date";
14
- embedded: "embedded";
15
- enum: "enum";
16
- money: "money";
17
- number: "number";
18
- text: "text";
19
- }>;
20
20
  fieldAccess: z.ZodOptional<z.ZodObject<{
21
21
  read: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
22
  write: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -28,6 +28,15 @@ export declare const defineFieldPayloadSchema: z.ZodObject<{
28
28
  delete: "delete";
29
29
  }>;
30
30
  }, z.core.$strip>>;
31
+ type: z.ZodEnum<{
32
+ boolean: "boolean";
33
+ date: "date";
34
+ embedded: "embedded";
35
+ enum: "enum";
36
+ money: "money";
37
+ number: "number";
38
+ text: "text";
39
+ }>;
31
40
  }, z.core.$loose>;
32
41
  required: z.ZodDefault<z.ZodBoolean>;
33
42
  searchable: z.ZodDefault<z.ZodBoolean>;
@@ -38,16 +47,11 @@ export type DefineFieldPayload = z.infer<typeof defineFieldPayloadSchema>;
38
47
  export declare const updateFieldPayloadSchema: z.ZodObject<{
39
48
  entityName: z.ZodString;
40
49
  fieldKey: z.ZodString;
50
+ required: z.ZodDefault<z.ZodBoolean>;
51
+ searchable: z.ZodDefault<z.ZodBoolean>;
52
+ displayOrder: z.ZodDefault<z.ZodNumber>;
53
+ label: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
41
54
  serializedField: z.ZodObject<{
42
- type: z.ZodEnum<{
43
- boolean: "boolean";
44
- date: "date";
45
- embedded: "embedded";
46
- enum: "enum";
47
- money: "money";
48
- number: "number";
49
- text: "text";
50
- }>;
51
55
  fieldAccess: z.ZodOptional<z.ZodObject<{
52
56
  read: z.ZodOptional<z.ZodArray<z.ZodString>>;
53
57
  write: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -59,13 +63,18 @@ export declare const updateFieldPayloadSchema: z.ZodObject<{
59
63
  delete: "delete";
60
64
  }>;
61
65
  }, z.core.$strip>>;
66
+ type: z.ZodOptional<z.ZodEnum<{
67
+ boolean: "boolean";
68
+ date: "date";
69
+ embedded: "embedded";
70
+ enum: "enum";
71
+ money: "money";
72
+ number: "number";
73
+ text: "text";
74
+ }>>;
62
75
  }, z.core.$loose>;
63
- required: z.ZodDefault<z.ZodBoolean>;
64
- searchable: z.ZodDefault<z.ZodBoolean>;
65
- displayOrder: z.ZodDefault<z.ZodNumber>;
66
- label: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
67
76
  }, z.core.$strip>;
68
- export type UpdateFieldPayload = DefineFieldPayload;
77
+ export type UpdateFieldPayload = z.infer<typeof updateFieldPayloadSchema>;
69
78
  export declare const deleteFieldPayloadSchema: z.ZodObject<{
70
79
  entityName: z.ZodString;
71
80
  fieldKey: z.ZodString;
@@ -1,7 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { SUPPORTED_FIELD_TYPES } from "./constants.js";
3
3
  // Field-Type-Validator — pinnt valid type-Werte für fieldDefinition.
4
- const fieldTypeSchema = z.enum(SUPPORTED_FIELD_TYPES);
4
+ export const fieldTypeSchema = z.enum(SUPPORTED_FIELD_TYPES);
5
5
  // Per-field access-control. When set, `set/clear-custom-field` handlers
6
6
  // require the calling user to hold at least one of the listed roles —
7
7
  // in addition to the handler-level RBAC. Absent or empty `write` means
@@ -33,9 +33,7 @@ export const customFieldAccessSchema = z
33
33
  // Everything else stays loose pending B2's per-type discriminated-union —
34
34
  // EXCEPT `sensitive`, which is explicitly rejected below: custom fields
35
35
  // carry supplemental business data only, PII support was removed (#972).
36
- const serializedFieldSchema = z
37
- .looseObject({
38
- type: fieldTypeSchema,
36
+ const serializedFieldBaseShape = {
39
37
  fieldAccess: customFieldAccessSchema,
40
38
  // `retention` (T1.5d): per-field expiry. The retention-cron strips
41
39
  // values whose host-row `modified_at` is older than `keepFor`. Strategy
@@ -50,9 +48,19 @@ const serializedFieldSchema = z
50
48
  strategy: z.enum(["delete", "anonymize"]),
51
49
  })
52
50
  .optional(),
53
- })
51
+ };
52
+ const rejectSensitive = [
53
+ (v) => !("sensitive" in v),
54
+ "custom fields don't support PII — `sensitive` was removed (#972). Model personal data as a schema entity field with a pii/userOwned/tenantOwned annotation instead.",
55
+ ];
56
+ const serializedFieldSchema = z
57
+ .looseObject({ type: fieldTypeSchema, ...serializedFieldBaseShape })
54
58
  .refine((v) => typeof v["type"] === "string", "serializedField must have a string `type`")
55
- .refine((v) => !("sensitive" in v), "custom fields don't support PII — `sensitive` was removed (#972). Model personal data as a schema entity field with a pii/userOwned/tenantOwned annotation instead.");
59
+ .refine(...rejectSensitive);
60
+ // Update may omit `type`: it is immutable, so the handler takes it from the stored definition.
61
+ const updateSerializedFieldSchema = z
62
+ .looseObject({ type: fieldTypeSchema.optional(), ...serializedFieldBaseShape })
63
+ .refine(...rejectSensitive);
56
64
  // i18n-labels — `{ de: "...", en: "...", ... }`. Mindestens ein Eintrag.
57
65
  const labelSchema = z.record(z.string().min(2).max(8), z.string().min(1));
58
66
  // Payload für `define-tenant-field` und `define-system-field` write-handler.
@@ -69,11 +77,13 @@ export const defineFieldPayloadSchema = z.object({
69
77
  displayOrder: z.number().int().min(0).default(0),
70
78
  label: labelSchema.optional(),
71
79
  });
72
- // Payload für `update-tenant-field` — bewusst dieselbe Shape wie define
73
- // (Vollersatz-Semantik: der Edit-Screen schickt den kompletten neuen Stand;
74
- // entityName+fieldKey sind die Identität, type ist immutable und wird im
75
- // Handler gegen den Bestand geprüft).
76
- export const updateFieldPayloadSchema = defineFieldPayloadSchema;
80
+ // Payload for `update-tenant-field`: full replacement like define (the edit
81
+ // screen sends the complete new state; entityName+fieldKey are the identity),
82
+ // except that `serializedField.type` may be omitted. type is immutable; a
83
+ // differing value is refused in the handler.
84
+ export const updateFieldPayloadSchema = defineFieldPayloadSchema.extend({
85
+ serializedField: updateSerializedFieldSchema,
86
+ });
77
87
  // Payload für `delete-tenant-field` / `delete-system-field`.
78
88
  export const deleteFieldPayloadSchema = z.object({
79
89
  entityName: z.string().min(1).max(64),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.358.0",
3
+ "version": "0.359.0",
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>",
@@ -516,12 +516,12 @@
516
516
  }
517
517
  },
518
518
  "dependencies": {
519
- "@cosmicdrift/kumiko-dispatcher-live": "0.358.0",
520
- "@cosmicdrift/kumiko-framework": "0.358.0",
521
- "@cosmicdrift/kumiko-headless": "0.358.0",
522
- "@cosmicdrift/kumiko-renderer": "0.358.0",
523
- "@cosmicdrift/kumiko-renderer-web": "0.358.0",
524
- "@cosmicdrift/kumiko-types": "0.358.0",
519
+ "@cosmicdrift/kumiko-dispatcher-live": "0.359.0",
520
+ "@cosmicdrift/kumiko-framework": "0.359.0",
521
+ "@cosmicdrift/kumiko-headless": "0.359.0",
522
+ "@cosmicdrift/kumiko-renderer": "0.359.0",
523
+ "@cosmicdrift/kumiko-renderer-web": "0.359.0",
524
+ "@cosmicdrift/kumiko-types": "0.359.0",
525
525
  "@mollie/api-client": "^4.5.0",
526
526
  "@node-rs/argon2": "^2.0.2",
527
527
  "@types/mailparser": "^3.4.6",
@@ -1050,8 +1050,8 @@
1050
1050
  ],
1051
1051
  "devDependencies": {
1052
1052
  "@testing-library/user-event": "^14.6.1",
1053
- "@cosmicdrift/kumiko-locale-de": "0.358.0",
1054
- "@cosmicdrift/kumiko-locale-es": "0.358.0",
1053
+ "@cosmicdrift/kumiko-locale-de": "0.359.0",
1054
+ "@cosmicdrift/kumiko-locale-es": "0.359.0",
1055
1055
  "jsqr": "^1.4.0"
1056
1056
  }
1057
1057
  }
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.359.0",
4
+ "type": "improvement",
5
+ "title": "update-tenant-field keeps the stored type when serializedField.type is omitted",
6
+ "detail": "custom-fields: update-tenant-field accepts serializedField without type and keeps the stored type; a different type is still refused with 422 field_type_immutable. The handler declares excludedFields [\"type\"]."
7
+ },
2
8
  {
3
9
  "version": "0.338.0",
4
10
  "type": "fix",