@carlonicora/nextjs-jsonapi 1.7.6 → 1.8.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.
Files changed (58) hide show
  1. package/dist/{BlockNoteEditor-CCOSI7TW.js → BlockNoteEditor-KSPPX6JO.js} +13 -13
  2. package/dist/{BlockNoteEditor-CCOSI7TW.js.map → BlockNoteEditor-KSPPX6JO.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-KH7WWHBK.mjs → BlockNoteEditor-N534QVBR.mjs} +3 -3
  4. package/dist/{chunk-3HL4VFJ4.js → chunk-7Z7FEMEB.js} +56 -36
  5. package/dist/chunk-7Z7FEMEB.js.map +1 -0
  6. package/dist/{chunk-KFON36OE.js → chunk-B426TLJC.js} +358 -358
  7. package/dist/{chunk-KFON36OE.js.map → chunk-B426TLJC.js.map} +1 -1
  8. package/dist/{chunk-2O7ODHTG.mjs → chunk-CK5KLBZV.mjs} +21 -1
  9. package/dist/chunk-CK5KLBZV.mjs.map +1 -0
  10. package/dist/{chunk-2TCBJO4B.mjs → chunk-TLBZWOCU.mjs} +3 -3
  11. package/dist/client/index.js +3 -3
  12. package/dist/client/index.mjs +2 -2
  13. package/dist/components/index.js +3 -3
  14. package/dist/components/index.mjs +2 -2
  15. package/dist/contexts/index.js +3 -3
  16. package/dist/contexts/index.mjs +2 -2
  17. package/dist/core/index.d.mts +10 -0
  18. package/dist/core/index.d.ts +10 -0
  19. package/dist/core/index.js +2 -2
  20. package/dist/core/index.mjs +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/index.mjs +1 -1
  23. package/dist/scripts/generate-web-module/templates/components/editor.template.js +108 -4
  24. package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -1
  25. package/dist/scripts/generate-web-module/templates/data/interface.template.js +15 -2
  26. package/dist/scripts/generate-web-module/templates/data/interface.template.js.map +1 -1
  27. package/dist/scripts/generate-web-module/templates/data/model.template.js +32 -4
  28. package/dist/scripts/generate-web-module/templates/data/model.template.js.map +1 -1
  29. package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts.map +1 -1
  30. package/dist/scripts/generate-web-module/transformers/i18n-generator.js +14 -1
  31. package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -1
  32. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts +5 -2
  33. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts.map +1 -1
  34. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js +17 -7
  35. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js.map +1 -1
  36. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts +1 -0
  37. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts.map +1 -1
  38. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts +6 -0
  39. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts.map +1 -1
  40. package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts.map +1 -1
  41. package/dist/scripts/generate-web-module/utils/i18n-updater.js +3 -2
  42. package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -1
  43. package/dist/server/index.js +3 -3
  44. package/dist/server/index.mjs +1 -1
  45. package/package.json +1 -1
  46. package/scripts/generate-web-module/templates/components/editor.template.ts +115 -5
  47. package/scripts/generate-web-module/templates/data/interface.template.ts +18 -2
  48. package/scripts/generate-web-module/templates/data/model.template.ts +40 -6
  49. package/scripts/generate-web-module/transformers/i18n-generator.ts +16 -1
  50. package/scripts/generate-web-module/transformers/relationship-resolver.ts +18 -7
  51. package/scripts/generate-web-module/types/json-schema.interface.ts +1 -0
  52. package/scripts/generate-web-module/types/template-data.interface.ts +9 -0
  53. package/scripts/generate-web-module/utils/i18n-updater.ts +3 -2
  54. package/src/core/abstracts/AbstractApiData.ts +34 -0
  55. package/dist/chunk-2O7ODHTG.mjs.map +0 -1
  56. package/dist/chunk-3HL4VFJ4.js.map +0 -1
  57. /package/dist/{BlockNoteEditor-KH7WWHBK.mjs.map → BlockNoteEditor-N534QVBR.mjs.map} +0 -0
  58. /package/dist/{chunk-2TCBJO4B.mjs.map → chunk-TLBZWOCU.mjs.map} +0 -0
