@carlonicora/nextjs-jsonapi 1.50.0 → 1.52.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 (131) hide show
  1. package/dist/{BlockNoteEditor-LENHWRS2.js → BlockNoteEditor-KQPSJCYG.js} +6 -6
  2. package/dist/{BlockNoteEditor-LENHWRS2.js.map → BlockNoteEditor-KQPSJCYG.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-WLS36QIF.mjs → BlockNoteEditor-WUVRCTQI.mjs} +2 -2
  4. package/dist/billing/index.js +299 -299
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-VQ35TGD7.mjs → chunk-BTLJZIDS.mjs} +2612 -2269
  7. package/dist/chunk-BTLJZIDS.mjs.map +1 -0
  8. package/dist/{chunk-KWAUWJYX.js → chunk-YKPIFJOB.js} +544 -201
  9. package/dist/chunk-YKPIFJOB.js.map +1 -0
  10. package/dist/client/index.js +2 -2
  11. package/dist/client/index.mjs +1 -1
  12. package/dist/components/index.d.mts +44 -4
  13. package/dist/components/index.d.ts +44 -4
  14. package/dist/components/index.js +38 -2
  15. package/dist/components/index.js.map +1 -1
  16. package/dist/components/index.mjs +37 -1
  17. package/dist/contexts/index.js +2 -2
  18. package/dist/contexts/index.mjs +1 -1
  19. package/dist/scripts/generate-web-module/generator.d.ts.map +1 -1
  20. package/dist/scripts/generate-web-module/generator.js +62 -4
  21. package/dist/scripts/generate-web-module/generator.js.map +1 -1
  22. package/dist/scripts/generate-web-module/templates/components/container.template.js +12 -43
  23. package/dist/scripts/generate-web-module/templates/components/container.template.js.map +1 -1
  24. package/dist/scripts/generate-web-module/templates/components/details.template.js +7 -5
  25. package/dist/scripts/generate-web-module/templates/components/details.template.js.map +1 -1
  26. package/dist/scripts/generate-web-module/templates/components/editor.template.d.ts.map +1 -1
  27. package/dist/scripts/generate-web-module/templates/components/editor.template.js +28 -17
  28. package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -1
  29. package/dist/scripts/generate-web-module/templates/components/list.template.js +4 -3
  30. package/dist/scripts/generate-web-module/templates/components/list.template.js.map +1 -1
  31. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -1
  32. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +6 -4
  33. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
  34. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -1
  35. package/dist/scripts/generate-web-module/templates/components/selector.template.js +8 -5
  36. package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -1
  37. package/dist/scripts/generate-web-module/templates/context.template.d.ts.map +1 -1
  38. package/dist/scripts/generate-web-module/templates/context.template.js +8 -6
  39. package/dist/scripts/generate-web-module/templates/context.template.js.map +1 -1
  40. package/dist/scripts/generate-web-module/templates/data/aliases.template.d.ts +15 -0
  41. package/dist/scripts/generate-web-module/templates/data/aliases.template.d.ts.map +1 -0
  42. package/dist/scripts/generate-web-module/templates/data/aliases.template.js +31 -0
  43. package/dist/scripts/generate-web-module/templates/data/aliases.template.js.map +1 -0
  44. package/dist/scripts/generate-web-module/templates/data/fields.template.js +2 -2
  45. package/dist/scripts/generate-web-module/templates/data/fields.template.js.map +1 -1
  46. package/dist/scripts/generate-web-module/templates/data/index.d.ts +1 -0
  47. package/dist/scripts/generate-web-module/templates/data/index.d.ts.map +1 -1
  48. package/dist/scripts/generate-web-module/templates/data/index.js +1 -0
  49. package/dist/scripts/generate-web-module/templates/data/index.js.map +1 -1
  50. package/dist/scripts/generate-web-module/templates/data/interface.template.js +11 -5
  51. package/dist/scripts/generate-web-module/templates/data/interface.template.js.map +1 -1
  52. package/dist/scripts/generate-web-module/templates/data/model.template.js +25 -16
  53. package/dist/scripts/generate-web-module/templates/data/model.template.js.map +1 -1
  54. package/dist/scripts/generate-web-module/templates/data/service.template.js +5 -1
  55. package/dist/scripts/generate-web-module/templates/data/service.template.js.map +1 -1
  56. package/dist/scripts/generate-web-module/templates/index.d.ts +1 -1
  57. package/dist/scripts/generate-web-module/templates/index.d.ts.map +1 -1
  58. package/dist/scripts/generate-web-module/templates/index.js +2 -1
  59. package/dist/scripts/generate-web-module/templates/index.js.map +1 -1
  60. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.d.ts.map +1 -1
  61. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js +6 -7
  62. package/dist/scripts/generate-web-module/templates/pages/detail-page.template.js.map +1 -1
  63. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js +3 -3
  64. package/dist/scripts/generate-web-module/templates/pages/list-page.template.js.map +1 -1
  65. package/dist/scripts/generate-web-module/transformers/field-mapper.js +5 -5
  66. package/dist/scripts/generate-web-module/transformers/field-mapper.js.map +1 -1
  67. package/dist/scripts/generate-web-module/transformers/i18n-generator.js +2 -2
  68. package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -1
  69. package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts +1 -0
  70. package/dist/scripts/generate-web-module/transformers/import-resolver.d.ts.map +1 -1
  71. package/dist/scripts/generate-web-module/transformers/import-resolver.js +1 -0
  72. package/dist/scripts/generate-web-module/transformers/import-resolver.js.map +1 -1
  73. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts +3 -3
  74. package/dist/scripts/generate-web-module/transformers/relationship-resolver.d.ts.map +1 -1
  75. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js +19 -13
  76. package/dist/scripts/generate-web-module/transformers/relationship-resolver.js.map +1 -1
  77. package/dist/scripts/generate-web-module/types/field-mapping.types.d.ts +4 -2
  78. package/dist/scripts/generate-web-module/types/field-mapping.types.d.ts.map +1 -1
  79. package/dist/scripts/generate-web-module/types/field-mapping.types.js +23 -7
  80. package/dist/scripts/generate-web-module/types/field-mapping.types.js.map +1 -1
  81. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts +2 -0
  82. package/dist/scripts/generate-web-module/types/template-data.interface.d.ts.map +1 -1
  83. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.js +2 -2
  84. package/dist/scripts/generate-web-module/utils/bootstrapper-updater.js.map +1 -1
  85. package/dist/scripts/generate-web-module/utils/file-writer.d.ts +6 -0
  86. package/dist/scripts/generate-web-module/utils/file-writer.d.ts.map +1 -1
  87. package/dist/scripts/generate-web-module/utils/file-writer.js +22 -0
  88. package/dist/scripts/generate-web-module/utils/file-writer.js.map +1 -1
  89. package/dist/scripts/generate-web-module/utils/i18n-updater.js +13 -13
  90. package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -1
  91. package/dist/scripts/generate-web-module/utils/index.d.ts +1 -1
  92. package/dist/scripts/generate-web-module/utils/index.d.ts.map +1 -1
  93. package/dist/scripts/generate-web-module/utils/index.js +2 -1
  94. package/dist/scripts/generate-web-module/utils/index.js.map +1 -1
  95. package/package.json +2 -2
  96. package/scripts/generate-web-module/generator.ts +70 -5
  97. package/scripts/generate-web-module/templates/components/container.template.ts +12 -43
  98. package/scripts/generate-web-module/templates/components/details.template.ts +7 -5
  99. package/scripts/generate-web-module/templates/components/editor.template.ts +27 -17
  100. package/scripts/generate-web-module/templates/components/list.template.ts +4 -3
  101. package/scripts/generate-web-module/templates/components/multi-selector.template.ts +6 -4
  102. package/scripts/generate-web-module/templates/components/selector.template.ts +8 -5
  103. package/scripts/generate-web-module/templates/context.template.ts +8 -6
  104. package/scripts/generate-web-module/templates/data/aliases.template.ts +33 -0
  105. package/scripts/generate-web-module/templates/data/fields.template.ts +2 -2
  106. package/scripts/generate-web-module/templates/data/index.ts +1 -0
  107. package/scripts/generate-web-module/templates/data/interface.template.ts +10 -5
  108. package/scripts/generate-web-module/templates/data/model.template.ts +24 -16
  109. package/scripts/generate-web-module/templates/data/service.template.ts +6 -1
  110. package/scripts/generate-web-module/templates/index.ts +1 -0
  111. package/scripts/generate-web-module/templates/pages/detail-page.template.ts +6 -7
  112. package/scripts/generate-web-module/templates/pages/list-page.template.ts +3 -3
  113. package/scripts/generate-web-module/transformers/field-mapper.ts +5 -5
  114. package/scripts/generate-web-module/transformers/i18n-generator.ts +2 -2
  115. package/scripts/generate-web-module/transformers/import-resolver.ts +2 -0
  116. package/scripts/generate-web-module/transformers/relationship-resolver.ts +19 -13
  117. package/scripts/generate-web-module/types/field-mapping.types.ts +19 -6
  118. package/scripts/generate-web-module/types/template-data.interface.ts +2 -0
  119. package/scripts/generate-web-module/utils/bootstrapper-updater.ts +2 -2
  120. package/scripts/generate-web-module/utils/file-writer.ts +21 -0
  121. package/scripts/generate-web-module/utils/i18n-updater.ts +13 -13
  122. package/scripts/generate-web-module/utils/index.ts +1 -1
  123. package/src/components/containers/RoundPageContainer.tsx +80 -0
  124. package/src/components/containers/RoundPageContainerTitle.tsx +60 -0
  125. package/src/components/containers/index.ts +2 -0
  126. package/src/components/navigations/Header.tsx +3 -2
  127. package/src/shadcnui/index.ts +1 -0
  128. package/src/shadcnui/ui/command.tsx +2 -2
  129. package/dist/chunk-KWAUWJYX.js.map +0 -1
  130. package/dist/chunk-VQ35TGD7.mjs.map +0 -1
  131. /package/dist/{BlockNoteEditor-WLS36QIF.mjs.map → BlockNoteEditor-WUVRCTQI.mjs.map} +0 -0
