@carlonicora/nextjs-jsonapi 1.7.6 → 1.8.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 (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 +118 -12
  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 +16 -3
  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 +36 -13
  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 +125 -13
  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 +18 -3
  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 +42 -15
  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
@@ -12,7 +12,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
12
12
 
13
13
 
14
14
 
15
- var _chunk3HL4VFJ4js = require('../chunk-3HL4VFJ4.js');
15
+ var _chunk7Z7FEMEBjs = require('../chunk-7Z7FEMEB.js');
16
16
  require('../chunk-IBS6NI7D.js');
17
17
 
18
18
 
@@ -93,7 +93,7 @@ var ServerSession = class {
93
93
  if (!rawModules) return false;
94
94
  const modules = JSON.parse(_zlib2.default.gunzipSync(Buffer.from(rawModules, "base64")).toString());
95
95
  const selectedModule = modules.find((module) => module.id === params.module.moduleId);
96
- return _chunk3HL4VFJ4js.checkPermissionsFromServer.call(void 0, {
96
+ return _chunk7Z7FEMEBjs.checkPermissionsFromServer.call(void 0, {
97
97
  module: params.module,
98
98
  action: params.action,
99
99
  data: params.data,
@@ -303,5 +303,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
303
303
 
304
304
 
305
305
 
306
- exports.ServerAuthService = _chunk3HL4VFJ4js.AuthService; exports.ServerCompanyService = _chunk3HL4VFJ4js.CompanyService; exports.ServerContentService = _chunk3HL4VFJ4js.ContentService; exports.ServerFeatureService = _chunk3HL4VFJ4js.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk3HL4VFJ4js.NotificationService; exports.ServerPushService = _chunk3HL4VFJ4js.PushService; exports.ServerRoleService = _chunk3HL4VFJ4js.RoleService; exports.ServerS3Service = _chunk3HL4VFJ4js.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk3HL4VFJ4js.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
306
+ exports.ServerAuthService = _chunk7Z7FEMEBjs.AuthService; exports.ServerCompanyService = _chunk7Z7FEMEBjs.CompanyService; exports.ServerContentService = _chunk7Z7FEMEBjs.ContentService; exports.ServerFeatureService = _chunk7Z7FEMEBjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk7Z7FEMEBjs.NotificationService; exports.ServerPushService = _chunk7Z7FEMEBjs.PushService; exports.ServerRoleService = _chunk7Z7FEMEBjs.RoleService; exports.ServerS3Service = _chunk7Z7FEMEBjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk7Z7FEMEBjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
307
307
  //# sourceMappingURL=index.js.map
@@ -12,7 +12,7 @@ import {
12
12
  S3Service,
13
13
  UserService,
14
14
  checkPermissionsFromServer
15
- } from "../chunk-2O7ODHTG.mjs";
15
+ } from "../chunk-CK5KLBZV.mjs";
16
16
  import "../chunk-C7C7VY4F.mjs";
17
17
  import {
18
18
  JsonApiDataFactory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.7.6",
3
+ "version": "1.8.1",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -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";`);
@@ -256,7 +271,7 @@ function generateFormSchema(data: FrontendTemplateData): string {
256
271
  // Add name field for Content-extending modules
257
272
  if (extendsContent) {
258
273
  schemaFields.push(` name: z.string().min(1, {
259
- message: t(\`features.${names.camelCase}.fields.name.error\`),
274
+ message: t(\`features.${names.camelCase.toLowerCase()}.fields.name.error\`),
260
275
  }),`);
261
276
  }
262
277
 
@@ -268,7 +283,7 @@ function generateFormSchema(data: FrontendTemplateData): string {
268
283
  schemaFields.push(` ${field.name}: z.string().optional(),`);
269
284
  } else {
270
285
  schemaFields.push(` ${field.name}: z.string().min(1, {
271
- message: t(\`features.${names.camelCase}.fields.${field.name}.error\`),
286
+ message: t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.error\`),
272
287
  }),`);
273
288
  }
274
289
  } else {
@@ -279,6 +294,7 @@ function generateFormSchema(data: FrontendTemplateData): string {
279
294
  // Relationship fields
280
295
  relationships.forEach((rel) => {
281
296
  const fieldId = toCamelCase(rel.variant || rel.name);
297
+ const fieldIdLower = fieldId.toLowerCase();
282
298
  if (rel.variant === AUTHOR_VARIANT) {
283
299
  schemaFields.push(` ${fieldId}: userObjectSchema.refine((data) => data.id && data.id.length > 0, {
284
300
  message: t(\`generic.relationships.author.error\`),
@@ -288,9 +304,34 @@ function generateFormSchema(data: FrontendTemplateData): string {
288
304
  schemaFields.push(` ${fieldId}: entityObjectSchema.optional(),`);
289
305
  } else {
290
306
  schemaFields.push(` ${fieldId}: entityObjectSchema.refine((data) => data.id && data.id.length > 0, {
291
- message: t(\`generic.relationships.${fieldId}.error\`),
307
+ message: t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.error\`),
292
308
  }),`);
293
309
  }
310
+ // Add relationship property fields to schema
311
+ if (rel.fields && rel.fields.length > 0) {
312
+ rel.fields.forEach((field) => {
313
+ const optional = rel.nullable ? ".optional()" : "";
314
+ switch (field.type) {
315
+ case "number":
316
+ schemaFields.push(` ${field.name}: z.number()${optional},`);
317
+ break;
318
+ case "boolean":
319
+ schemaFields.push(` ${field.name}: z.boolean()${optional},`);
320
+ break;
321
+ case "date":
322
+ case "datetime":
323
+ schemaFields.push(` ${field.name}: z.coerce.date()${optional},`);
324
+ break;
325
+ case "any":
326
+ schemaFields.push(` ${field.name}: z.any()${optional},`);
327
+ break;
328
+ case "string":
329
+ default:
330
+ schemaFields.push(` ${field.name}: z.string()${optional},`);
331
+ break;
332
+ }
333
+ });
334
+ }
294
335
  } else {
295
336
  schemaFields.push(` ${fieldId}: z.array(entityObjectSchema).optional(),`);
296
337
  }
@@ -349,6 +390,28 @@ function generateDefaultValues(data: FrontendTemplateData): string {
349
390
  defaults.push(` ${fieldId}: ${names.camelCase}?.${propertyName}
350
391
  ? { id: ${names.camelCase}.${propertyName}.id, name: ${names.camelCase}.${propertyName}.name }
351
392
  : undefined,`);
393
+ // Add relationship property field defaults
394
+ if (rel.fields && rel.fields.length > 0) {
395
+ rel.fields.forEach((field) => {
396
+ switch (field.type) {
397
+ case "number":
398
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name} ?? 0,`);
399
+ break;
400
+ case "boolean":
401
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name} ?? false,`);
402
+ break;
403
+ case "date":
404
+ case "datetime":
405
+ case "any":
406
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name},`);
407
+ break;
408
+ case "string":
409
+ default:
410
+ defaults.push(` ${field.name}: ${names.camelCase}?.${propertyName}?.${field.name} ?? "",`);
411
+ break;
412
+ }
413
+ });
414
+ }
352
415
  } else {
353
416
  defaults.push(` ${fieldId}: ${names.camelCase}?.${pluralPropertyName}
354
417
  ? ${names.camelCase}.${pluralPropertyName}.map((item) => ({ id: item.id, name: item.name }))
@@ -394,6 +457,12 @@ function generateOnSubmit(data: FrontendTemplateData): string {
394
457
 
395
458
  if (rel.single) {
396
459
  payloadFields.push(` ${payloadKey}: values.${fieldId}?.id,`);
460
+ // Add relationship property fields to payload
461
+ if (rel.fields && rel.fields.length > 0) {
462
+ rel.fields.forEach((field) => {
463
+ payloadFields.push(` ${field.name}: values.${field.name},`);
464
+ });
465
+ }
397
466
  } else {
398
467
  payloadFields.push(` ${payloadKey}: values.${fieldId} ? values.${fieldId}.map((item) => item.id) : [],`);
399
468
  }
@@ -435,8 +504,8 @@ function generateFormFields(data: FrontendTemplateData): string {
435
504
  formElements.push(` <FormInput
436
505
  form={form}
437
506
  id="name"
438
- name={t(\`features.${names.camelCase}.fields.name.label\`)}
439
- placeholder={t(\`features.${names.camelCase}.fields.name.placeholder\`)}
507
+ name={t(\`features.${names.camelCase.toLowerCase()}.fields.name.label\`)}
508
+ placeholder={t(\`features.${names.camelCase.toLowerCase()}.fields.name.placeholder\`)}
440
509
  isRequired
441
510
  />`);
442
511
  }
@@ -448,7 +517,7 @@ function generateFormFields(data: FrontendTemplateData): string {
448
517
 
449
518
  fieldsToInclude.forEach((field) => {
450
519
  if (field.name === "content" || field.isContentField) {
451
- formElements.push(` <FormContainerGeneric form={form} id="${field.name}" name={t(\`features.${names.camelCase}.fields.${field.name}.label\`)}>
520
+ formElements.push(` <FormContainerGeneric form={form} id="${field.name}" name={t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.label\`)}>
452
521
  <BlockNoteEditorContainer
453
522
  id={form.getValues("id")}
454
523
  type="${names.camelCase}"
@@ -456,7 +525,7 @@ function generateFormFields(data: FrontendTemplateData): string {
456
525
  onChange={(content, isEmpty, hasUnresolvedDiff) => {
457
526
  form.setValue("${field.name}", content);
458
527
  }}
459
- placeholder={t(\`features.${names.camelCase}.fields.${field.name}.placeholder\`)}
528
+ placeholder={t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.placeholder\`)}
460
529
  bordered
461
530
  />
462
531
  </FormContainerGeneric>`);
@@ -465,8 +534,8 @@ function generateFormFields(data: FrontendTemplateData): string {
465
534
  formElements.push(` <FormInput
466
535
  form={form}
467
536
  id="${field.name}"
468
- name={t(\`features.${names.camelCase}.fields.${field.name}.label\`)}
469
- placeholder={t(\`features.${names.camelCase}.fields.${field.name}.placeholder\`)}${isRequired ? "\n isRequired" : ""}
537
+ name={t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.label\`)}
538
+ placeholder={t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.placeholder\`)}${isRequired ? "\n isRequired" : ""}
470
539
  />`);
471
540
  }
472
541
  });
@@ -479,14 +548,57 @@ function generateFormFields(data: FrontendTemplateData): string {
479
548
  }
480
549
 
481
550
  const fieldId = toCamelCase(rel.variant || rel.name);
551
+ const fieldIdLower = fieldId.toLowerCase();
482
552
 
483
553
  if (rel.single) {
484
554
  formElements.push(` <${rel.name}Selector
485
555
  id="${fieldId}"
486
556
  form={form}
487
- label={t(\`generic.relationships.${fieldId}.label\`)}
488
- placeholder={t(\`generic.relationships.${fieldId}.placeholder\`)}${!rel.nullable ? "\n isRequired" : ""}
557
+ label={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.label\`)}
558
+ placeholder={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.placeholder\`)}${!rel.nullable ? "\n isRequired" : ""}
559
+ />`);
560
+ // Add form inputs for relationship property fields
561
+ if (rel.fields && rel.fields.length > 0) {
562
+ rel.fields.forEach((field) => {
563
+ const isRequired = !rel.nullable;
564
+ switch (field.type) {
565
+ case "number":
566
+ formElements.push(` <FormInput
567
+ form={form}
568
+ id="${field.name}"
569
+ name={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.fields.${field.name}.label\`)}
570
+ placeholder={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.fields.${field.name}.placeholder\`)}
571
+ type="number"${isRequired ? "\n isRequired" : ""}
572
+ />`);
573
+ break;
574
+ case "boolean":
575
+ formElements.push(` <FormCheckbox
576
+ form={form}
577
+ id="${field.name}"
578
+ name={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.fields.${field.name}.label\`)}
489
579
  />`);
580
+ break;
581
+ case "date":
582
+ case "datetime":
583
+ formElements.push(` <FormDatePicker
584
+ form={form}
585
+ id="${field.name}"
586
+ name={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.fields.${field.name}.label\`)}${isRequired ? "\n isRequired" : ""}
587
+ />`);
588
+ break;
589
+ case "string":
590
+ case "any":
591
+ default:
592
+ formElements.push(` <FormInput
593
+ form={form}
594
+ id="${field.name}"
595
+ name={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.fields.${field.name}.label\`)}
596
+ placeholder={t(\`features.${names.camelCase.toLowerCase()}.relationships.${fieldIdLower}.fields.${field.name}.placeholder\`)}${isRequired ? "\n isRequired" : ""}
597
+ />`);
598
+ break;
599
+ }
600
+ });
601
+ }
490
602
  } else {
491
603
  formElements.push(` <${rel.name}MultiSelector
492
604
  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 {
@@ -20,7 +20,7 @@ export function generateI18nKeys(
20
20
  fields: FrontendField[],
21
21
  relationships: FrontendRelationship[]
22
22
  ): I18nKeySet {
23
- const lowerModuleName = names.camelCase;
23
+ const lowerModuleName = names.camelCase.toLowerCase();
24
24
 
25
25
  // Generate field keys
26
26
  const fieldKeys: I18nKeySet["fields"] = {};
@@ -36,13 +36,25 @@ export function generateI18nKeys(
36
36
  const relationshipKeys: I18nKeySet["relationships"] = {};
37
37
  relationships.forEach((rel) => {
38
38
  const effectiveName = rel.variant || rel.name;
39
- const effectiveKey = toCamelCase(effectiveName);
39
+ const effectiveKey = toCamelCase(effectiveName).toLowerCase();
40
40
  relationshipKeys[effectiveKey] = {
41
41
  label: toTitleCase(effectiveName),
42
42
  placeholder: `Select ${toTitleCase(effectiveName).toLowerCase()}`,
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: {
@@ -52,8 +52,47 @@ export function updateI18n(
52
52
  };
53
53
  }
54
54
 
55
+ // Build the i18n messages for this module
56
+ const moduleMessages = buildI18nMessages(i18nKeys);
57
+ const lowercaseModuleName = names.camelCase.toLowerCase();
58
+
59
+ // Always ensure types section is updated (even if features already exist)
60
+ let typesUpdated = false;
61
+ if (!messages.types) {
62
+ messages.types = {};
63
+ }
64
+ const typesKey = Object.keys(moduleMessages.types)[0];
65
+ const lowercasePluralKey = names.pluralCamel.toLowerCase();
66
+ if (typesKey && !messages.types[lowercasePluralKey]) {
67
+ messages.types[lowercasePluralKey] = moduleMessages.types[typesKey];
68
+ typesUpdated = true;
69
+ }
70
+
55
71
  // Check if module already exists in features
56
- if (messages.features && messages.features[names.camelCase]) {
72
+ const featuresAlreadyExist = messages.features && messages.features[lowercaseModuleName];
73
+
74
+ if (featuresAlreadyExist) {
75
+ // Features exist, but we may have added types
76
+ if (typesUpdated) {
77
+ if (dryRun) {
78
+ return {
79
+ success: true,
80
+ message: `[DRY RUN] Module ${names.camelCase} exists, would add types.${lowercasePluralKey}`,
81
+ alreadyExists: true,
82
+ };
83
+ }
84
+
85
+ // Write updated content (types were added)
86
+ const updatedContent = JSON.stringify(messages, null, 2);
87
+ fs.writeFileSync(messagesPath, updatedContent, "utf-8");
88
+
89
+ return {
90
+ success: true,
91
+ message: `Module ${names.camelCase} exists, added types.${lowercasePluralKey}`,
92
+ alreadyExists: true,
93
+ };
94
+ }
95
+
57
96
  return {
58
97
  success: true,
59
98
  message: `Module ${names.camelCase} already exists in messages/${language}.json`,
@@ -61,23 +100,11 @@ export function updateI18n(
61
100
  };
62
101
  }
63
102
 
64
- // Build the i18n messages for this module
65
- const moduleMessages = buildI18nMessages(i18nKeys);
66
-
67
- // Add to features section
103
+ // Add to features section (new module)
68
104
  if (!messages.features) {
69
105
  messages.features = {};
70
106
  }
71
- messages.features[names.camelCase] = moduleMessages.features[i18nKeys.moduleName];
72
-
73
- // Add to types section (if not exists)
74
- if (!messages.types) {
75
- messages.types = {};
76
- }
77
- const typesKey = Object.keys(moduleMessages.types)[0];
78
- if (typesKey && !messages.types[names.pluralCamel]) {
79
- messages.types[names.pluralCamel] = moduleMessages.types[typesKey];
80
- }
107
+ messages.features[lowercaseModuleName] = moduleMessages.features[i18nKeys.moduleName];
81
108
 
82
109
  if (dryRun) {
83
110
  return {