@@ -8,7 +8,7 @@ import { FrontendTemplateData, FrontendField, FrontendRelationship } from "../..
8
8
  import { toCamelCase, pluralize, toPascalCase } from "../../transformers/name-transformer";
9
9
  import { AUTHOR_VARIANT } from "../../types/field-mapping.types";
10
10
  import { getFormFieldJsx } from "../../transformers/field-mapper";
11
- import { getRelationshipFormJsx, getDefaultValueExpression, getPayloadMapping, isFoundationImport, FOUNDATION_PACKAGE } from "../../transformers/relationship-resolver";
11
+ import { getRelationshipFormJsx, getDefaultValueExpression, getPayloadMapping, isFoundationImport, FOUNDATION_COMPONENTS_PACKAGE } from "../../transformers/relationship-resolver";
12
12
 
13
13
  /**
14
14
  * Generate the editor component file content
@@ -156,7 +156,7 @@ function generateImports(data: FrontendTemplateData): string {
156
156
  const componentName = rel.single ? `${rel.name}Selector` : `${rel.name}MultiSelector`;
157
157
  if (rel.isFoundation) {
158
158
  // Foundation entities use named imports from the package
159
- imports.push(`import { ${componentName} } from "${FOUNDATION_PACKAGE}";`);
159
+ imports.push(`import { ${componentName} } from "${FOUNDATION_COMPONENTS_PACKAGE}";`);
160
160
  } else {
161
161
  imports.push(`import ${componentName} from "${rel.importPath}";`);
162
162
  }
@@ -188,6 +188,21 @@ function generateImports(data: FrontendTemplateData): string {
188
188
  componentImports.push("FormInput");
189
189
  }
190
190
 
191
+ // Check if any relationship has boolean or date fields that need specific form components
192
+ const hasRelBooleanFields = relationships.some((rel) =>
193
+ rel.fields?.some((f) => f.type === "boolean")
194
+ );
195
+ const hasRelDateFields = relationships.some((rel) =>
196
+ rel.fields?.some((f) => f.type === "date" || f.type === "datetime")
197
+ );
198
+
199
+ if (hasRelBooleanFields) {
200
+ componentImports.push("FormCheckbox");
201
+ }
202
+ if (hasRelDateFields) {
203
+ componentImports.push("FormDatePicker");
204
+ }
205
+
191
206
  imports.push(`import {
192
207
  ${componentImports.join(",\n ")},
193
208
  } from "@carlonicora/nextjs-jsonapi/components";`);
@@ -288,9 +303,34 @@ function generateFormSchema(data: FrontendTemplateData): string {
288
303
  schemaFields.push(` ${fieldId}: entityObjectSchema.optional(),`);
289
304
  } else {
290
305
  schemaFields.push(` ${fieldId}: entityObjectSchema.refine((data) => data.id && data.id.length > 0, {
291
- message: t(\`generic.relationships.${fieldId}.error\`),
306
+ message: t(\`features.${names.camelCase}.relationships.${fieldId}.error\`),
292
307
  }),`);
293
308
  }
309
+ // Add relationship property fields to schema
310
+ if (rel.fields && rel.fields.length > 0) {
311
+ rel.fields.forEach((field) => {
312
+ const optional = rel.nullable ? ".optional()" : "";
313
+ switch (field.type) {
314
+ case "number":
315
+ schemaFields.push(` ${field.name}: z.number()${optional},`);
316
+ break;
317
+ case "boolean":
318
+ schemaFields.push(` ${field.name}: z.boolean()${optional},`);
319
+ break;
320
+ case "date":
321
+ case "datetime":
322
+ schemaFields.push(` ${field.name}: z.coerce.date()${optional},`);
323
+ break;
324
+ case "any":
325
+ schemaFields.push(` ${field.name}: z.any()${optional},`);
326
+ break;
327
+ case "string":
328
+ default:
329
+ schemaFields.push(` ${field.name}: z.string()${optional},`);
330
+ break;
331
+ }
332
+ });
333
+ }
294
334
  } else {
295
335
  schemaFields.push(` ${fieldId}: z.array(entityObjectSchema).optional(),`);
296
336
  }
@@ -349,6 +389,28 @@ function generateDefaultValues(data: FrontendTemplateData): string {
349
389
  defaults.push(` ${fieldId}: ${names.camelCase}?.${propertyName}
350
390
  ? { id: ${names.camelCase}.${propertyName}.id, name: ${names.camelCase}.${propertyName}.name }
351
391
  : undefined,`);
392
+ // Add relationship property field defaults
393
+ if (rel.fields && rel.fields.length > 0) {
394
+ rel.fields.forEach((field) => {
395
+ switch (field.type) {
396
+ case "number":
397
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name} ?? 0,`);
398
+ break;
399
+ case "boolean":
400
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name} ?? false,`);
401
+ break;
402
+ case "date":
403
+ case "datetime":
404
+ case "any":
405
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name},`);
406
+ break;
407
+ case "string":
408
+ default:
409
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name} ?? "",`);
410
+ break;
411
+ }
412
+ });
413
+ }
352
414
  } else {
353
415
  defaults.push(` ${fieldId}: ${names.camelCase}?.${pluralPropertyName}
354
416
  ? ${names.camelCase}.${pluralPropertyName}.map((item) => ({ id: item.id, name: item.name }))
@@ -394,6 +456,12 @@ function generateOnSubmit(data: FrontendTemplateData): string {
394
456
 
395
457
  if (rel.single) {
396
458
  payloadFields.push(` ${payloadKey}: values.${fieldId}?.id,`);
459
+ // Add relationship property fields to payload
460
+ if (rel.fields && rel.fields.length > 0) {
461
+ rel.fields.forEach((field) => {
462
+ payloadFields.push(` ${field.name}: values.${field.name},`);
463
+ });
464
+ }
397
465
  } else {
398
466
  payloadFields.push(` ${payloadKey}: values.${fieldId} ? values.${fieldId}.map((item) => item.id) : [],`);
399
467
  }
@@ -484,9 +552,51 @@ function generateFormFields(data: FrontendTemplateData): string {
484
552
  formElements.push(` <${rel.name}Selector
485
553
  id="${fieldId}"
486
554
  form={form}
487
- label={t(\`generic.relationships.${fieldId}.label\`)}
488
- placeholder={t(\`generic.relationships.${fieldId}.placeholder\`)}${!rel.nullable ? "\n isRequired" : ""}
555
+ label={t(\`features.${names.camelCase}.relationships.${fieldId}.label\`)}
556
+ placeholder={t(\`features.${names.camelCase}.relationships.${fieldId}.placeholder\`)}${!rel.nullable ? "\n isRequired" : ""}
557
+ />`);
558
+ // Add form inputs for relationship property fields
559
+ if (rel.fields && rel.fields.length > 0) {
560
+ rel.fields.forEach((field) => {
561
+ const isRequired = !rel.nullable;
562
+ switch (field.type) {
563
+ case "number":
564
+ formElements.push(` <FormInput
565
+ form={form}
566
+ id="${field.name}"
567
+ name={t(\`features.${names.camelCase}.relationships.${fieldId}.fields.${field.name}.label\`)}
568
+ placeholder={t(\`features.${names.camelCase}.relationships.${fieldId}.fields.${field.name}.placeholder\`)}
569
+ type="number"${isRequired ? "\n isRequired" : ""}
570
+ />`);
571
+ break;
572
+ case "boolean":
573
+ formElements.push(` <FormCheckbox
574
+ form={form}
575
+ id="${field.name}"
576
+ name={t(\`features.${names.camelCase}.relationships.${fieldId}.fields.${field.name}.label\`)}
489
577
  />`);
578
+ break;
579
+ case "date":
580
+ case "datetime":
581
+ formElements.push(` <FormDatePicker
582
+ form={form}
583
+ id="${field.name}"
584
+ name={t(\`features.${names.camelCase}.relationships.${fieldId}.fields.${field.name}.label\`)}${isRequired ? "\n isRequired" : ""}
585
+ />`);
586
+ break;
587
+ case "string":
588
+ case "any":
589
+ default:
590
+ formElements.push(` <FormInput
591
+ form={form}
592
+ id="${field.name}"
593
+ name={t(\`features.${names.camelCase}.relationships.${fieldId}.fields.${field.name}.label\`)}
594
+ placeholder={t(\`features.${names.camelCase}.relationships.${fieldId}.fields.${field.name}.placeholder\`)}${isRequired ? "\n isRequired" : ""}
595
+ />`);
596
+ break;
597
+ }
598
+ });
599
+ }
490
600
  } else {
491
601
  formElements.push(` <${rel.name}MultiSelector
492
602
  id="${fieldId}"
@@ -74,13 +74,21 @@ function generateInputType(data: FrontendTemplateData): string {
74
74
  fieldLines.push(` ${field.name}${optional}: ${field.tsType};`);
75
75
  });
76
76
 
77
- // Add relationship IDs
77
+ // Add relationship IDs and relationship property fields
78
78
  relationships.forEach((rel) => {
79
79
  const effectiveName = rel.variant || rel.name;
80
80
  if (rel.single) {
81
81
  const key = `${toCamelCase(effectiveName)}Id`;
82
82
  const optional = rel.nullable ? "?" : "";
83
83
  fieldLines.push(` ${key}${optional}: string;`);
84
+
85
+ // Add relationship property fields to input type (match relationship optionality)
86
+ if (rel.fields && rel.fields.length > 0) {
87
+ rel.fields.forEach((field) => {
88
+ const fieldOptional = rel.nullable ? "?" : "";
89
+ fieldLines.push(` ${field.name}${fieldOptional}: ${field.tsType};`);
90
+ });
91
+ }
84
92
  } else {
85
93
  const key = `${toCamelCase(rel.name)}Ids`;
86
94
  fieldLines.push(` ${key}?: string[];`);
@@ -120,7 +128,15 @@ function generateInterface(data: FrontendTemplateData): string {
120
128
  const effectiveName = rel.variant || rel.name;
121
129
  if (rel.single) {
122
130
  const propertyName = toCamelCase(effectiveName);
123
- const type = rel.nullable ? `${rel.interfaceName} | undefined` : rel.interfaceName;
131
+
132
+ // Build return type - use intersection if relationship has fields
133
+ let baseType = rel.interfaceName;
134
+ if (rel.fields && rel.fields.length > 0) {
135
+ const metaFields = rel.fields.map(f => `${f.name}?: ${f.tsType}`).join("; ");
136
+ baseType = `${rel.interfaceName} & { ${metaFields} }`;
137
+ }
138
+
139
+ const type = rel.nullable ? `(${baseType}) | undefined` : baseType;
124
140
  getterLines.push(` get ${propertyName}(): ${type};`);
125
141
  } else {
126
142
  const propertyName = pluralize(toCamelCase(rel.name));
@@ -91,7 +91,13 @@ function generatePrivateFields(data: FrontendTemplateData): string {
91
91
  const effectiveName = rel.variant || rel.name;
92
92
  if (rel.single) {
93
93
  const propName = toCamelCase(effectiveName);
94
- lines.push(` private _${propName}?: ${rel.interfaceName};`);
94
+ // Use intersection type if relationship has fields
95
+ let typeDecl = rel.interfaceName;
96
+ if (rel.fields && rel.fields.length > 0) {
97
+ const metaFields = rel.fields.map((f) => `${f.name}?: ${f.tsType}`).join("; ");
98
+ typeDecl = `${rel.interfaceName} & { ${metaFields} }`;
99
+ }
100
+ lines.push(` private _${propName}?: ${typeDecl};`);
95
101
  } else {
96
102
  const propName = pluralize(toCamelCase(rel.name));
97
103
  lines.push(` private _${propName}?: ${rel.interfaceName}[];`);
@@ -127,12 +133,20 @@ function generateGetters(data: FrontendTemplateData): string {
127
133
  const effectiveName = rel.variant || rel.name;
128
134
  if (rel.single) {
129
135
  const propName = toCamelCase(effectiveName);
136
+
137
+ // Build return type - use intersection if relationship has fields
138
+ let baseType = rel.interfaceName;
139
+ if (rel.fields && rel.fields.length > 0) {
140
+ const metaFields = rel.fields.map((f) => `${f.name}?: ${f.tsType}`).join("; ");
141
+ baseType = `${rel.interfaceName} & { ${metaFields} }`;
142
+ }
143
+
130
144
  if (rel.nullable) {
131
- lines.push(` get ${propName}(): ${rel.interfaceName} | undefined {
145
+ lines.push(` get ${propName}(): (${baseType}) | undefined {
132
146
  return this._${propName};
133
147
  }`);
134
148
  } else {
135
- lines.push(` get ${propName}(): ${rel.interfaceName} {
149
+ lines.push(` get ${propName}(): ${baseType} {
136
150
  if (this._${propName} === undefined) throw new Error("JsonApi error: ${data.names.camelCase} ${propName} is missing");
137
151
  return this._${propName};
138
152
  }`);
@@ -188,9 +202,18 @@ function generateRehydrateMethod(data: FrontendTemplateData): string {
188
202
  if (rel.single) {
189
203
  const propName = toCamelCase(effectiveName);
190
204
  const relationshipKey = effectiveName.toLowerCase();
191
- lines.push(
192
- ` this._${propName} = this._readIncluded(data, "${relationshipKey}", Modules.${rel.name}) as ${rel.interfaceName}${rel.nullable ? " | undefined" : ""};`,
193
- );
205
+
206
+ // Use _readIncludedWithMeta for relationships with fields
207
+ if (rel.fields && rel.fields.length > 0) {
208
+ const metaType = `{ ${rel.fields.map((f) => `${f.name}?: ${f.tsType}`).join("; ")} }`;
209
+ lines.push(
210
+ ` this._${propName} = this._readIncludedWithMeta<${rel.interfaceName}, ${metaType}>(data, "${relationshipKey}", Modules.${rel.name});`,
211
+ );
212
+ } else {
213
+ lines.push(
214
+ ` this._${propName} = this._readIncluded(data, "${relationshipKey}", Modules.${rel.name}) as ${rel.interfaceName}${rel.nullable ? " | undefined" : ""};`,
215
+ );
216
+ }
194
217
  } else {
195
218
  const propName = pluralize(toCamelCase(rel.name));
196
219
  const relationshipKey = pluralize(rel.name.toLowerCase());
@@ -268,6 +291,17 @@ function generateCreateJsonApiMethod(data: FrontendTemplateData): string {
268
291
  lines.push(` type: Modules.${rel.name}.name,`);
269
292
  lines.push(` id: data.${payloadKey},`);
270
293
  lines.push(` },`);
294
+
295
+ // Add meta for relationship fields
296
+ if (rel.fields && rel.fields.length > 0) {
297
+ lines.push(` meta: {`);
298
+ rel.fields.forEach((field, i) => {
299
+ const comma = i < rel.fields!.length - 1 ? "," : "";
300
+ lines.push(` ${field.name}: data.${field.name}${comma}`);
301
+ });
302
+ lines.push(` },`);
303
+ }
304
+
271
305
  lines.push(` };`);
272
306
  lines.push(` }`);
273
307
  } else {
@@ -43,6 +43,18 @@ export function generateI18nKeys(
43
43
  error: `${toTitleCase(effectiveName)} is required`,
44
44
  list: pluralize(toTitleCase(rel.name)),
45
45
  };
46
+
47
+ // Add fields for relationship edge properties
48
+ if (rel.fields && rel.fields.length > 0) {
49
+ relationshipKeys[effectiveKey].fields = {};
50
+ rel.fields.forEach((field) => {
51
+ relationshipKeys[effectiveKey].fields![field.name] = {
52
+ label: toTitleCase(field.name),
53
+ placeholder: `Enter ${toTitleCase(field.name).toLowerCase()}`,
54
+ error: `${toTitleCase(field.name)} is required`,
55
+ };
56
+ });
57
+ }
46
58
  });
47
59
 
48
60
  // Generate type keys
@@ -68,6 +80,9 @@ export function generateI18nKeys(
68
80
  * @returns Object structure for en.json
69
81
  */
