@carlonicora/nextjs-jsonapi 1.7.5 → 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 (59) 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.d.ts.map +1 -1
  28. package/dist/scripts/generate-web-module/templates/data/model.template.js +34 -8
  29. package/dist/scripts/generate-web-module/templates/data/model.template.js.map +1 -1
  30. package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts.map +1 -1
  31. package/dist/scripts/generate-web-module/transformers/i18n-generator.js +14 -1
  32. package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -1
  33. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts +5 -2
  34. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts.map +1 -1
  35. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js +17 -7
  36. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js.map +1 -1
  37. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts +1 -0
  38. package/dist/scripts/generate-web-module/types/json-schema.interface.d.ts.map +1 -1
  39. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts +6 -0
  40. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts.map +1 -1
  41. package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts.map +1 -1
  42. package/dist/scripts/generate-web-module/utils/i18n-updater.js +3 -2
  43. package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -1
  44. package/dist/server/index.js +3 -3
  45. package/dist/server/index.mjs +1 -1
  46. package/package.json +1 -1
  47. package/scripts/generate-web-module/templates/components/editor.template.ts +115 -5
  48. package/scripts/generate-web-module/templates/data/interface.template.ts +18 -2
  49. package/scripts/generate-web-module/templates/data/model.template.ts +55 -33
  50. package/scripts/generate-web-module/transformers/i18n-generator.ts +16 -1
  51. package/scripts/generate-web-module/transformers/relationship-resolver.ts +18 -7
  52. package/scripts/generate-web-module/types/json-schema.interface.ts +1 -0
  53. package/scripts/generate-web-module/types/template-data.interface.ts +9 -0
  54. package/scripts/generate-web-module/utils/i18n-updater.ts +3 -2
  55. package/src/core/abstracts/AbstractApiData.ts +34 -0
  56. package/dist/chunk-2O7ODHTG.mjs.map +0 -1
  57. package/dist/chunk-3HL4VFJ4.js.map +0 -1
  58. /package/dist/{BlockNoteEditor-KH7WWHBK.mjs.map → BlockNoteEditor-N534QVBR.mjs.map} +0 -0
  59. /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));
@@ -4,9 +4,9 @@
4
4
  * Generates {Module}.ts class file with rehydrate and createJsonApi methods.
5
5
  */
6
6
 
7
- import { FrontendTemplateData, FrontendField, FrontendRelationship } from "../../types/template-data.interface";
8
- import { toCamelCase, toPascalCase, pluralize } from "../../transformers/name-transformer";
7
+ import { pluralize, toCamelCase } from "../../transformers/name-transformer";
9
8
  import { AUTHOR_VARIANT } from "../../types/field-mapping.types";
9
+ import { FrontendField, FrontendTemplateData } from "../../types/template-data.interface";
10
10
 