@@ -13,7 +13,10 @@ import { FrontendTemplateData } from "../../types/template-data.interface";
13
13
  * @returns Generated file content
14
14
  */
15
15
  export function generateSelectorTemplate(data: FrontendTemplateData): string {
16
- const { names } = data;
16
+ const { names, fields, extendsContent } = data;
17
+ const hasNameField = extendsContent || fields.some((f) => f.name === "name");
18
+ const i18nKey = names.pluralCamel.toLowerCase();
19
+ const displayProp = hasNameField ? "name" : "id";
17
20
 
18
21
  return `"use client";
19
22
 
@@ -103,7 +106,7 @@ export default function ${names.pascalCase}Selector({
103
106
  return;
104
107
  }
105
108
 
106
- form.setValue(id, { id: ${names.camelCase}.id, name: ${names.camelCase}.name });
109
+ form.setValue(id, { id: ${names.camelCase}.id, name: ${names.camelCase}.${displayProp} });
107
110
  setOpen(false);
108
111
 
109
112
  setTimeout(() => {
@@ -125,7 +128,7 @@ export default function ${names.pascalCase}Selector({
125
128
  className="h-auto min-h-10 w-full justify-between px-3 py-2"
126
129
  >
127
130
  <span className={field.value ? "" : "text-muted-foreground"}>
128
- {field.value?.name ?? placeholder ?? t(\`generic.search.placeholder\`, { type: t(\`types.${names.pluralKebab}\`, { count: 1 }) })}
131
+ {field.value?.name ?? placeholder ?? t(\`generic.search.placeholder\`, { type: t(\`entities.${i18nKey}\`, { count: 1 }) })}
129
132
  </span>
130
133
  <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
131
134
  </Button>
@@ -140,7 +143,7 @@ export default function ${names.pascalCase}Selector({
140
143
  <PopoverContent className="w-(--radix-popover-trigger-width) p-0" align="start">
141
144
  <Command shouldFilter={false}>
142
145
  <CommandInput
143
- placeholder={t(\`generic.search.placeholder\`, { type: t(\`types.${names.pluralKebab}\`, { count: 1 }) })}
146
+ placeholder={t(\`generic.search.placeholder\`, { type: t(\`entities.${i18nKey}\`, { count: 1 }) })}
144
147
  value={searchTerm}
145
148
  onValueChange={setSearchTerm}
146
149
  />
@@ -162,7 +165,7 @@ export default function ${names.pascalCase}Selector({
162
165
  onSelect={() => set${names.pascalCase}(${names.camelCase})}
163
166
  className="hover:bg-muted data-selected:hover:bg-muted bg-transparent data-selected:bg-transparent cursor-pointer"
164
167
  >
165
- {${names.camelCase}.name}
168
+ {${names.camelCase}.${displayProp}}
166
169
  </CommandItem>
167
170
  ))}
168
171
  </CommandGroup>
@@ -13,7 +13,9 @@ import { FrontendTemplateData } from "../types/template-data.interface";
13
13
  * @returns Generated file content
14
14
  */
15
15
  export function generateContextTemplate(data: FrontendTemplateData): string {
16
- const { names, extendsContent } = data;
16
+ const { names, fields, extendsContent } = data;
17
+ const hasNameField = extendsContent || fields.some((f) => f.name === "name");
18
+ const i18nKey = names.pluralCamel.toLowerCase();
17
19
 
18
20
  return `"use client";
19
21
 
@@ -61,13 +63,13 @@ export const ${names.pascalCase}Provider = ({ children, dehydrated${names.pascal
61
63
  const response: BreadcrumbItemData[] = [];
62
64
 
63
65
  response.push({
64
- name: t(\`types.${names.pluralCamel}\`, { count: 2 }),
66
+ name: t(\`entities.${i18nKey}\`, { count: 2 }),
65
67
  href: generateUrl({ page: Modules.${names.pascalCase} }),
66
68
  });
67
69
 
68
70
  if (${names.camelCase})
69
71
  response.push({
70
- name: ${names.camelCase}.name,
72
+ name: ${hasNameField ? `${names.camelCase}.name` : `${names.camelCase}.id`},
71
73
  href: generateUrl({ page: Modules.${names.pascalCase}, id: ${names.camelCase}.id }),
72
74
  });
73
75
 
@@ -76,13 +78,13 @@ export const ${names.pascalCase}Provider = ({ children, dehydrated${names.pascal
76
78
 
77
79
  const title = () => {
78
80
  const response: any = {
79
- type: t(\`types.${names.pluralCamel}\`, { count: ${names.camelCase} ? 1 : 2 }),
81
+ type: t(\`entities.${i18nKey}\`, { count: ${names.camelCase} ? 1 : 2 }),
80
82
  };
81
83
 
82
84
  const functions: ReactNode[] = [];
83
85
 
84
- if (${names.camelCase}) {
85
- response.element = ${names.camelCase}.name;
86
+ if (${names.camelCase}) {${hasNameField ? `
87
+ response.element = ${names.camelCase}.name;` : ""}
86
88
 
87
89
  functions.push(<${names.pascalCase}Deleter key={\`${names.pascalCase}Deleter\`} ${names.camelCase}={${names.camelCase}} />);
88
90
  functions.push(<${names.pascalCase}Editor key={\`${names.pascalCase}Editor\`} ${names.camelCase}={${names.camelCase}} propagateChanges={set${names.pascalCase}} />);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Aliases Template
3
+ *
4
+ * Generates {Module}Aliases.ts const object for type-safe alias references.
5
+ * Only generated when the module has aliased relationships.
6
+ */
7
+
8
+ import { FrontendTemplateData } from "../../types/template-data.interface";
9
+ import { toCamelCase, toKebabCase, pluralize } from "../../transformers/name-transformer";
10
+
11
+ /**
12
+ * Generate the aliases file content
13
+ *
14
+ * @param data - Frontend template data
15
+ * @returns Generated file content, or null if no aliases exist
16
+ */
17
+ export function generateAliasesTemplate(data: FrontendTemplateData): string | null {
18
+ const { names, relationships } = data;
19
+
20
+ const aliasedRels = relationships.filter((rel) => rel.alias);
21
+ if (aliasedRels.length === 0) return null;
22
+
23
+ const entries = aliasedRels.map((rel) => {
24
+ const endpoint = rel.single
25
+ ? toKebabCase(rel.alias!)
26
+ : pluralize(toKebabCase(rel.alias!));
27
+ const property = toCamelCase(rel.alias!);
28
+
29
+ return ` ${rel.alias}: {\n endpoint: "${endpoint}",\n property: "${property}",\n },`;
30
+ });
31
+
32
+ return `export const ${names.pascalCase}Aliases = {\n${entries.join("\n")}\n} as const;\n`;
33
+ }
@@ -47,10 +47,10 @@ function generateEnumEntries(data: FrontendTemplateData): string {
47
47
 
48
48
  // Add relationship fields
49
49
  relationships.forEach((rel) => {
50
- const effectiveName = rel.variant || rel.name;
50
+ const effectiveName = rel.alias || rel.variant || rel.name;
51
51
  const key = rel.single
52
52
  ? toCamelCase(effectiveName)
53
- : pluralize(toCamelCase(rel.name));
53
+ : pluralize(toCamelCase(effectiveName));
54
54
  entries.push(` ${key} = "${key}",`);
55
55
  });
56
56
 
@@ -8,3 +8,4 @@ export * from "./interface.template";
8
8
  export * from "./model.template";
9
9
  export * from "./service.template";
10
10
  export * from "./fields.template";
11
+ export * from "./aliases.template";
@@ -35,11 +35,14 @@ function generateImports(data: FrontendTemplateData): string {
35
35
  const { names, extendsContent, relationships } = data;
36
36
  const imports: string[] = [];
37
37
 
38
- // Relationship interface imports (skip self-referential - interface is defined in this file)
38
+ // Relationship interface imports (deduplicated, skip self-referential)
39
+ const seenInterfaces = new Set<string>();
39
40
  relationships.forEach((rel) => {
40
41
  if (rel.name === names.pascalCase) {
41
42
  return; // Skip self-reference - interface defined in this file
42
43
  }
44
+ if (seenInterfaces.has(rel.interfaceName)) return;
45
+ seenInterfaces.add(rel.interfaceName);
43
46
  imports.push(`import { ${rel.interfaceName} } from "${rel.interfaceImportPath}";`);
44
47
  });
45
48
 
@@ -73,7 +76,7 @@ function generateInputType(data: FrontendTemplateData): string {
73
76
 
74
77
  // Add relationship IDs and relationship property fields
75
78
  relationships.forEach((rel) => {
76
- const effectiveName = rel.variant || rel.name;
79
+ const effectiveName = rel.alias || rel.variant || rel.name;
77
80
  if (rel.single) {
78
81
  const key = `${toCamelCase(effectiveName)}Id`;
79
82
  const optional = rel.nullable ? "?" : "";
@@ -87,7 +90,8 @@ function generateInputType(data: FrontendTemplateData): string {
87
90
  });
88
91
  }
89
92
  } else {
90
- const key = `${toCamelCase(rel.name)}Ids`;
93
+ const effectiveMany = rel.alias || rel.name;
94
+ const key = `${toCamelCase(effectiveMany)}Ids`;
91
95
  fieldLines.push(` ${key}?: string[];`);
92
96
  }
93
97
  });
@@ -122,7 +126,7 @@ function generateInterface(data: FrontendTemplateData): string {
122
126
 
123
127
  // Add relationship getters
124
128
  relationships.forEach((rel) => {
125
- const effectiveName = rel.variant || rel.name;
129
+ const effectiveName = rel.alias || rel.variant || rel.name;
126
130
  if (rel.single) {
127
131
  const propertyName = toCamelCase(effectiveName);
128
132
 
@@ -136,7 +140,8 @@ function generateInterface(data: FrontendTemplateData): string {
136
140
  const type = rel.nullable ? `(${baseType}) | undefined` : baseType;
137
141
  getterLines.push(` get ${propertyName}(): ${type};`);
138
142
  } else {
139
- const propertyName = pluralize(toCamelCase(rel.name));
143
+ const effectiveMany = rel.alias || rel.name;
144
+ const propertyName = pluralize(toCamelCase(effectiveMany));
140
145
  // Use intersection type if relationship has fields (edge properties)
141
146
  if (rel.fields && rel.fields.length > 0) {
142
147
  const metaFields = rel.fields.map((f) => `${f.name}?: ${f.tsType}`).join("; ");
@@ -51,11 +51,14 @@ function generateImports(data: FrontendTemplateData): string {
51
51
  `import { ${names.pascalCase}Input, ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";`,
52
52
  );
53
53
 
54
- // Relationship interface imports (skip self-referential - already imported above)
54
+ // Relationship interface imports (deduplicated, skip self-referential)
55
+ const seenInterfaces = new Set<string>();
55
56
  relationships.forEach((rel) => {
56
57
  if (rel.name === names.pascalCase) {
57
58
  return; // Skip self-reference - interface already imported
58
59
  }
60
+ if (seenInterfaces.has(rel.interfaceName)) return;
61
+ seenInterfaces.add(rel.interfaceName);
59
62
  imports.push(`import { ${rel.interfaceName} } from "${rel.interfaceImportPath}";`);
60
63
  });
61
64
 
@@ -91,7 +94,7 @@ function generatePrivateFields(data: FrontendTemplateData): string {
91
94
  // Relationship private members
92
95
  if (!extendsContent) {
93
96
  relationships.forEach((rel) => {
94
- const effectiveName = rel.variant || rel.name;
97
+ const effectiveName = rel.alias || rel.variant || rel.name;
95
98
  if (rel.single) {
96
99
  const propName = toCamelCase(effectiveName);
97
100
  // Use intersection type if relationship has fields
@@ -102,7 +105,8 @@ function generatePrivateFields(data: FrontendTemplateData): string {
102
105
  }
103
106
  lines.push(` private _${propName}?: ${typeDecl};`);
104
107
  } else {
105
- const propName = pluralize(toCamelCase(rel.name));
108
+ const effectiveMany = rel.alias || rel.name;
109
+ const propName = pluralize(toCamelCase(effectiveMany));
106
110
  // Use intersection type if relationship has fields (edge properties)
107
111
  if (rel.fields && rel.fields.length > 0) {
108
112
  const metaFields = rel.fields.map((f) => `${f.name}?: ${f.tsType}`).join("; ");
@@ -139,7 +143,7 @@ function generateGetters(data: FrontendTemplateData): string {
139
143
  // Relationship getters (only for non-Content extending)
140
144
  if (!extendsContent) {
141
145
  relationships.forEach((rel) => {
142
- const effectiveName = rel.variant || rel.name;
146
+ const effectiveName = rel.alias || rel.variant || rel.name;
143
147
  if (rel.single) {
144
148
  const propName = toCamelCase(effectiveName);
145
149
 
@@ -161,7 +165,8 @@ function generateGetters(data: FrontendTemplateData): string {
161
165
  }`);
162
166
  }
163
167
  } else {
164
- const propName = pluralize(toCamelCase(rel.name));
168
+ const effectiveMany = rel.alias || rel.name;
169
+ const propName = pluralize(toCamelCase(effectiveMany));
165
170
  // Use intersection type if relationship has fields (edge properties)
166
171
  if (rel.fields && rel.fields.length > 0) {
167
172
  const metaFields = rel.fields.map((f) => `${f.name}?: ${f.tsType}`).join("; ");
@@ -197,7 +202,7 @@ function generateRehydrateMethod(data: FrontendTemplateData): string {
197
202
  : fields;
198
203
 
199
204
  fieldsToInclude.forEach((field) => {
200
- if (field.isContentField || field.name === "content") {
205
+ if (field.isContentField) {
201
206
  // Content fields need JSON parsing
202
207
  lines.push(
203
208
  ` this._${field.name} = data.jsonApi.attributes.${field.name} ? JSON.parse(data.jsonApi.attributes.${field.name}) : undefined;`,
@@ -215,17 +220,19 @@ function generateRehydrateMethod(data: FrontendTemplateData): string {
215
220
  if (!extendsContent && relationships.length > 0) {
216
221
  lines.push(``);
217
222
  relationships.forEach((rel) => {
218
- const effectiveName = rel.variant || rel.name;
223
+ const effectiveName = rel.alias || rel.variant || rel.name;
219
224
  if (rel.single) {
220
225
  const propName = toCamelCase(effectiveName);
221
- const relationshipKey = effectiveName.toLowerCase();
226
+ const relationshipKey = toCamelCase(effectiveName);
222
227
 
223
228
  // Use _readIncludedWithMeta for relationships with fields
224
229
  if (rel.fields && rel.fields.length > 0) {
225
230
  const metaType = `{ ${rel.fields.map((f) => `${f.name}?: ${f.tsType}`).join("; ")} }`;
226
- const nullableCast = rel.nullable ? ` as (${rel.interfaceName} & ${metaType}) | undefined` : "";
231
+ const singleCast = rel.nullable
232
+ ? ` as (${rel.interfaceName} & ${metaType}) | undefined`
233
+ : ` as ${rel.interfaceName} & ${metaType}`;
227
234
  lines.push(
228
- ` this._${propName} = this._readIncludedWithMeta<${rel.interfaceName}, ${metaType}>(data, "${relationshipKey}", Modules.${rel.name})${nullableCast};`,
235
+ ` this._${propName} = this._readIncludedWithMeta<${rel.interfaceName}, ${metaType}>(data, "${relationshipKey}", Modules.${rel.name})${singleCast};`,
229
236
  );
230
237
  } else {
231
238
  lines.push(
@@ -233,8 +240,9 @@ function generateRehydrateMethod(data: FrontendTemplateData): string {
233
240
  );
234
241
  }
235
242
  } else {
236
- const propName = pluralize(toCamelCase(rel.name));
237
- const relationshipKey = pluralize(rel.name.toLowerCase());
243
+ const effectiveMany = rel.alias || rel.name;
244
+ const propName = pluralize(toCamelCase(effectiveMany));
245
+ const relationshipKey = toCamelCase(effectiveMany);
238
246
 
239
247
  // Use _readIncludedWithMeta for relationships with fields (edge properties)
240
248
  if (rel.fields && rel.fields.length > 0) {
@@ -290,7 +298,7 @@ function generateCreateJsonApiMethod(data: FrontendTemplateData): string {
290
298
  : fields;
291
299
 
292
300
  fieldsToInclude.forEach((field) => {
293
- if (field.isContentField || field.name === "content") {
301
+ if (field.isContentField) {
294
302
  lines.push(
295
303
  ` if (data.${field.name} !== undefined) response.data.attributes.${field.name} = JSON.stringify(data.${field.name});`,
296
304
  );
@@ -307,8 +315,8 @@ function generateCreateJsonApiMethod(data: FrontendTemplateData): string {
307
315
  if (relationshipsToSerialize.length > 0) {
308
316
  lines.push(``);
309
317
  relationshipsToSerialize.forEach((rel) => {
310
- const effectiveName = rel.variant || rel.name;
311
- const payloadKey = rel.single ? `${toCamelCase(effectiveName)}Id` : `${toCamelCase(rel.name)}Ids`;
318
+ const effectiveName = rel.alias || rel.variant || rel.name;
319
+ const payloadKey = rel.single ? `${toCamelCase(effectiveName)}Id` : `${toCamelCase(effectiveName)}Ids`;
312
320
  const relationshipKey = toCamelCase(effectiveName);
313
321
 
314
322
  if (rel.single) {
@@ -355,7 +363,7 @@ function generateCreateJsonApiMethod(data: FrontendTemplateData): string {
355
363
  * Get default value for a field type
356
364
  */
357
365
  function getDefaultValue(field: FrontendField): string {
358
- if (field.isContentField || field.name === "content") {
366
+ if (field.isContentField) {
359
367
  return "[]";
360
368
  }
361
369
 
@@ -108,6 +108,11 @@ function generateRelationshipMethods(data: FrontendTemplateData): string {
108
108
 
109
109
  return relationshipServiceMethods
110
110
  .map((method) => {
111
+ // For aliases, use a raw string endpoint; for standard relationships, use Modules reference
112
+ const endpointValue = method.aliasEndpoint
113
+ ? `"${method.aliasEndpoint}"`
114
+ : `Modules.${method.relationshipName}`;
115
+
111
116
  return ` static async ${method.methodName}(params: {
112
117
  ${method.paramName}: string;
113
118
  search?: string;
@@ -116,7 +121,7 @@ function generateRelationshipMethods(data: FrontendTemplateData): string {
116
121
  prev?: PreviousRef;
117
122
  }): Promise<${names.pascalCase}Interface[]> {
118
123
  const endpoint = new EndpointCreator({
119
- endpoint: Modules.${method.relationshipName},
124
+ endpoint: ${endpointValue},
120
125
  id: params.${method.paramName},
121
126
  childEndpoint: Modules.${names.pascalCase},
122
127
  });
@@ -10,6 +10,7 @@ export {
10
10
  generateModelTemplate,
11
11
  generateServiceTemplate,
12
12
  generateFieldsTemplate,
13
+ generateAliasesTemplate,
13
14
  } from "./data";
14
15
 
15
16
  // Component templates
@@ -13,14 +13,15 @@ import { FrontendTemplateData } from "../../types/template-data.interface";
13
13
  * @returns Generated file content
14
14
  */
15
15
  export function generateDetailPageTemplate(data: FrontendTemplateData): string {
16
- const { names } = data;
16
+ const { names, fields, extendsContent } = data;
17
+ const hasNameField = extendsContent || fields.some((f) => f.name === "name");
18
+ const i18nKey = names.pluralCamel.toLowerCase();
17
19
 
18
20
  return `import ${names.pascalCase}Container from "@/features/${data.importTargetDir}/${names.kebabCase}/components/containers/${names.pascalCase}Container";
19
21
  import { ${names.pascalCase}Provider } from "@/features/${data.importTargetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
20
22
  import { ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
21
23
  import { ${names.pascalCase}Service } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
22
24
  import { generateSpecificMetadata } from "@/utils/metadata";
23
- import { PageContainer } from "@carlonicora/nextjs-jsonapi/components";
24
25
  import { Modules } from "@carlonicora/nextjs-jsonapi/core";
25
26
  import { Action } from "@carlonicora/nextjs-jsonapi/core";
26
27
  import { ServerSession } from "@carlonicora/nextjs-jsonapi/server";
@@ -41,8 +42,8 @@ export async function generateMetadata(props: { params: Promise<{ id: string }>
41
42
  action: Action.Read,
42
43
  data: ${names.camelCase},
43
44
  }))
44
- ? \`[\${t(\`types.${names.pluralCamel}\`, { count: 1 })}] \${${names.camelCase}.name}\`
45
- : \`\${t(\`types.${names.pluralCamel}\`, { count: 1 })}\`;
45
+ ? \`[\${t(\`entities.${i18nKey}\`, { count: 1 })}]${hasNameField ? ` \${${names.camelCase}.name}` : ""}\`
46
+ : \`\${t(\`entities.${i18nKey}\`, { count: 1 })}\`;
46
47
 
47
48
  return await generateSpecificMetadata({ title: title });
48
49
  }
@@ -55,9 +56,7 @@ export default async function ${names.pascalCase}Page(props: { params: Promise<{
55
56
 
56
57
  return (
57
58
  <${names.pascalCase}Provider dehydrated${names.pascalCase}={${names.camelCase}.dehydrate()}>
58
- <PageContainer>
59
- <${names.pascalCase}Container />
60
- </PageContainer>
59
+ <${names.pascalCase}Container />
61
60
  </${names.pascalCase}Provider>
62
61
  );
63
62
  }
@@ -17,7 +17,7 @@ export function generateListPageTemplate(data: FrontendTemplateData): string {
17
17
 
18
18
  return `import ${names.pascalCase}ListContainer from "@/features/${data.importTargetDir}/${names.kebabCase}/components/containers/${names.pascalCase}ListContainer";
19
19
  import { ${names.pascalCase}Provider } from "@/features/${data.importTargetDir}/${names.kebabCase}/contexts/${names.pascalCase}Context";
20
- import { PageContainer } from "@carlonicora/nextjs-jsonapi/components";
20
+ import { RoundPageContainer } from "@carlonicora/nextjs-jsonapi/components";
21
21
  import { Modules } from "@carlonicora/nextjs-jsonapi/core";
22
22
  import { Action } from "@carlonicora/nextjs-jsonapi/core";
23
23
  import { ServerSession } from "@carlonicora/nextjs-jsonapi/server";
@@ -27,9 +27,9 @@ export default async function ${names.pluralPascal}ListPage() {
27
27
 
28
28
  return (
29
29
  <${names.pascalCase}Provider>
30
- <PageContainer testId="page-${names.pluralKebab}-container">
30
+ <RoundPageContainer module={Modules.${names.pascalCase}}>
31
31
  <${names.pascalCase}ListContainer />
32
- </PageContainer>
32
+ </RoundPageContainer>
33
33
  </${names.pascalCase}Provider>
34
34
  );
35
35
  }
@@ -23,7 +23,7 @@ import {
23
23
  */
24
24
  export function mapField(field: JsonFieldDefinition, moduleName: string): FrontendField {
25
25
  const formComponent = getFormComponent(field.name, field.type);
26
- const isContent = isContentField(field.name);
26
+ const isContent = isContentField(field.name, field.type);
27
27
 
28
28
  return {
29
29
  name: field.name,
@@ -56,7 +56,7 @@ export function mapFields(fields: JsonFieldDefinition[], moduleName: string): Fr
56
56
  */
57
57
  export function buildZodSchema(field: JsonFieldDefinition, moduleName: string): string {
58
58
  // Content fields always use z.any()
59
- if (isContentField(field.name)) {
59
+ if (isContentField(field.name, field.type)) {
60
60
  return "z.any()";
61
61
  }
62
62
 
@@ -87,12 +87,12 @@ export function buildZodSchema(field: JsonFieldDefinition, moduleName: string):
87
87
  return base;
88
88
  }
89
89
 
90
- // Date fields
90
+ // Date fields — use z.date() not z.coerce.date() to avoid react-hook-form type inference issues
91
91
  if (field.type === "date") {
92
92
  if (field.nullable) {
93
- return `z.coerce.date().optional()`;
93
+ return `z.date().optional()`;
94
94
  }
95
- return `z.coerce.date()`;
95
+ return `z.date()`;
96
96
  }
97
97
 
98
98
  // Default case
@@ -90,7 +90,7 @@ export function buildI18nMessages(i18nKeys: I18nKeySet): Record<string, any> {
90
90
  relationships: i18nKeys.relationships,
91
91
  },
92
92
  },
93
- types: {
93
+ entities: {
94
94
  [pluralLowercaseKey]: i18nKeys.type.icuPlural,
95
95
  },
96
96
  };
@@ -136,7 +136,7 @@ export function getFieldErrorKey(moduleName: string, fieldName: string): string
136
136
  * @returns Translation key path
137
137
  */
138
138
  export function getTypeKey(pluralKebab: string): string {
139
- return `types.${pluralKebab}`;
139
+ return `entities.${pluralKebab}`;
140
140
  }
141
141
 
142
142
  /**
@@ -171,6 +171,7 @@ export interface ModuleFilePaths {
171
171
  model: string;
172
172
  service: string;
173
173
  fields: string;
174
+ aliases: string;
174
175
  editor: string;
175
176
  deleter: string;
176
177
  selector: string;
@@ -211,6 +212,7 @@ export function buildFilePaths(
211
212
  model: `${basePath}/data/${names.pascalCase}.ts`,
212
213
  service: `${basePath}/data/${names.pascalCase}Service.ts`,
213
214
  fields: `${basePath}/data/${names.pascalCase}Fields.ts`,
215
+ aliases: `${basePath}/data/${names.pascalCase}Aliases.ts`,
214
216
 
215
217
  // Components
216
218
  editor: `${basePath}/components/forms/${names.pascalCase}Editor.tsx`,
@@ -35,7 +35,7 @@ export function isFoundationImport(directory: string): boolean {
35
35
  * @param rel - JSON relationship definition
36
36
  * @returns Frontend relationship representation
37
37
  */
38
- export function resolveRelationship(rel: JsonRelationshipDefinition): FrontendRelationship {
38
+ export function resolveRelationship(rel: JsonRelationshipDefinition, targetHasNameMap?: Map<string, boolean>): FrontendRelationship {
39
39
  const isAuthor = rel.variant === AUTHOR_VARIANT;
40
40
  const effectiveName = rel.alias || rel.variant || rel.name;
41
41
  const effectiveNameLower = toCamelCase(effectiveName);
@@ -50,19 +50,18 @@ export function resolveRelationship(rel: JsonRelationshipDefinition): FrontendRe
50
50
  if (rel.single) {
51
51
  payloadFieldId = `${effectiveNameLower}Id`;
52
52
  } else {
53
- payloadFieldId = `${toCamelCase(rel.name)}Ids`;
53
+ payloadFieldId = `${effectiveNameLower}Ids`;
54
54
  }
55
55
 
56
- // Selector component name
56
+ // Selector component name - always based on target entity name, not alias
57
57
  // Foundation components use MultiSelect, generated modules use MultiSelector
58
58
  let selectorComponent: string;
59
- const selectorBaseName = rel.alias || rel.name;
60
59
  if (rel.single) {
61
- selectorComponent = `${selectorBaseName}Selector`;
60
+ selectorComponent = `${rel.name}Selector`;
62
61
  } else {
63
62
  selectorComponent = isFoundationImport(rel.directory)
64
- ? `${selectorBaseName}MultiSelect`
65
- : `${selectorBaseName}MultiSelector`;
63
+ ? `${rel.name}MultiSelect`
64
+ : `${rel.name}MultiSelector`;
66
65
  }
67
66
 
68
67
  // Zod schema
@@ -122,6 +121,7 @@ export function resolveRelationship(rel: JsonRelationshipDefinition): FrontendRe
122
121
  interfaceName: `${rel.name}Interface`,
123
122
  modelKebab,
124
123
  fields,
124
+ targetHasName: targetHasNameMap?.get(rel.name) ?? true, // Foundation entities default to true
125
125
  };
126
126
  }
127
127
 
@@ -131,8 +131,8 @@ export function resolveRelationship(rel: JsonRelationshipDefinition): FrontendRe
131
131
  * @param relationships - Array of JSON relationship definitions
132
132
  * @returns Array of frontend relationship representations
133
133
  */
134
- export function resolveRelationships(relationships: JsonRelationshipDefinition[]): FrontendRelationship[] {
135
- return relationships.map(resolveRelationship);
134
+ export function resolveRelationships(relationships: JsonRelationshipDefinition[], targetHasNameMap?: Map<string, boolean>): FrontendRelationship[] {
135
+ return relationships.map((rel) => resolveRelationship(rel, targetHasNameMap));
136
136
  }
137
137
 
138
138
  /**
@@ -157,14 +157,18 @@ export function mapDirectoryToWebPath(directory: string): string {
157
157
  * @param relationships - Array of frontend relationships
158
158
  * @returns Array of service method definitions
159
159
  */
160
- export function generateServiceMethods(relationships: FrontendRelationship[]): RelationshipServiceMethod[] {
160
+ export function generateServiceMethods(relationships: FrontendRelationship[], conflictingAliases?: Set<string>): RelationshipServiceMethod[] {
161
161
  return relationships.map((rel) => {
162
162
  const effectiveName = rel.alias || rel.variant || rel.name;
163
+ // Only use alias endpoint when the alias conflicts with another alias targeting the same entity
164
+ const needsAliasEndpoint = rel.alias && (conflictingAliases?.has(rel.alias) ?? false);
163
165
  return {
164
166
  methodName: `findManyBy${toPascalCase(effectiveName)}`,
165
167
  paramName: `${toCamelCase(effectiveName)}Id`,
166
168
  relationshipName: rel.name,
167
169
  relationshipEndpoint: pluralize(toKebabCase(rel.name)),
170
+ // For conflicting aliases, use a raw string endpoint (e.g., "created-by") instead of Modules reference
171
+ aliasEndpoint: needsAliasEndpoint ? toKebabCase(rel.alias!) : undefined,
168
172
  };
169
173
  });
170
174
  }
@@ -219,7 +223,7 @@ export function getRelationshipFormJsx(rel: FrontendRelationship, moduleName: st
219
223
  return `<${rel.name}MultiSelector
220
224
  id="${rel.formFieldId}"
221
225
  form={form}
222
- label={t(\`types.${pluralize(toKebabCase(rel.name))}\`, { count: 2 })}
226
+ label={t(\`entities.${pluralize(rel.name.toLowerCase())}\`, { count: 2 })}
223
227
  />`;
224
228
  }
225
229
  }
@@ -242,13 +246,15 @@ export function getDefaultValueExpression(rel: FrontendRelationship, modelVarNam
242
246
  ? { id: ${modelVarName}.${propertyName}.id, name: ${modelVarName}.${propertyName}.name, avatar: ${modelVarName}.${propertyName}.avatar }
243
247
  : undefined`;
244
248
  }
249
+ const displayProp = rel.targetHasName ? "name" : "id";
245
250
  return `${modelVarName}?.${propertyName}
246
- ? { id: ${modelVarName}.${propertyName}.id, name: ${modelVarName}.${propertyName}.name }
251
+ ? { id: ${modelVarName}.${propertyName}.id, name: ${modelVarName}.${propertyName}.${displayProp} }
247
252
  : undefined`;
248
253
  } else {
249
254
  // Multi-select
255
+ const displayProp = rel.targetHasName ? "name" : "id";
250
256
  return `${modelVarName}?.${pluralPropertyName}
251
- ? ${modelVarName}.${pluralPropertyName}.map((item) => ({ id: item.id, name: item.name }))
257
+ ? ${modelVarName}.${pluralPropertyName}.map((item) => ({ id: item.id, name: item.${displayProp} }))
252
258
  : []`;
253
259
  }
254
260
  }
@@ -98,19 +98,32 @@ export function getZodBase(jsonType: string): string {
98
98
  * Get form component for a field
99
99
  */
100
100
  export function getFormComponent(fieldName: string, fieldType: string): FormComponentType {
101
- // Check for special field names first
101
+ // Check for special field names first, but only if the type is compatible
102
+ // (e.g., "content" as a string field should NOT use BlockNoteEditor)
102
103
  if (SPECIAL_FIELD_COMPONENTS[fieldName]) {
103
- return SPECIAL_FIELD_COMPONENTS[fieldName];
104
+ const specialComponent = SPECIAL_FIELD_COMPONENTS[fieldName];
105
+ if (specialComponent === "BlockNoteEditor" && isContentField(fieldName, fieldType)) {
106
+ return specialComponent;
107
+ } else if (specialComponent !== "BlockNoteEditor") {
108
+ return specialComponent;
109
+ }
104
110
  }
105
111
  // Fall back to type-based component
106
112
  return TYPE_TO_FORM_COMPONENT[fieldType] || "FormInput";
107
113
  }
108
114
 
109
115
  /**
110
- * Check if a field name indicates BlockNoteEditor should be used
116
+ * Check if a field indicates BlockNoteEditor should be used.
117
+ * Only fields named "content" with non-primitive types (not string/number/boolean/date)
118
+ * are treated as rich-content (JSON/BlockNote) fields.
111
119
  */
112
- export function isContentField(fieldName: string): boolean {
113
- return fieldName === "content";
120
+ export function isContentField(fieldName: string, fieldType?: string): boolean {
121
+ if (fieldName !== "content") return false;
122
+ // If no type provided, assume it's a content field (backwards compat)
123
+ if (!fieldType) return true;
124
+ // Primitive types are NOT content fields even when named "content"
125
+ const primitiveTypes = ["string", "number", "boolean", "date", "datetime"];
126
+ return !primitiveTypes.includes(fieldType);
114
127
  }
115
128
 
116
129
  /**
@@ -120,7 +133,7 @@ export function buildZodSchema(fieldType: string, nullable: boolean, fieldName:
120
133
  const base = getZodBase(fieldType);
121
134
 
122
135
  // Content fields use z.any()
123
- if (isContentField(fieldName)) {
136
+ if (isContentField(fieldName, fieldType)) {
124
137
  return "z.any()";
125
138
  }
126
139
 
@@ -67,6 +67,7 @@ export interface FrontendRelationship {
67
67
  interfaceName: string; // e.g., "UserInterface"
68
68
  modelKebab: string; // e.g., "user"
69
69
  fields?: FrontendField[]; // Relationship property fields (stored on edges)
70
+ targetHasName: boolean; // Whether the target entity has a "name" field (or extends Content)
70
71
  }
71
72
 
72
73
  /**
@@ -162,6 +163,7 @@ export interface RelationshipServiceMethod {
162
163
  paramName: string; // e.g., "authorId"
163
164
  relationshipName: string; // e.g., "User"
164
165
  relationshipEndpoint: string; // e.g., "users"
166
+ aliasEndpoint?: string; // e.g., "created-by" — when set, uses raw string instead of Modules reference
165
167
  }
166
168
 
167
169
  /**