70
82
  export function buildI18nMessages(i18nKeys: I18nKeySet): Record<string, any> {
83
+ // Use proper pluralization and lowercase for types key
84
+ const pluralLowercaseKey = pluralize(i18nKeys.moduleName).toLowerCase();
85
+
71
86
  return {
72
87
  features: {
73
88
  [i18nKeys.moduleName]: {
@@ -76,7 +91,7 @@ export function buildI18nMessages(i18nKeys: I18nKeySet): Record<string, any> {
76
91
  },
77
92
  },
78
93
  types: {
79
- [i18nKeys.moduleName + "s"]: i18nKeys.type.icuPlural,
94
+ [pluralLowercaseKey]: i18nKeys.type.icuPlural,
80
95
  },
81
96
  };
82
97
  }
@@ -6,14 +6,18 @@
6
6
  */
7
7
 
8
8
  import { JsonRelationshipDefinition } from "../types/json-schema.interface";
9
- import { FrontendRelationship, RelationshipServiceMethod } from "../types/template-data.interface";
9
+ import { FrontendRelationship, FrontendField, RelationshipServiceMethod } from "../types/template-data.interface";
10
10
  import { AUTHOR_VARIANT, AUTHOR_ZOD_SCHEMA, ENTITY_ZOD_SCHEMA } from "../types/field-mapping.types";
11
11
  import { toCamelCase, toKebabCase, pluralize, toPascalCase } from "./name-transformer";
12
+ import { mapFields } from "./field-mapper";
12
13
 
13
14
  /**
14
- * Foundation package name constant for web imports
15
+ * Foundation package constants for web imports
16
+ * Components (selectors) come from /components
17
+ * Data (interfaces, services) come from /core
15
18
  */
16
- export const FOUNDATION_PACKAGE = "@carlonicora/nextjs-jsonapi/features";
19
+ export const FOUNDATION_COMPONENTS_PACKAGE = "@carlonicora/nextjs-jsonapi/components";
20
+ export const FOUNDATION_CORE_PACKAGE = "@carlonicora/nextjs-jsonapi/core";
17
21
 
18
22
  /**
19
23
  * Check if a directory represents a foundation import (from the package)
@@ -78,9 +82,9 @@ export function resolveRelationship(rel: JsonRelationshipDefinition): FrontendRe
78
82
 
79
83
  if (isFoundationImport(rel.directory)) {
80
84
  // Foundation entities import from the package
81
- importPath = FOUNDATION_PACKAGE;
82
- interfaceImportPath = FOUNDATION_PACKAGE;
83
- serviceImportPath = FOUNDATION_PACKAGE;
85
+ importPath = FOUNDATION_COMPONENTS_PACKAGE; // Selectors from /components
86
+ interfaceImportPath = FOUNDATION_CORE_PACKAGE; // Interfaces from /core
87
+ serviceImportPath = FOUNDATION_CORE_PACKAGE; // Services from /core
84
88
  } else {
85
89
  // Feature entities use local paths
86
90
  const webDirectory = mapDirectoryToWebPath(rel.directory);
@@ -89,6 +93,12 @@ export function resolveRelationship(rel: JsonRelationshipDefinition): FrontendRe
89
93
  serviceImportPath = `@/features/${webDirectory}/${modelKebab}/data/${rel.name}Service`;
90
94
  }
91
95
 
96
+ // Map relationship fields (only for single relationships)
97
+ let fields: FrontendField[] | undefined;
98
+ if (rel.single && rel.fields && rel.fields.length > 0) {
99
+ fields = mapFields(rel.fields, toCamelCase(rel.name));
100
+ }
101
+
92
102
  return {
93
103
  name: rel.name,
94
104
  variant: rel.variant,
@@ -106,6 +116,7 @@ export function resolveRelationship(rel: JsonRelationshipDefinition): FrontendRe
106
116
  serviceImportPath,
107
117
  interfaceName: `${rel.name}Interface`,
108
118
  modelKebab,
119
+ fields,
109
120
  };
110
121
  }
111
122
 
@@ -161,7 +172,7 @@ export function getSelectorImports(relationships: FrontendRelationship[]): strin
161
172
  relationships.forEach((rel) => {
162
173
  if (isFoundationImport(rel.directory)) {
163
174
  // Foundation entities use named imports from the package
164
- imports.add(`import { ${rel.selectorComponent} } from "${FOUNDATION_PACKAGE}";`);
175
+ imports.add(`import { ${rel.selectorComponent} } from "${FOUNDATION_COMPONENTS_PACKAGE}";`);
165
176
  } else {
166
177
  imports.add(`import ${rel.selectorComponent} from "${rel.importPath}";`);
167
178
  }
@@ -26,6 +26,7 @@ export interface JsonRelationshipDefinition {
26
26
  relationshipName: string; // Backend-specific, ignored in frontend
27
27
  toNode: boolean; // Backend-specific, ignored in frontend
28
28
  nullable: boolean;
29
+ fields?: JsonFieldDefinition[]; // Relationship property fields (stored on edges)
29
30
  }
30
31
 
31
32
  /**
@@ -58,6 +58,7 @@ export interface FrontendRelationship {
58
58
  serviceImportPath: string; // Full import path for service
59
59
  interfaceName: string; // e.g., "UserInterface"
60
60
  modelKebab: string; // e.g., "user"
61
+ fields?: FrontendField[]; // Relationship property fields (stored on edges)
61
62
  }
62
63
 
63
64
  /**
@@ -80,6 +81,14 @@ export interface I18nKeySet {
80
81
  placeholder: string;
81
82
  error: string;
82
83
  list: string;
84
+ fields?: Record<
85
+ string,
86
+ {
87
+ label: string;
88
+ placeholder: string;
89
+ error: string;
90
+ }
91
+ >;
83
92
  }
84
93
  >;
85
94
  type: {
@@ -75,8 +75,9 @@ export function updateI18n(
75
75
  messages.types = {};
76
76
  }
77
77
  const typesKey = Object.keys(moduleMessages.types)[0];
78
- if (typesKey && !messages.types[names.pluralCamel]) {
79
- messages.types[names.pluralCamel] = moduleMessages.types[typesKey];
78
+ const lowercasePluralKey = names.pluralCamel.toLowerCase();
79
+ if (typesKey && !messages.types[lowercasePluralKey]) {
80
+ messages.types[lowercasePluralKey] = moduleMessages.types[typesKey];
80
81
  }
81
82
 
82
83
  if (dryRun) {
@@ -115,6 +115,40 @@ export abstract class AbstractApiData implements ApiDataInterface {
115
115
  }) as T;
116
116
  }
117
117
 
118
+ /**
119
+ * Read included relationship data and augment with relationship meta properties.
120
+ * Used for single relationships (one-to-one) that have edge properties.
121
+ *
122
+ * @param data - Hydrated JSON:API data
123
+ * @param type - Relationship type key (e.g., "guide")
124
+ * @param dataType - Module reference for rehydration
125
+ * @returns Related object augmented with meta properties, or undefined
126
+ */
127
+ protected _readIncludedWithMeta<T extends ApiDataInterface, M extends Record<string, any>>(
128
+ data: JsonApiHydratedDataInterface,
129
+ type: string,
130
+ dataType: ApiRequestDataTypeInterface,
131
+ ): (T & M) | undefined {
132
+ // Get the base related object using existing logic
133
+ const related = this._readIncluded<T>(data, type, dataType);
134
+
135
+ // Only works for single relationships (not arrays)
136
+ if (!related || Array.isArray(related)) {
137
+ return undefined;
138
+ }
139
+
140
+ // Extract relationship meta from JSON:API data
141
+ const relationshipMeta = data.jsonApi.relationships?.[type]?.meta;
142
+
143
+ // If no meta, return the related object as-is
144
+ if (!relationshipMeta) {
145
+ return related as T & M;
146
+ }
147
+
148
+ // Augment the object with meta properties
149
+ return Object.assign(related, relationshipMeta) as T & M;
150
+ }
151
+
118
152
  dehydrate(): JsonApiHydratedDataInterface {
119
153
  return {
120
154
  jsonApi: this._jsonApi,