11
11
  /**
12
12
  * Generate the model file content
@@ -48,27 +48,21 @@ function generateImports(data: FrontendTemplateData): string {
48
48
 
49
49
  // Own interface import
50
50
  imports.push(
51
- `import { ${names.pascalCase}Input, ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";`
51
+ `import { ${names.pascalCase}Input, ${names.pascalCase}Interface } from "@/features/${data.targetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";`,
52
52
  );
53
53
 
54
54
  // Relationship interface imports
55
55
  relationships.forEach((rel) => {
56
- imports.push(
57
- `import { ${rel.interfaceName} } from "${rel.interfaceImportPath}";`
58
- );
56
+ imports.push(`import { ${rel.interfaceName} } from "${rel.interfaceImportPath}";`);
59
57
  });
60
58
 
61
59
  // Base class and core imports
62
60
  if (extendsContent) {
63
- imports.push(
64
- `import { Content } from "@/features/features/content/data/Content";`
65
- );
66
- imports.push(
67
- `import { JsonApiHydratedDataInterface, Modules } from "@carlonicora/nextjs-jsonapi/core";`
68
- );
61
+ imports.push(`import { Content } from "@/features/features/content/data/Content";`);
62
+ imports.push(`import { JsonApiHydratedDataInterface, Modules } from "@carlonicora/nextjs-jsonapi/core";`);
69
63
  } else {
70
64
  imports.push(
71
- `import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "@carlonicora/nextjs-jsonapi/core";`
65
+ `import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "@carlonicora/nextjs-jsonapi/core";`,
72
66
  );
73
67
  }
74
68
 
@@ -97,7 +91,13 @@ function generatePrivateFields(data: FrontendTemplateData): string {
97
91
  const effectiveName = rel.variant || rel.name;
98
92
  if (rel.single) {
99
93
  const propName = toCamelCase(effectiveName);
100
- 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};`);
101
101
  } else {
102
102
  const propName = pluralize(toCamelCase(rel.name));
103
103
  lines.push(` private _${propName}?: ${rel.interfaceName}[];`);
@@ -133,12 +133,20 @@ function generateGetters(data: FrontendTemplateData): string {
133
133
  const effectiveName = rel.variant || rel.name;
134
134
  if (rel.single) {
135
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
+
136
144
  if (rel.nullable) {
137
- lines.push(` get ${propName}(): ${rel.interfaceName} | undefined {
145
+ lines.push(` get ${propName}(): (${baseType}) | undefined {
138
146
  return this._${propName};
139
147
  }`);
140
148
  } else {
141
- lines.push(` get ${propName}(): ${rel.interfaceName} {
149
+ lines.push(` get ${propName}(): ${baseType} {
142
150
  if (this._${propName} === undefined) throw new Error("JsonApi error: ${data.names.camelCase} ${propName} is missing");
143
151
  return this._${propName};
144
152
  }`);
@@ -175,16 +183,14 @@ function generateRehydrateMethod(data: FrontendTemplateData): string {
175
183
  if (field.isContentField || field.name === "content") {
176
184
  // Content fields need JSON parsing
177
185
  lines.push(
178
- ` this._${field.name} = data.jsonApi.attributes.${field.name} ? JSON.parse(data.jsonApi.attributes.${field.name}) : undefined;`
186
+ ` this._${field.name} = data.jsonApi.attributes.${field.name} ? JSON.parse(data.jsonApi.attributes.${field.name}) : undefined;`,
179
187
  );
180
188
  } else if (field.type === "date") {
181
189
  lines.push(
182
- ` this._${field.name} = data.jsonApi.attributes.${field.name} ? new Date(data.jsonApi.attributes.${field.name}) : undefined;`
190
+ ` this._${field.name} = data.jsonApi.attributes.${field.name} ? new Date(data.jsonApi.attributes.${field.name}) : undefined;`,
183
191
  );
184
192
  } else {
185
- lines.push(
186
- ` this._${field.name} = data.jsonApi.attributes.${field.name};`
187
- );
193
+ lines.push(` this._${field.name} = data.jsonApi.attributes.${field.name};`);
188
194
  }
189
195
  });
190
196
 
@@ -196,14 +202,23 @@ function generateRehydrateMethod(data: FrontendTemplateData): string {
196
202
  if (rel.single) {
197
203
  const propName = toCamelCase(effectiveName);
198
204
  const relationshipKey = effectiveName.toLowerCase();
199
- lines.push(
200
- ` this._${propName} = this._readIncluded(data, "${relationshipKey}", Modules.${rel.name}) as ${rel.interfaceName}${rel.nullable ? " | undefined" : ""};`
201
- );
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
+ }
202
217
  } else {
203
218
  const propName = pluralize(toCamelCase(rel.name));
204
219
  const relationshipKey = pluralize(rel.name.toLowerCase());
205
220
  lines.push(
206
- ` this._${propName} = this._readIncludedList(data, "${relationshipKey}", Modules.${rel.name}) as ${rel.interfaceName}[];`
221
+ ` this._${propName} = this._readIncluded(data, "${relationshipKey}", Modules.${rel.name}) as ${rel.interfaceName}[];`,
207
222
  );
208
223
  }
209
224
  });
@@ -250,27 +265,23 @@ function generateCreateJsonApiMethod(data: FrontendTemplateData): string {
250
265
  fieldsToInclude.forEach((field) => {
251
266
  if (field.isContentField || field.name === "content") {
252
267
  lines.push(
253
- ` if (data.${field.name} !== undefined) response.data.attributes.${field.name} = JSON.stringify(data.${field.name});`
268
+ ` if (data.${field.name} !== undefined) response.data.attributes.${field.name} = JSON.stringify(data.${field.name});`,
254
269
  );
255
270
  } else {
256
271
  lines.push(
257
- ` if (data.${field.name} !== undefined) response.data.attributes.${field.name} = data.${field.name};`
272
+ ` if (data.${field.name} !== undefined) response.data.attributes.${field.name} = data.${field.name};`,
258
273
  );
259
274
  }
260
275
  });
261
276
 
262
277
  // Relationship serialization (skip author for Content, handle others)
263
- const relationshipsToSerialize = relationships.filter(
264
- (rel) => !(extendsContent && rel.variant === AUTHOR_VARIANT)
265
- );
278
+ const relationshipsToSerialize = relationships.filter((rel) => !(extendsContent && rel.variant === AUTHOR_VARIANT));
266
279
 
267
280
  if (relationshipsToSerialize.length > 0) {
268
281
  lines.push(``);
269
282
  relationshipsToSerialize.forEach((rel) => {
270
283
  const effectiveName = rel.variant || rel.name;
271
- const payloadKey = rel.single
272
- ? `${toCamelCase(effectiveName)}Id`
273
- : `${toCamelCase(rel.name)}Ids`;
284
+ const payloadKey = rel.single ? `${toCamelCase(effectiveName)}Id` : `${toCamelCase(rel.name)}Ids`;
274
285
  const relationshipKey = toCamelCase(effectiveName);
275
286
 
276
287
  if (rel.single) {
@@ -280,6 +291,17 @@ function generateCreateJsonApiMethod(data: FrontendTemplateData): string {
280
291
  lines.push(` type: Modules.${rel.name}.name,`);
281
292
  lines.push(` id: data.${payloadKey},`);
282
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
+
283
305
  lines.push(` };`);
284
306
  lines.push(` }`);
285
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,